Skip to content

Commit 213fcf8

Browse files
committed
refactor(api): Reorganize API structure to v1 versioning and enhance pagination validation
1 parent a053f7c commit 213fcf8

30 files changed

Lines changed: 418 additions & 81 deletions

app/controller/api/dummy/views.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

app/controller/api/router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi.routing import APIRouter
22

3-
from app.controller.api import docs, dummy, echo, kafka, monitoring, rabbit, redis, users
3+
from app.controller.api.v1 import docs, dummy, echo, kafka, monitoring, rabbit, redis, users
44

55
api_router = APIRouter()
66
api_router.include_router(monitoring.router)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Routes for swagger and redoc."""
22

3-
from app.controller.api.docs.views import router
3+
from app.controller.api.v1.docs.views import router
44

55
__all__ = ["router"]
Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
1-
from pydantic import BaseModel, ConfigDict, Field
1+
from pydantic import BaseModel, Field
22

33
from app.controller.utils.pagination import Pagination
44

55

6-
class DummyModelDTO(BaseModel):
7-
"""
8-
DTO for dummy models.
9-
10-
It returned when accessing dummy models from the API.
11-
"""
12-
13-
id: int
14-
name: str
15-
16-
model_config = ConfigDict(from_attributes=True)
17-
18-
19-
class DummyModelInputDTO(BaseModel):
20-
"""DTO for creating new dummy model."""
21-
22-
name: str
23-
24-
256
class DummyDataResponse(BaseModel):
267
"""Model for the data returned by a Dummy API endpoint."""
278

@@ -41,7 +22,7 @@ class DummyUpdate(BaseModel):
4122
name: str | None = Field(default=None, examples=["John Doe"])
4223

4324

44-
class CustomerListResponse(BaseModel):
25+
class DummyListResponse(BaseModel):
4526
"""Model for the response of a dummy API endpoint.
4627
4728
This class represents the structure of the response returned by an customer API endpoint.
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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+
)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import APIRouter
22

3-
from app.controller.api.echo.schema import Message
3+
from app.controller.api.v1.echo.schema import Message
44

55
router = APIRouter()
66

app/controller/api/v1/errors/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)