Skip to content

Commit

Permalink
[DOP-20049] add PATCH 'v1/locations/{location_id}' endpoint (#101)
Browse files Browse the repository at this point in the history
* [DOP-20049] add PATCH 'v1/locations/{location_id}' endpoint

* [DOP-20049] add test with external_id=None

* [DOP-20049] rebase, fix naming

* [DOP-20049] remove 'to_exception' method from horizon
  • Loading branch information
TiGrib authored Nov 5, 2024
1 parent bc8c585 commit 753c6f1
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 11 deletions.
10 changes: 10 additions & 0 deletions data_rentgen/db/repositories/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from data_rentgen.db.repositories.base import Repository
from data_rentgen.db.utils.search import make_tsquery, ts_match, ts_rank
from data_rentgen.dto import LocationDTO, PaginationDTO
from data_rentgen.exceptions.entity import EntityNotFoundError


class LocationRepository(Repository[Location]):
Expand Down Expand Up @@ -91,6 +92,15 @@ async def paginate(
page_size=page_size,
)

async def update_external_id(self, location_id: int, external_id: str | None) -> Location:
query = select(Location).where(Location.id == location_id)
location = await self._session.scalar(query)
if not location:
raise EntityNotFoundError("Location", "id", location_id)
location.external_id = external_id
await self._session.flush([location])
return location

async def _get(self, location: LocationDTO) -> Location | None:
by_name = select(Location).where(Location.type == location.type, Location.name == location.name)
by_addresses = (
Expand Down
10 changes: 10 additions & 0 deletions data_rentgen/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2024 MTS PJSC
# SPDX-License-Identifier: Apache-2.0

from data_rentgen.exceptions.base import ApplicationError
from data_rentgen.exceptions.entity import EntityNotFoundError

__all__ = [
"ApplicationError",
"EntityNotFoundError",
]
File renamed without changes.
46 changes: 46 additions & 0 deletions data_rentgen/exceptions/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: 2024 MTS PJSC
# SPDX-License-Identifier: Apache-2.0
from typing import Any

from data_rentgen.exceptions.base import ApplicationError


class EntityNotFoundError(ApplicationError):
"""Entity not found.
Examples
--------
>>> from data_rentgen.exceptions import EntityNotFoundError
>>> raise EntityNotFoundError("User", "username", "test")
Traceback (most recent call last):
data_rentgen.exceptions.entity.EntityNotFoundError: User with username='test' not found
"""

entity_type: str
"""Entity type"""

field: str
"""Entity identifier field"""

value: Any
"""Entity identifier value"""

def __init__(self, entity_type: str, field: str, value: Any):
self.entity_type = entity_type
self.field = field
self.value = value

@property
def message(self) -> str:
if self.field is not None:
return f"{self.entity_type} with {self.field}={self.value!r} not found"
return f"{self.entity_type} not found"

@property
def details(self) -> dict[str, Any]:
return {
"entity_type": self.entity_type,
"field": self.field,
"value": self.value,
}
2 changes: 1 addition & 1 deletion data_rentgen/server/api/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError

from data_rentgen.exceptions import ApplicationError
from data_rentgen.server.errors.base import APIErrorSchema, BaseErrorSchema
from data_rentgen.server.errors.registration import get_response_for_exception
from data_rentgen.server.exceptions import ApplicationError
from data_rentgen.server.settings.server import ServerSettings

logger = logging.getLogger(__name__)
Expand Down
15 changes: 13 additions & 2 deletions data_rentgen/server/api/v1/router/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
from fastapi import APIRouter, Depends

from data_rentgen.server.errors import get_error_responses
from data_rentgen.server.errors.schemas import InvalidRequestSchema
from data_rentgen.server.errors.schemas import InvalidRequestSchema, NotFoundSchema
from data_rentgen.server.schemas.v1 import (
LocationPaginateQueryV1,
LocationResponseV1,
PageResponseV1,
UpdateLocationRequestV1,
)
from data_rentgen.services import UnitOfWork

router = APIRouter(
prefix="/locations",
tags=["Locations"],
responses=get_error_responses(include={InvalidRequestSchema}),
responses=get_error_responses(include={InvalidRequestSchema, NotFoundSchema}),
)


Expand All @@ -33,3 +34,13 @@ async def paginate_locations(
search_query=query_args.search_query,
)
return PageResponseV1[LocationResponseV1].from_pagination(pagination)


@router.patch("/{location_id}")
async def update_location(
location_id: int,
location_data: UpdateLocationRequestV1,
unit_of_work: Annotated[UnitOfWork, Depends()],
) -> LocationResponseV1:
location = await unit_of_work.location.update_external_id(location_id, location_data.external_id)
return LocationResponseV1.model_validate(location)
2 changes: 2 additions & 0 deletions data_rentgen/server/errors/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# SPDX-FileCopyrightText: 2024 MTS PJSC
# SPDX-License-Identifier: Apache-2.0
from data_rentgen.server.errors.schemas.invalid_request import InvalidRequestSchema
from data_rentgen.server.errors.schemas.not_found import NotFoundSchema

__all__ = [
"InvalidRequestSchema",
"NotFoundSchema",
]
26 changes: 26 additions & 0 deletions data_rentgen/server/errors/schemas/not_found.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2024 MTS PJSC
# SPDX-License-Identifier: Apache-2.0
import http
from typing import Any

from pydantic import BaseModel
from typing_extensions import Literal

from data_rentgen.exceptions.entity import EntityNotFoundError
from data_rentgen.server.errors.base import BaseErrorSchema
from data_rentgen.server.errors.registration import register_error_response


class NotFoundDetailsSchema(BaseModel):
entity_type: str
field: str
value: Any


@register_error_response(
exception=EntityNotFoundError,
status=http.HTTPStatus.NOT_FOUND,
)
class NotFoundSchema(BaseErrorSchema):
code: Literal["not_found"] = "not_found"
details: NotFoundDetailsSchema
8 changes: 0 additions & 8 deletions data_rentgen/server/exceptions/__init__.py

This file was deleted.

2 changes: 2 additions & 0 deletions data_rentgen/server/schemas/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from data_rentgen.server.schemas.v1.location import (
LocationPaginateQueryV1,
LocationResponseV1,
UpdateLocationRequestV1,
)
from data_rentgen.server.schemas.v1.operation import (
OperationQueryV1,
Expand Down Expand Up @@ -51,6 +52,7 @@
"LineageResponseV1",
"LocationPaginateQueryV1",
"LocationResponseV1",
"UpdateLocationRequestV1",
"PageMetaResponseV1",
"PageResponseV1",
"OperationQueryV1",
Expand Down
6 changes: 6 additions & 0 deletions data_rentgen/server/schemas/v1/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ class LocationPaginateQueryV1(PaginateQueryV1):
)

model_config = ConfigDict(extra="forbid")


class UpdateLocationRequestV1(BaseModel):
external_id: str | None = Field(description="External ID for integration with other systems")

model_config = ConfigDict(extra="forbid")
101 changes: 101 additions & 0 deletions tests/test_server/test_locations/test_patch_locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from http import HTTPStatus

import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from data_rentgen.db.models import Location
from tests.test_server.utils.enrich import enrich_locations

pytestmark = [pytest.mark.server, pytest.mark.asyncio]


@pytest.mark.parametrize(
"location",
[
pytest.param({"external_id": None}),
],
indirect=True,
)
async def test_add_loction_external_id(
test_client: AsyncClient,
async_session: AsyncSession,
location: Location,
):
[location] = await enrich_locations([location], async_session)
assert location.external_id is None

response = await test_client.patch(
f"v1/locations/{location.id}",
json={"external_id": "external_id"},
)
[location] = await enrich_locations([location], async_session)

assert response.status_code == HTTPStatus.OK, response.json()
assert response.json() == {
"name": location.name,
"type": location.type,
"addresses": [{"url": address.url} for address in location.addresses],
"external_id": "external_id",
}


async def test_update_location_external_id(
test_client: AsyncClient,
async_session: AsyncSession,
location: Location,
):
response = await test_client.patch(
f"v1/locations/{location.id}",
json={"external_id": "new_external_id"},
)
[location] = await enrich_locations([location], async_session)

assert response.status_code == HTTPStatus.OK, response.json()
assert response.json() == {
"name": location.name,
"type": location.type,
"addresses": [{"url": address.url} for address in location.addresses],
"external_id": "new_external_id",
}


async def test_update_location_not_found(
test_client: AsyncClient,
new_location: Location,
):
response = await test_client.patch(
f"v1/locations/{new_location.id}",
json={"external_id": "new_external_id"},
)

assert response.status_code == HTTPStatus.NOT_FOUND, response.json()
assert response.json() == {
"error": {
"code": "not_found",
"details": {
"entity_type": "Location",
"field": "id",
"value": new_location.id,
},
"message": f"Location with id={new_location.id} not found",
},
}


async def test_update_location_writing_null_to_external_id(
test_client: AsyncClient,
location: Location,
):
response = await test_client.patch(
f"v1/locations/{location.id}",
json={"external_id": None},
)

assert response.status_code == HTTPStatus.OK, response.json()
assert response.json() == {
"name": location.name,
"type": location.type,
"addresses": [{"url": address.url} for address in location.addresses],
"external_id": None,
}

0 comments on commit 753c6f1

Please sign in to comment.