Skip to content

Commit c331c21

Browse files
authored
docs: Add comprehensive Python docstrings throughout codebase (#27)
1 parent 1c1b494 commit c331c21

25 files changed

Lines changed: 1300 additions & 20 deletions

fastpubsub/api/app.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""FastAPI application setup and configuration."""
2+
13
from fastapi import FastAPI, Request, status
24
from fastapi.responses import ORJSONResponse
35
from prometheus_fastapi_instrumentator import Instrumentator
@@ -36,6 +38,14 @@
3638

3739

3840
def create_app() -> FastAPI:
41+
"""Create and configure the FastAPI application.
42+
43+
Sets up the complete application including middleware, exception handlers,
44+
routers, and monitoring instrumentation.
45+
46+
Returns:
47+
Configured FastAPI application instance.
48+
"""
3949
app = FastAPI(
4050
title="fastpubsub",
4151
description="Simple pubsub system based on FastAPI and PostgreSQL.",
@@ -49,22 +59,77 @@ def create_app() -> FastAPI:
4959
# Add exception handlers
5060
@app.exception_handler(AlreadyExistsError)
5161
def already_exists_exception_handler(request: Request, exc: AlreadyExistsError):
62+
"""Handle AlreadyExistsError exceptions.
63+
64+
Returns a 409 Conflict response when attempting to create resources that already exist.
65+
66+
Args:
67+
request: The incoming HTTP request.
68+
exc: The AlreadyExistsError exception.
69+
70+
Returns:
71+
JSON error response with 409 status code.
72+
"""
5273
return _create_error_response(models.GenericError, status.HTTP_409_CONFLICT, exc)
5374

5475
@app.exception_handler(NotFoundError)
5576
def not_found_exception_handler(request: Request, exc: NotFoundError):
77+
"""Handle NotFoundError exceptions.
78+
79+
Returns a 404 Not Found response when requesting non-existent resources.
80+
81+
Args:
82+
request: The incoming HTTP request.
83+
exc: The NotFoundError exception.
84+
85+
Returns:
86+
JSON error response with 404 status code.
87+
"""
5688
return _create_error_response(models.GenericError, status.HTTP_404_NOT_FOUND, exc)
5789

5890
@app.exception_handler(ServiceUnavailable)
5991
def service_unavailable_exception_handler(request: Request, exc: ServiceUnavailable):
92+
"""Handle ServiceUnavailable exceptions.
93+
94+
Returns a 503 Service Unavailable response when services are unavailable.
95+
96+
Args:
97+
request: The incoming HTTP request.
98+
exc: The ServiceUnavailable exception.
99+
100+
Returns:
101+
JSON error response with 503 status code.
102+
"""
60103
return _create_error_response(models.GenericError, status.HTTP_503_SERVICE_UNAVAILABLE, exc)
61104

62105
@app.exception_handler(InvalidClient)
63106
def invalid_client_exception_handler(request: Request, exc: InvalidClient):
107+
"""Handle InvalidClient exceptions.
108+
109+
Returns a 401 Unauthorized response when client authentication fails.
110+
111+
Args:
112+
request: The incoming HTTP request.
113+
exc: The InvalidClient exception.
114+
115+
Returns:
116+
JSON error response with 401 status code.
117+
"""
64118
return _create_error_response(models.GenericError, status.HTTP_401_UNAUTHORIZED, exc)
65119

66120
@app.exception_handler(InvalidClientToken)
67121
def invalid_client_token_exception_handler(request: Request, exc: InvalidClientToken):
122+
"""Handle InvalidClientToken exceptions.
123+
124+
Returns a 403 Forbidden response when client token is invalid or expired.
125+
126+
Args:
127+
request: The incoming HTTP request.
128+
exc: The InvalidClientToken exception.
129+
130+
Returns:
131+
JSON error response with 403 status code.
132+
"""
68133
return _create_error_response(models.GenericError, status.HTTP_403_FORBIDDEN, exc)
69134

70135
# Add routers

fastpubsub/api/helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
"""Helper functions for API responses and error handling."""
2+
13
from fastapi.encoders import jsonable_encoder
24
from fastapi.responses import JSONResponse
35

46

57
def _create_error_response(model_class, status_code: int, exc: Exception):
6-
"""Helper to create error responses."""
8+
"""Create a standardized error response JSON object.
9+
10+
Args:
11+
model_class: Pydantic model class for the error response.
12+
status_code: HTTP status code for the error.
13+
exc: Exception instance containing the error message.
14+
15+
Returns:
16+
JSONResponse with formatted error content and appropriate status code.
17+
"""
718
response = jsonable_encoder(model_class(detail=exc.args[0]))
819
return JSONResponse(status_code=status_code, content=response)

fastpubsub/api/middlewares.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""HTTP middleware for request logging and monitoring."""
2+
13
import time
24
from uuid import uuid7
35

@@ -9,6 +11,26 @@
911

1012

1113
async def log_requests(request: Request, call_next):
14+
"""Middleware to log HTTP requests and responses with timing and request IDs.
15+
16+
This middleware:
17+
- Generates a unique request ID for tracking
18+
- Logs request details at the start
19+
- Measures processing time
20+
- Logs response details including status code and timing
21+
- Adds request ID header to response
22+
- Handles and logs any exceptions during request processing
23+
24+
Args:
25+
request: The incoming FastAPI request.
26+
call_next: The next middleware or endpoint to call.
27+
28+
Returns:
29+
The processed HTTP response.
30+
31+
Raises:
32+
Exception: Re-raises any exceptions encountered during processing.
33+
"""
1234
start_time = time.perf_counter()
1335
request_id = str(uuid7())
1436
logger.info(

fastpubsub/api/routers/clients.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""API endpoints for client management operations."""
2+
13
import uuid
24
from typing import Annotated
35

@@ -18,6 +20,22 @@ async def create_client(
1820
data: models.CreateClient,
1921
token: Annotated[models.DecodedClientToken, Depends(services.require_scope("clients", "create"))],
2022
):
23+
"""Create a new client with specified name and scopes.
24+
25+
Creates a new authorized client that can access the pub/sub API
26+
based on their granted scopes. Returns the client ID and generated secret.
27+
28+
Args:
29+
data: Client creation data including name, scopes, and active status.
30+
token: Decoded client token with 'clients:create' scope.
31+
32+
Returns:
33+
CreateClientResult containing the new client ID and secret.
34+
35+
Raises:
36+
AlreadyExistsError: If a client with the same ID already exists.
37+
InvalidClient: If the requesting client lacks 'clients:create' scope.
38+
"""
2139
return await services.create_client(data)
2240

2341

@@ -32,6 +50,22 @@ async def get_client(
3250
id: uuid.UUID,
3351
token: Annotated[models.DecodedClientToken, Depends(services.require_scope("clients", "read"))],
3452
):
53+
"""Retrieve a client by ID.
54+
55+
Returns the full details of an existing client including ID, name,
56+
scopes, status, and timestamps.
57+
58+
Args:
59+
id: UUID of the client to retrieve.
60+
token: Decoded client token with 'clients:read' scope.
61+
62+
Returns:
63+
Client model with full client details.
64+
65+
Raises:
66+
NotFoundError: If no client with the given ID exists.
67+
InvalidClient: If the requesting client lacks 'clients:read' scope.
68+
"""
3569
return await services.get_client(id)
3670

3771

@@ -47,6 +81,23 @@ async def update_client(
4781
data: models.UpdateClient,
4882
token: Annotated[models.DecodedClientToken, Depends(services.require_scope("clients", "update"))],
4983
):
84+
"""Update an existing client's name, scopes, or active status.
85+
86+
Modifies the properties of an existing client. Only the fields
87+
provided in the update data will be modified.
88+
89+
Args:
90+
id: UUID of the client to update.
91+
data: Updated client data including name, scopes, and/or active status.
92+
token: Decoded client token with 'clients:update' scope.
93+
94+
Returns:
95+
Client model with updated details.
96+
97+
Raises:
98+
NotFoundError: If no client with the given ID exists.
99+
InvalidClient: If the requesting client lacks 'clients:update' scope.
100+
"""
50101
return await services.update_client(id, data)
51102

52103

@@ -61,6 +112,21 @@ async def list_client(
61112
offset: int = Query(default=0, ge=0),
62113
limit: int = Query(default=10, ge=1, le=100),
63114
):
115+
"""List clients with pagination support.
116+
117+
Returns a paginated list of all clients in the system.
118+
119+
Args:
120+
token: Decoded client token with 'clients:read' scope.
121+
offset: Number of items to skip (for pagination).
122+
limit: Maximum number of items to return (1-100).
123+
124+
Returns:
125+
ListClientAPI containing the list of clients.
126+
127+
Raises:
128+
InvalidClient: If the requesting client lacks 'clients:read' scope.
129+
"""
64130
clients = await services.list_client(offset, limit)
65131
return models.ListClientAPI(data=clients)
66132

@@ -75,6 +141,18 @@ async def delete_client(
75141
id: uuid.UUID,
76142
token: Annotated[models.DecodedClientToken, Depends(services.require_scope("clients", "delete"))],
77143
):
144+
"""Delete a client by ID.
145+
146+
Permanently removes a client from the system. This action cannot be undone.
147+
148+
Args:
149+
id: UUID of the client to delete.
150+
token: Decoded client token with 'clients:delete' scope.
151+
152+
Raises:
153+
NotFoundError: If no client with the given ID exists.
154+
InvalidClient: If the requesting client lacks 'clients:delete' scope.
155+
"""
78156
await services.delete_client(id)
79157

80158

@@ -85,4 +163,20 @@ async def delete_client(
85163
summary="Issue a new client token",
86164
)
87165
async def issue_client_token(data: models.IssueClientToken):
166+
"""Issue a new JWT access token for a client.
167+
168+
Generates a new access token that the client can use for authentication
169+
in subsequent API requests. The token includes the client's scopes
170+
and has an expiration time.
171+
172+
Args:
173+
data: Client credentials including ID and secret for authentication.
174+
175+
Returns:
176+
ClientToken containing the access token, type, expiration, and scopes.
177+
178+
Raises:
179+
InvalidClient: If client ID or secret is invalid.
180+
ServiceUnavailable: If token generation service is unavailable.
181+
"""
88182
return await services.issue_jwt_client_token(client_id=data.client_id, client_secret=data.client_secret)

fastpubsub/api/routers/monitoring.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""API endpoints for monitoring and health check operations."""
2+
13
from fastapi import APIRouter, status
24

35
from fastpubsub import models, services
@@ -13,6 +15,15 @@
1315
summary="Liveness probe",
1416
)
1517
async def liveness_probe():
18+
"""Check if the application is alive.
19+
20+
Simple liveness check that always returns "alive" status.
21+
Used by Kubernetes and other orchestration systems to determine
22+
if the application process is running.
23+
24+
Returns:
25+
HealthCheck model with status "alive".
26+
"""
1627
return models.HealthCheck(status="alive")
1728

1829

@@ -24,6 +35,18 @@ async def liveness_probe():
2435
summary="Readiness probe",
2536
)
2637
async def readiness_probe():
38+
"""Check if the application is ready to serve traffic.
39+
40+
Comprehensive health check that verifies database connectivity.
41+
Returns "ready" status only if all critical dependencies are available.
42+
Used by Kubernetes to determine if the application can handle requests.
43+
44+
Returns:
45+
HealthCheck model with status "ready".
46+
47+
Raises:
48+
ServiceUnavailable: If database connection fails.
49+
"""
2750
try:
2851
is_db_ok = await services.database_ping()
2952
if not is_db_ok:

0 commit comments

Comments
 (0)