diff --git a/docker-compose.yaml b/docker-compose.yaml index 8af4457..13fbc50 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: favorite-places-app: build: . container_name: favorite-places-app - command: uvicorn main:app --log-config logging.conf --reload --host 0.0.0.0 --port=8000 + command: uvicorn main:app --log-config logging.conf --reload --host 0.0.0.0 --port=8000 --proxy-headers --forwarded-allow-ips * ports: - "8010:8000" volumes: diff --git a/src/bootstrap.py b/src/bootstrap.py index 65327cc..e270b8c 100644 --- a/src/bootstrap.py +++ b/src/bootstrap.py @@ -3,6 +3,7 @@ from exceptions import setup_exception_handlers from routes import metadata_tags, setup_routes from settings import settings +from fastapi_pagination import add_pagination def build_app() -> FastAPI: @@ -19,5 +20,6 @@ def build_app() -> FastAPI: setup_routes(app) setup_exception_handlers(app) + add_pagination(app) return app diff --git a/src/clients/geo.py b/src/clients/geo.py index 4941f1c..d612991 100644 --- a/src/clients/geo.py +++ b/src/clients/geo.py @@ -6,7 +6,7 @@ from urllib.parse import urlencode, urljoin import httpx - +import os from clients.base.base import BaseClient from clients.shemas import LocalityDTO @@ -42,11 +42,12 @@ async def get_location( :return: """ - endpoint = "reverse-geocode-client" + endpoint = "reverse-geocode" query_params = { "latitude": latitude, "longitude": longitude, "localityLanguage": "en", + "key": os.getenv("API_KEY"), } url = urljoin( self.base_url, diff --git a/src/exceptions.py b/src/exceptions.py index 440d5d1..6ade967 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -11,7 +11,7 @@ class ApiHTTPException(HTTPException): """Обработка ошибок API.""" status_code: int - code: str + code: str = "api-error" detail: str def __init__( diff --git a/src/schemas/places.py b/src/schemas/places.py index 19b9037..5b78c65 100644 --- a/src/schemas/places.py +++ b/src/schemas/places.py @@ -30,3 +30,9 @@ class PlacesListResponse(ListResponse): """ data: list[Place] + + +class Description(BaseModel): + """Модель для описания любимых мест""" + + description: str = Field(None, min_length=3, max_length=255) \ No newline at end of file diff --git a/src/services/places_service.py b/src/services/places_service.py index 7733f98..c5ebeb4 100644 --- a/src/services/places_service.py +++ b/src/services/places_service.py @@ -99,17 +99,48 @@ async def update_place(self, primary_key: int, place: PlaceUpdate) -> Optional[i :return: """ - # при изменении координат – обогащение данных путем получения дополнительной информации от API - # todo + original_place = await self.places_repository.find(primary_key) + only_description_updated = (original_place.latitude == place.latitude + and original_place.longitude == place.longitude) + + if only_description_updated: + updated_place = original_place + updated_place.description = place.description + + else: + updated_place = Place( + latitude=place.latitude, + longitude=place.longitude, + description=place.description, + ) + # при изменении координат – обогащение данных путем получения дополнительной информации от API + if location := await LocationClient().get_location( + latitude=place.latitude, longitude=place.longitude + ): + updated_place.country = location.alpha2code + updated_place.city = location.city + updated_place.locality = location.locality matched_rows = await self.places_repository.update_model( - primary_key, **place.dict(exclude_unset=True) + primary_key, **updated_place.dict(exclude_unset=True) ) await self.session.commit() # публикация события для попытки импорта информации # по обновленному объекту любимого места в сервисе Countries Informer - # todo + try: + place_data = CountryCityDTO( + city=updated_place.city, + alpha2code=updated_place.country, + ) + EventProducer().publish( + queue_name=settings.rabbitmq.queue.places_import, body=place_data.json() + ) + except ValidationError: + logger.warning( + "The message was not well-formed during publishing event.", + exc_info=True, + ) return matched_rows diff --git a/src/transport/handlers/places.py b/src/transport/handlers/places.py index f19df12..f70d225 100644 --- a/src/transport/handlers/places.py +++ b/src/transport/handlers/places.py @@ -2,7 +2,7 @@ from exceptions import ApiHTTPException, ObjectNotFoundException from models.places import Place -from schemas.places import PlaceResponse, PlacesListResponse, PlaceUpdate +from schemas.places import PlaceResponse, Description, PlaceUpdate from schemas.routes import MetadataTag from services.places_service import PlacesService @@ -18,14 +18,14 @@ @router.get( "", summary="Получение списка объектов", - response_model=PlacesListResponse, + response_model=Page[Place], ) async def get_list( limit: int = Query( 20, gt=0, le=100, description="Ограничение на количество объектов в выборке" ), places_service: PlacesService = Depends(), -) -> PlacesListResponse: +) -> Page[Place]: """ Получение списка любимых мест. @@ -34,7 +34,7 @@ async def get_list( :return: """ - return PlacesListResponse(data=await places_service.get_places_list(limit=limit)) + return paginate(await places_service.get_places_list(limit=limit)) @router.get( @@ -66,7 +66,7 @@ async def get_one( status_code=status.HTTP_201_CREATED, ) async def create( - place: Place, places_service: PlacesService = Depends() + place_update: PlaceUpdate, places_service: PlacesService = Depends() ) -> PlaceResponse: """ Создание нового объекта любимого места по переданным данным. @@ -97,11 +97,15 @@ async def update( Обновление объекта любимого места по переданным данным. :param primary_key: Идентификатор объекта. - :param place: Данные для обновления объекта. + :param place_request: Данные для обновления объекта. :param places_service: Сервис для работы с информацией о любимых местах. :return: """ - + place = Place( + description=place_update.description, + longitude=place_update.longitude, + latitude=place_update.latitude, + ) if not await places_service.update_place(primary_key, place): raise ObjectNotFoundException @@ -127,18 +131,35 @@ async def delete(primary_key: int, places_service: PlacesService = Depends()) -> @router.post( - "", + "/auto", summary="Создание нового объекта с автоматическим определением координат", response_model=PlaceResponse, status_code=status.HTTP_201_CREATED, ) -async def create_auto() -> PlaceResponse: +async def create_auto(request: Request, + description: Description, + places_service: PlacesService = Depends(),) -> PlaceResponse: """ Создание нового объекта любимого места с автоматическим определением координат. :return: """ + geo_info: IpinfoQuery = geocoder.ip("me") + if geo_info.latlng: + latitude, longitude = geo_info.latlng + place = Place( + description=description.description, + longitude=longitude, + latitude=latitude, + ) + + if primary_key := await places_service.create_place(place): + return PlaceResponse(data=await places_service.get_place(primary_key)) + raise ApiHTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Объект не был создан", + ) # Пример: # # import geocoder