- Add python-development-database skill: metaorm patterns for tables, repositories, filters, settings, transactions, pagination, and exceptions - Add python-development-telegram-bot skill: aiogram bot service with polling/webhook, router structure, middlewares, inline keyboards, and pagination - Add python-development-tests skill: test organization guidelines with pytest, fixtures, and dependency injection rules - Update python-development-stack: replace SQLModel/SQLAlchemy with metaorm; add pytest, pytest-asyncio, pytest-cov, faker - Update python-development-fastapi: add sections for dependencies and list endpoints with ListResponse schema - Update python-development-code-style: allow ctx as exception for typer.Context parameter - Update python-development index with links to all three new skills Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
225 lines
7.3 KiB
Markdown
225 lines
7.3 KiB
Markdown
---
|
|
name: python-development-fastapi
|
|
description: Patterns for building a FastAPI HTTP service inside the facet service tree. Use when creating REST endpoints, routers, or the api/rest service package.
|
|
---
|
|
|
|
## Rules
|
|
- The FastAPI service lives in a sub-package named `api/rest/` inside the root package. It is a regular facet service: it has `service.py`, `settings.py`, and `__init__.py`.
|
|
- Provide a custom `UvicornServer` that disables signal handlers so the parent facet orchestrator controls startup and shutdown. Example:
|
|
```python
|
|
import uvicorn
|
|
|
|
class UvicornServer(uvicorn.Server):
|
|
def install_signal_handlers(self):
|
|
pass
|
|
```
|
|
- The service class inherits from `facet.AsyncioServiceMixin`. Its constructor accepts the settings instance declared in the same package's `settings.py`. Keep the constructor minimal; heavy initialization happens in `start()`.
|
|
- The `start()` method builds the FastAPI application asynchronously, configures uvicorn, and adds the server coroutine to the facet task list with `self.add_task(server.serve())`.
|
|
- Extract app construction into an `async` `_build_app()` method so router registration and dependency wiring can use `await`.
|
|
- Register routers in `_setup_app()` or `_build_app()` using `app.include_router(...)`. Group related endpoints under routers with a `prefix`.
|
|
- The settings class inherits from `pydantic.BaseModel` (not `BaseSettings`). It declares the minimum required for the HTTP layer: `host`, `port`, and references to other subsystem settings when the REST layer wires them directly.
|
|
|
|
### Minimal complete example
|
|
|
|
`api/rest/settings.py`:
|
|
```python
|
|
from pydantic import BaseModel, conint
|
|
|
|
|
|
class RESTAPISettings(BaseModel):
|
|
host: str = "127.0.0.1"
|
|
port: conint(ge=1, le=65535) = 8000
|
|
```
|
|
|
|
`api/rest/service.py`:
|
|
```python
|
|
import facet
|
|
import fastapi
|
|
import uvicorn
|
|
|
|
from . import handlers, users
|
|
from .settings import RESTAPISettings
|
|
|
|
|
|
class UvicornServer(uvicorn.Server):
|
|
def install_signal_handlers(self):
|
|
pass
|
|
|
|
|
|
class RESTAPIService(facet.AsyncioServiceMixin):
|
|
def __init__(self, settings: RESTAPISettings):
|
|
self._settings = settings
|
|
|
|
async def start(self):
|
|
app = await self._build_app()
|
|
config = uvicorn.Config(
|
|
app=app,
|
|
host=self._settings.host,
|
|
port=self._settings.port,
|
|
)
|
|
server = UvicornServer(config)
|
|
self.add_task(server.serve())
|
|
|
|
async def _build_app(self) -> fastapi.FastAPI:
|
|
app = fastapi.FastAPI()
|
|
await self._setup_app(app)
|
|
return app
|
|
|
|
async def _setup_app(self, app: fastapi.FastAPI):
|
|
app.get("/health")(handlers.health)
|
|
app.include_router(users.get_users, prefix="/users")
|
|
```
|
|
|
|
### Router and handler structure
|
|
|
|
- Endpoints must be RESTful. The URL path structure maps directly to the package structure inside `api/rest/`.
|
|
- Each URL segment becomes a sub-package. For example `/users/{id}` lives in `api/rest/users/`.
|
|
- Every handler sub-package contains at minimum:
|
|
- `router.py` — defines `get_router() -> fastapi.APIRouter` and wires each route to a handler function.
|
|
- `handlers.py` — plain async functions that implement the endpoint logic.
|
|
- `schemas.py` — Pydantic request and response models shared by the handlers in this package.
|
|
- `__init__.py` — exports only `get_router`.
|
|
- Optional but common additional files:
|
|
- `dependencies.py` — FastAPI dependency functions reused across handlers in the package.
|
|
- `exceptions.py` — custom HTTP exceptions for this domain.
|
|
|
|
### Dependencies
|
|
|
|
- Declare reusable dependencies in `dependencies.py` inside the handler sub-package. Dependencies are plain functions or async functions that return a value needed by endpoints (database session, current user, settings, etc.).
|
|
- Use `fastapi.Depends` in `router.py` to wire dependencies to routes. Avoid calling dependencies directly in handlers.
|
|
- Keep dependencies stateless; if they need configuration, accept it as an argument and use a closure or a callable class when wiring.
|
|
|
|
Example `api/rest/users/dependencies.py`:
|
|
```python
|
|
import fastapi
|
|
|
|
from project.api.rest.exceptions import HTTPUnauthorized
|
|
from project.api.rest.dependencies import database
|
|
|
|
|
|
async def current_user(
|
|
database: DatabaseService = fastapi.Depends(database),
|
|
token: str = fastapi.Depends(tokens.oauth2_scheme),
|
|
) -> User:
|
|
db_token = await database.get_token(token)
|
|
if db_token is None:
|
|
raise HTTPUnauthorized()
|
|
return await database.get_user(filter_=UserFilter(id=db_token.user_id))
|
|
```
|
|
|
|
Example `api/rest/users/schemas.py`:
|
|
```python
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
user_id: int
|
|
requested_by: str
|
|
```
|
|
|
|
Example `api/rest/users/handlers.py` consuming a dependency:
|
|
```python
|
|
import fastapi
|
|
|
|
from . import dependencies, schemas
|
|
|
|
|
|
async def get_user(
|
|
id: int,
|
|
user: User = fastapi.Depends(dependencies.current_user),
|
|
) -> schemas.UserResponse:
|
|
return schemas.UserResponse(user_id=id, requested_by=user.username)
|
|
```
|
|
|
|
### List endpoints
|
|
|
|
- Use a shared `ListResponse` schema for endpoints that return collections. It is defined once in `api/rest/schemas.py` and reused across packages.
|
|
- Extract pagination and search via a reusable `pagination` dependency declared in `api/rest/dependencies.py`.
|
|
|
|
Example `api/rest/schemas.py`:
|
|
```python
|
|
from typing import Any
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class ListResponse(BaseModel):
|
|
total: int
|
|
next: str | None
|
|
previous: str | None
|
|
data: list[Any]
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
...
|
|
|
|
|
|
class UsersResponse(ListResponse):
|
|
data: list[UserResponse]
|
|
```
|
|
|
|
Example `api/rest/dependencies.py`:
|
|
```python
|
|
import fastapi
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class Pagination(BaseModel):
|
|
cursor: str | None = None
|
|
limit: int = 20
|
|
search: str | None = None
|
|
order_by: str | None = None
|
|
|
|
|
|
def pagination(
|
|
cursor: str | None = fastapi.Query(default=None),
|
|
limit: int = fastapi.Query(default=20, ge=1),
|
|
search: str | None = fastapi.Query(default=None),
|
|
order_by: str | None = fastapi.Query(default=None)
|
|
) -> PaginationParams:
|
|
return PaginationParams(cursor=cursor, limit=limit, search=search)
|
|
```
|
|
|
|
Example `api/rest/users/handlers.py` using the list schema and pagination dependency:
|
|
```python
|
|
import fastapi
|
|
|
|
from api.rest.dependencies import PaginationParams, pagination
|
|
from . import schemas
|
|
|
|
|
|
async def get_users(
|
|
pagination: PaginationParams = fastapi.Depends(pagination),
|
|
) -> schemas.UsersResponse:
|
|
...
|
|
```
|
|
|
|
Example `api/rest/users/router.py`:
|
|
```python
|
|
import fastapi
|
|
|
|
from . import handlers
|
|
|
|
|
|
def get_router() -> fastapi.APIRouter:
|
|
router = fastapi.APIRouter()
|
|
router.add_api_route(path="/", methods=["GET"], endpoint=handlers.list_users)
|
|
router.add_api_route(path="/", methods=["POST"], endpoint=handlers.create_user)
|
|
router.add_api_route(path="/{id}", methods=["GET"], endpoint=handlers.get_user)
|
|
router.add_api_route(path="/{id}", methods=["PUT"], endpoint=handlers.update_user)
|
|
router.add_api_route(path="/{id}", methods=["DELETE"], endpoint=handlers.delete_user)
|
|
return router
|
|
```
|
|
|
|
Example `api/rest/users/__init__.py`:
|
|
```python
|
|
from .router import get_router
|
|
|
|
__all__ = (
|
|
# router
|
|
"get_router",
|
|
)
|
|
```
|
|
|
|
- Do not construct `fastapi.APIRouter` instances at import time outside `get_router()`. The router is created only when `get_router()` is called during `_setup_app()`.
|
|
```
|