|
| 1 | +from typing import Annotated, Any |
| 2 | + |
| 3 | +from fastapi import APIRouter, Body, Depends, Path, Query, Request, status |
| 4 | +from fastapi.responses import JSONResponse, Response |
| 5 | +from loguru import logger |
| 6 | +from pydantic.v1 import UUID4 |
| 7 | +from sqlalchemy.ext.asyncio import AsyncSession |
| 8 | + |
| 9 | +from app.controller.api.v1.dummy.schema import ( |
| 10 | + DummyCreate, |
| 11 | + DummyDataResponse, |
| 12 | + DummyListResponse, |
| 13 | + DummyUpdate, |
| 14 | +) |
| 15 | +from app.controller.api.v1.errors.deps import compose_responses |
| 16 | +from app.controller.errors.exceptions import HTTP404NotFoundError, HTTP500InternalServerError |
| 17 | +from app.controller.utils.pagination import MAX_LIMIT, MAX_OFFSET, Pagination |
| 18 | +from app.controller.utils.query_parameters import common_query_parameters |
| 19 | +from app.db.dependencies import get_db_session |
| 20 | +from app.db.exceptions import ElementNotFoundError |
| 21 | +from app.services.dummy.service import DummyService |
| 22 | + |
| 23 | +ROUT_TAGS = ["Dummy"] |
| 24 | + |
| 25 | +router = APIRouter() |
| 26 | + |
| 27 | +CommonDeps = Annotated[dict[str, Any], Depends(common_query_parameters)] |
| 28 | + |
| 29 | + |
| 30 | +@router.get( |
| 31 | + "/", |
| 32 | + responses=compose_responses({200: {"model": DummyListResponse, "description": "OK."}}), |
| 33 | + tags=ROUT_TAGS, |
| 34 | + summary="List of Dummy models.", |
| 35 | + response_model_by_alias=True, |
| 36 | + response_model=DummyListResponse, |
| 37 | +) |
| 38 | +async def get_dummies( |
| 39 | + request: Request, |
| 40 | + http_request_info: CommonDeps, |
| 41 | + db_connection: Annotated[AsyncSession, Depends(get_db_session)], |
| 42 | + name: Annotated[str | None, Query(description="Filter dummies by name.")] = None, |
| 43 | + dummy_id: Annotated[int | None, Query(description="Filter dummies by ID.")] = None, |
| 44 | + limit: Annotated[ |
| 45 | + int, |
| 46 | + Query( |
| 47 | + description="Number of records returned per page." |
| 48 | + " If specified on entry, this will be the value of the query," |
| 49 | + " otherwise it will be the value value set by default.", |
| 50 | + ge=1, |
| 51 | + le=MAX_LIMIT, |
| 52 | + ), |
| 53 | + ] = 10, |
| 54 | + offset: Annotated[ |
| 55 | + int, |
| 56 | + Query( |
| 57 | + description="Record number from which you want to receive" |
| 58 | + " the number of records indicated in the limit." |
| 59 | + " If it is indicated at the entry, it will be the value of the query." |
| 60 | + " If it is not indicated at the input, as the query is on the first page," |
| 61 | + " its value will be 0.", |
| 62 | + ge=0, |
| 63 | + le=MAX_OFFSET, |
| 64 | + ), |
| 65 | + ] = 0, |
| 66 | +) -> JSONResponse: |
| 67 | + """ |
| 68 | + Retrieve a paginated list of dummy models, optionally filtered by name or ID. |
| 69 | +
|
| 70 | + Args: |
| 71 | + request: The current HTTP request. |
| 72 | + http_request_info: Common HTTP headers. |
| 73 | + db_connection: SQLAlchemy async session. |
| 74 | + name: Optional; filter by dummy name. |
| 75 | + dummy_id: Optional; filter by dummy ID. |
| 76 | + limit: Pagination size. |
| 77 | + offset: Pagination offset. |
| 78 | +
|
| 79 | + Returns: |
| 80 | + JSONResponse: List of dummies with pagination metadata. |
| 81 | +
|
| 82 | + Raises: |
| 83 | + HTTP500InternalServerError: If database access fails. |
| 84 | + """ |
| 85 | + logger.info(logger.info("Entering...")) |
| 86 | + try: |
| 87 | + response_data, db_count = await DummyService.get_dummies( |
| 88 | + db_connection=db_connection, limit=limit, offset=offset, name=name, dummy_id=dummy_id |
| 89 | + ) |
| 90 | + logger.debug("Dummies retrieved.") |
| 91 | + except Exception as error: |
| 92 | + logger.exception(f"Error getting dummies: {error}") |
| 93 | + raise HTTP500InternalServerError from error |
| 94 | + |
| 95 | + pagination = Pagination.get_pagination( |
| 96 | + offset=offset, |
| 97 | + limit=limit, |
| 98 | + total_elements=db_count, |
| 99 | + url=str(request.url), |
| 100 | + ) |
| 101 | + response = DummyListResponse(data=response_data, pagination=pagination) |
| 102 | + logger.info("Exiting...") |
| 103 | + return JSONResponse( |
| 104 | + content=response.model_dump(), status_code=status.HTTP_200_OK, headers=http_request_info |
| 105 | + ) |
| 106 | + |
| 107 | + |
| 108 | +@router.post( |
| 109 | + "/", |
| 110 | + responses=compose_responses({201: {"description": "Created."}}), |
| 111 | + tags=ROUT_TAGS, |
| 112 | + summary="Create a new dummy.", |
| 113 | + response_model_by_alias=True, |
| 114 | +) |
| 115 | +async def post_dummy( |
| 116 | + request: Request, |
| 117 | + http_request_info: CommonDeps, |
| 118 | + db_connection: Annotated[AsyncSession, Depends(get_db_session)], |
| 119 | + dummy_body: Annotated[DummyCreate, Body()], |
| 120 | +) -> Response: |
| 121 | + """ |
| 122 | + Create a new dummy model. |
| 123 | +
|
| 124 | + Args: |
| 125 | + request: The HTTP request. |
| 126 | + http_request_info: Common HTTP headers. |
| 127 | + db_connection: SQLAlchemy async session. |
| 128 | + dummy_body: DummyCreate schema instance. |
| 129 | +
|
| 130 | + Returns: |
| 131 | + Response: 201 Created with location header of the new resource. |
| 132 | +
|
| 133 | + Raises: |
| 134 | + HTTP500InternalServerError: If creation fails. |
| 135 | + """ |
| 136 | + logger.info("Entering...") |
| 137 | + try: |
| 138 | + dummy_id = await DummyService.post_dummy( |
| 139 | + db_connection, |
| 140 | + dummy_body, |
| 141 | + ) |
| 142 | + logger.debug(f"Created dummy ID: {dummy_id}") |
| 143 | + except Exception as error: |
| 144 | + logger.exception("Error creating a dummy.") |
| 145 | + raise HTTP500InternalServerError from error |
| 146 | + |
| 147 | + # Merge standard headers with dynamic location for resource creation |
| 148 | + headers = http_request_info | { |
| 149 | + "location": f"{request.url.scheme}://{request.url.netloc}/dummy/{dummy_id}", |
| 150 | + } |
| 151 | + logger.info("Exiting...") |
| 152 | + return Response(status_code=status.HTTP_201_CREATED, headers=headers) |
| 153 | + |
| 154 | + |
| 155 | +@router.put( |
| 156 | + "/{dummy_id}", |
| 157 | + responses=compose_responses({204: {"description": "No Content."}}), |
| 158 | + tags=ROUT_TAGS, |
| 159 | + summary="Update information from a dummy.", |
| 160 | + response_model_by_alias=True, |
| 161 | +) |
| 162 | +async def put_dummy_with_id( |
| 163 | + dummy_id: Annotated[UUID4, Path(description="Id of a specific dummy.")], |
| 164 | + http_request_info: CommonDeps, |
| 165 | + db_connection: Annotated[AsyncSession, Depends(get_db_session)], |
| 166 | + dummy_body: Annotated[DummyUpdate, Body()], |
| 167 | +) -> Response: |
| 168 | + """ |
| 169 | + Update a dummy model by its UUID. |
| 170 | +
|
| 171 | + Args: |
| 172 | + dummy_id: UUID4 of the dummy. |
| 173 | + http_request_info: Common HTTP headers. |
| 174 | + db_connection: SQLAlchemy async session. |
| 175 | + dummy_body: Update data. |
| 176 | +
|
| 177 | + Returns: |
| 178 | + Response: 204 No Content on success. |
| 179 | +
|
| 180 | + Raises: |
| 181 | + HTTP404NotFoundError: If the dummy does not exist. |
| 182 | + HTTP500InternalServerError: On other failures. |
| 183 | + """ |
| 184 | + logger.info("Entering...") |
| 185 | + try: |
| 186 | + await DummyService.put_dummy( |
| 187 | + db_connection, |
| 188 | + dummy_id, |
| 189 | + dummy_body, |
| 190 | + ) |
| 191 | + logger.debug(f"Updated dummy with ID: {dummy_id}") |
| 192 | + |
| 193 | + except ElementNotFoundError as error: |
| 194 | + logger.exception(f"Dummy with id={dummy_id} not found") |
| 195 | + raise HTTP404NotFoundError from error |
| 196 | + |
| 197 | + except Exception as error: |
| 198 | + logger.exception(f"Error updating dummy with ID {dummy_id}.") |
| 199 | + raise HTTP500InternalServerError from error |
| 200 | + logger.info("Exiting...") |
| 201 | + return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info) |
| 202 | + |
| 203 | + |
| 204 | +@router.delete( |
| 205 | + "/{dummy_id}", |
| 206 | + responses=compose_responses({204: {"description": "No Content."}}), |
| 207 | + tags=ROUT_TAGS, |
| 208 | + summary="Delete specific dummy.", |
| 209 | + response_model=None, |
| 210 | +) |
| 211 | +async def delete_dummy( |
| 212 | + dummy_id: Annotated[UUID4, Path(description="Id of a specific dummy.")], |
| 213 | + http_request_info: CommonDeps, |
| 214 | + db_connection: Annotated[AsyncSession, Depends(get_db_session)], |
| 215 | +) -> Response: |
| 216 | + """ |
| 217 | + Delete a dummy model by UUID. |
| 218 | +
|
| 219 | + Args: |
| 220 | + dummy_id: UUID4 of the dummy. |
| 221 | + http_request_info: Common HTTP headers. |
| 222 | + db_connection: SQLAlchemy async session. |
| 223 | +
|
| 224 | + Returns: |
| 225 | + Response: 204 No Content. |
| 226 | +
|
| 227 | + Raises: |
| 228 | + HTTP404NotFoundError: If the dummy does not exist. |
| 229 | + HTTP500InternalServerError: On other failures. |
| 230 | + """ |
| 231 | + logger.info("Entering...") |
| 232 | + try: |
| 233 | + await DummyService.delete_dummy( |
| 234 | + db_connection, |
| 235 | + dummy_id, |
| 236 | + ) |
| 237 | + logger.debug(f"Deleted dummy with ID: {dummy_id}") |
| 238 | + |
| 239 | + except ElementNotFoundError as error: |
| 240 | + logger.exception(f"Dummy with id={dummy_id} not found") |
| 241 | + raise HTTP404NotFoundError from error |
| 242 | + |
| 243 | + except Exception as error: |
| 244 | + logger.exception(f"Error deleting dummy with ID {dummy_id}.") |
| 245 | + raise HTTP500InternalServerError from error |
| 246 | + logger.info("Exiting...") |
| 247 | + return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info) |
| 248 | + |
| 249 | + |
| 250 | +@router.get( |
| 251 | + "{dummy_id}", |
| 252 | + responses=compose_responses({200: {"model": DummyDataResponse, "description": "OK."}}), |
| 253 | + tags=ROUT_TAGS, |
| 254 | + summary="Get a dummy.", |
| 255 | + response_model=DummyDataResponse, |
| 256 | + response_model_by_alias=True, |
| 257 | +) |
| 258 | +async def get_dummy( |
| 259 | + dummy_id: Annotated[UUID4, Path(description="Id of a specific dummy.")], |
| 260 | + http_request_info: CommonDeps, |
| 261 | + db_connection: Annotated[AsyncSession, Depends(get_db_session)], |
| 262 | +) -> JSONResponse: |
| 263 | + """ |
| 264 | + Retrieve a single dummy by UUID. |
| 265 | +
|
| 266 | + Args: |
| 267 | + dummy_id: UUID4 of the dummy. |
| 268 | + http_request_info: Common HTTP headers. |
| 269 | + db_connection: SQLAlchemy async session. |
| 270 | +
|
| 271 | + Returns: |
| 272 | + JSONResponse: Dummy model. |
| 273 | +
|
| 274 | + Raises: |
| 275 | + HTTP404NotFoundError: If not found. |
| 276 | + HTTP500InternalServerError: On unexpected error. |
| 277 | + """ |
| 278 | + logger.info("Entering...") |
| 279 | + try: |
| 280 | + api_data = await DummyService.get_dummy_id(db_connection, dummy_id) |
| 281 | + logger.debug(f"Retrieved dummy with ID: {dummy_id}") |
| 282 | + |
| 283 | + except ElementNotFoundError as error: |
| 284 | + logger.exception(f"Dummy with id={dummy_id} not found") |
| 285 | + raise HTTP404NotFoundError from error |
| 286 | + |
| 287 | + except Exception as error: |
| 288 | + logger.exception(f"Error retrieving dummy with ID {dummy_id}.") |
| 289 | + raise HTTP500InternalServerError from error |
| 290 | + |
| 291 | + logger.info("Exiting...") |
| 292 | + return JSONResponse( |
| 293 | + content=api_data.model_dump(), status_code=status.HTTP_200_OK, headers=http_request_info |
| 294 | + ) |
0 commit comments