Skip to content

Commit

Permalink
feat: add takes an ID, new api call create id
Browse files Browse the repository at this point in the history
  • Loading branch information
majsan committed Nov 14, 2024
1 parent a979143 commit 6124dea
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 18 deletions.
69 changes: 60 additions & 9 deletions karp/api/routes/entries_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,66 @@ def get_history_for_entry(
return entry_queries.get_entry_history(resource_id, entry_id, version=version)


@router.get(
"/create_id",
status_code=status.HTTP_200_OK,
tags=["Editing"],
description="""
Create an ID (ULID) to be used as input for `add/<resource_id>/<entry_id>`.
""",
)
def create_id():
return schemas.EntryAddResponse(newID=unique_id.make_unique_id().str)


@router.put(
"/{resource_id}",
status_code=status.HTTP_201_CREATED,
response_model=schemas.EntryAddResponse,
tags=["Editing"],
deprecated=True,
description="Depracated, use `add/<resource_id>/<entry_id>` instead.",
)
def add_entry(
resource_id: str,
data: schemas.EntryAdd,
user: User = Depends(deps.get_user),
resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permission_queries),
entry_commands: EntryCommands = Depends(inject_from_req(EntryCommands)),
entry_queries: EntryQueries = Depends(deps.get_entry_queries),
published_resources: List[str] = Depends(deps.get_published_resources),
):
entry_id = unique_id.make_unique_id()
return add_entry(
resource_id, entry_id, data, user, resource_permissions, entry_commands, entry_queries, published_resources
)


@router.put(
"/{resource_id}/{entry_id}",
status_code=status.HTTP_201_CREATED,
response_model=schemas.EntryAddResponse,
tags=["Editing"],
description="""
Add a new entry. For data consistency reasons, first generate an ID (ULID), for example using the `create_id` API
call. If the request fails, use the same ID to try again, this ensures that the entry body is not added several
times. Answers:
- 201 created if the entry exists with the same body, at version 1
- 400
- if the entry_id exists, but the body is different
- if the entry_id is not valid
- if the entry is not valid according to resource settings
""",
)
def add_entry(
resource_id: str,
entry_id: UniqueIdStr,
data: schemas.EntryAdd,
user: User = Depends(deps.get_user),
resource_permissions: ResourcePermissionQueries = Depends(deps.get_resource_permission_queries),
entry_commands: EntryCommands = Depends(inject_from_req(EntryCommands)),
entry_queries: EntryQueries = Depends(deps.get_entry_queries),
published_resources: List[str] = Depends(deps.get_published_resources),
):
if not resource_permissions.has_permission(PermissionLevel.write, user, [resource_id]):
Expand All @@ -74,20 +122,23 @@ def add_entry(
raise ResourceNotFound(resource_id)
logger.info("adding entry", extra={"resource_id": resource_id, "data": data})
try:
new_entry = entry_commands.add_entry(
entry_commands.add_entry(
resource_id=resource_id,
entry_id=entry_id,
user=user.identifier,
message=data.message,
entry=data.entry,
)
except errors.IntegrityError as exc:
return responses.JSONResponse(
status_code=400,
content={
"error": str(exc),
"errorCode": karp_errors.ClientErrorCodes.DB_INTEGRITY_ERROR,
},
)
existing_entry = entry_queries.by_id_optional(resource_id, entry_id, expand_plugins=False)
if not existing_entry or existing_entry.version != 1 or existing_entry.entry != data.entry:
return responses.JSONResponse(
status_code=400,
content={
"error": str(exc),
"errorCode": karp_errors.ClientErrorCodes.DB_INTEGRITY_ERROR,
},
)
except errors.InvalidEntry as exc:
return responses.JSONResponse(
status_code=400,
Expand All @@ -97,7 +148,7 @@ def add_entry(
},
)

return {"newID": new_entry.id}
return schemas.EntryAddResponse(newID=entry_id)


# must go before update_entry otherwise it thinks this is an
Expand Down
2 changes: 2 additions & 0 deletions karp/cliapp/subapps/entries_subapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tqdm import tqdm

from karp.entry_commands import EntryCommands
from karp.foundation.value_objects import unique_id
from karp.lex.domain.value_objects import entry_schema, ResourceConfig

from karp.cliapp.utility import cli_error_handler, cli_timer
Expand Down Expand Up @@ -40,6 +41,7 @@ def add_entries_to_resource(
user = user or "local admin"
message = message or "imported through cli"
entries = tqdm(json_arrays.load_from_file(data), desc="Adding", unit=" entries")
entries = ((unique_id.make_unique_id(), entry) for entry in entries)
if chunked:
entry_commands.add_entries_in_chunks(
resource_id=resource_id,
Expand Down
40 changes: 31 additions & 9 deletions karp/entry_commands.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from collections import defaultdict
from typing import Any, Generator, Iterable
from typing import Any, Dict, Generator, Iterable, List, Tuple

import sqlalchemy
from injector import inject
from sqlalchemy.orm import Session

from karp import plugins
from karp.foundation.timings import utc_now
from karp.foundation.value_objects import unique_id
from karp.foundation.value_objects import UniqueId, unique_id
from karp.lex import EntryDto
from karp.lex.domain import errors
from karp.lex.domain.entities import Resource
from karp.lex.domain.errors import EntryNotFound, ResourceNotFound
from karp.lex.infrastructure import EntryRepository, ResourceRepository
Expand Down Expand Up @@ -54,7 +56,15 @@ def _transform_entries(self, config, entries: Iterable[EntryDto]) -> Generator[I
entries = plugins.transform_entries(self.plugins, config, entries)
return (entry_transformer.transform(config, entry) for entry in entries)

def add_entries_in_chunks(self, resource_id, chunk_size, entries, user, message, timestamp=None):
def add_entries_in_chunks(
self,
resource_id: str,
chunk_size: int,
entries: Iterable[Tuple[UniqueId, Dict]],
user: str,
message: str,
timestamp: float | None = None,
) -> list[EntryDto]:
"""
Add entries to DB and INDEX (if present and resource is active).
Expand All @@ -76,12 +86,12 @@ def add_entries_in_chunks(self, resource_id, chunk_size, entries, user, message,
resource = self._get_resource(resource_id)

entry_table = self._get_entries(resource_id)
for i, entry_raw in enumerate(entries):
for i, (entry_id, entry_raw) in enumerate(entries):
entry = resource.create_entry_from_dict(
entry_raw,
user=user,
message=message,
id=unique_id.make_unique_id(),
id=entry_id,
timestamp=timestamp,
)
entry_table.save(entry)
Expand All @@ -96,7 +106,14 @@ def add_entries_in_chunks(self, resource_id, chunk_size, entries, user, message,

return created_db_entries

def add_entries(self, resource_id, entries, user, message, timestamp=None):
def add_entries(
self,
resource_id: str,
entries: Iterable[Tuple[UniqueId, Dict]],
user: str,
message: str,
timestamp: float | None = None,
):
return self.add_entries_in_chunks(resource_id, 0, entries, user, message, timestamp=timestamp)

def import_entries(self, resource_id, entries, user, message):
Expand Down Expand Up @@ -145,8 +162,8 @@ def import_entries_in_chunks(self, resource_id, chunk_size, entries, user, messa

return created_db_entries

def add_entry(self, resource_id, entry, user, message, timestamp=None):
result = self.add_entries(resource_id, [entry], user, message, timestamp=timestamp)
def add_entry(self, resource_id, entry_id, entry, user, message, timestamp=None):
result = self.add_entries(resource_id, [(entry_id, entry)], user, message, timestamp=timestamp)
assert len(result) == 1 # noqa: S101
return result[0]

Expand Down Expand Up @@ -198,7 +215,12 @@ def _commit(self):
if self.in_transaction:
return

self.session.commit()
try:
self.session.commit()
except sqlalchemy.exc.IntegrityError as e:
self.session.rollback()
raise errors.IntegrityError(str(e)) from None

for resource_id, entry_dtos in self.added_entries.items():
resource = self._get_resource(resource_id)
index_entries = self._transform_entries(resource.config, entry_dtos)
Expand Down
68 changes: 68 additions & 0 deletions tests/e2e/test_entries_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,71 @@ def test_preview_entry(
assert response_data["municipality"] == [m["code"] for m in response_data["_municipality"]]
del response_data["_municipality"]
assert response_data == entry


def create(client, token, entry_id, entry=None):
if not entry:
entry = {
"code": 203,
"name": "add203",
"municipality": [2, 3],
}
body = {"entry": entry}
response = client.put(
f"/entries/places/{entry_id}",
json=body,
headers=token.as_header(),
)
return response.status_code


def delete(client, token, entry_id):
response = client.delete(
f"/entries/places/{entry_id}/1",
headers=token.as_header(),
)
return response.status_code


def test_add_with_id_many_times(
fa_data_client,
write_token: AccessToken,
):
entry_id = fa_data_client.get("/entries/create_id").json()["newID"]

try:
for _ in range(0, 10):
status_code = create(fa_data_client, write_token, entry_id)
assert status_code == 201
finally:
# delete it after the test is done to not affect other tests
delete(fa_data_client, write_token, entry_id)


def test_add_with_id_many_times_changes(
fa_data_client,
write_token: AccessToken,
):
entry_id = fa_data_client.get("/entries/create_id").json()["newID"]
entry = {"code": 203, "name": "add203", "municipality": [1]}
try:
# first create it
status_code = create(fa_data_client, write_token, entry_id, entry=entry)
assert status_code == 201
for i in range(0, 3):
# then try to create it again with different body, should not work
entry["name"] = entry["name"] + str(i)
status_code = create(fa_data_client, write_token, entry_id, entry=entry)
assert status_code == 400
finally:
# delete it after the test is done to not affect other tests
delete(fa_data_client, write_token, entry_id)


# assert response.status_code == 201
# response_data = response.json()
# assert "newID" in response_data
# new_id = unique_id.parse(response_data["newID"])
#
# entries = get_entries(fa_data_client.app.state.app_context.injector, resource_id="places")
# assert new_id in entries.entity_ids()

0 comments on commit 6124dea

Please sign in to comment.