- 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>
7.3 KiB
7.3 KiB
name, description
| name | description |
|---|---|
| python-development-fastapi | 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 hasservice.py,settings.py, and__init__.py. - Provide a custom
UvicornServerthat disables signal handlers so the parent facet orchestrator controls startup and shutdown. Example: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'ssettings.py. Keep the constructor minimal; heavy initialization happens instart(). - The
start()method builds the FastAPI application asynchronously, configures uvicorn, and adds the server coroutine to the facet task list withself.add_task(server.serve()). - Extract app construction into an
async_build_app()method so router registration and dependency wiring can useawait. - Register routers in
_setup_app()or_build_app()usingapp.include_router(...). Group related endpoints under routers with aprefix. - The settings class inherits from
pydantic.BaseModel(notBaseSettings). 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:
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:
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 inapi/rest/users/. - Every handler sub-package contains at minimum:
router.py— definesget_router() -> fastapi.APIRouterand 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 onlyget_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.pyinside 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.Dependsinrouter.pyto 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:
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:
from pydantic import BaseModel
class UserResponse(BaseModel):
user_id: int
requested_by: str
Example api/rest/users/handlers.py consuming a dependency:
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
ListResponseschema for endpoints that return collections. It is defined once inapi/rest/schemas.pyand reused across packages. - Extract pagination and search via a reusable
paginationdependency declared inapi/rest/dependencies.py.
Example api/rest/schemas.py:
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:
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:
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:
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:
from .router import get_router
__all__ = (
# router
"get_router",
)
- Do not construct
fastapi.APIRouterinstances at import time outsideget_router(). The router is created only whenget_router()is called during_setup_app().