FastAPI Integration¶
1. Standalone application¶
Routes: GET /ping, and GET/POST/OPTIONS on / (action in the action parameter) and /{action}.
2. Inside an existing application¶
from fastapi import FastAPI
from jcpy import create_router
app = FastAPI()
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "healthy"}
# Your routes first: the connector answers every other /{name}.
app.include_router(create_router("config.json"))
Now GET /health is yours, GET /?action=files and GET /files are the connector's.
Route order
The connector's /{action} route matches any single path segment. Register your own routes before including the router, or mount the connector under a prefix.
FastAPI's own /docs and /openapi.json also collide with /{action}; create_app() disables them. In your application either keep them and mount the connector under a prefix, or disable them: FastAPI(docs_url=None, redoc_url=None, openapi_url=None). The connector's documented API is the OpenAPI document generated from its schemas.
3. Under a path prefix¶
GET /api/files/ping
GET /api/files/?action=files or GET /api/files/files
POST /api/files/?action=fileUpload
4. Middleware and sessions¶
Middleware of your application runs for the connector too, so the authentication callback can use what it prepared: a session, a user loaded by an auth middleware, request state. With Starlette sessions:
def role_from_session(request: Request) -> str:
"""Read the role stored in the session.
Args:
request: Incoming request.
Returns:
Stored role, ``guest`` before login.
"""
role = request.session.get("userRole")
return role if isinstance(role, str) else "guest"
def build_app() -> FastAPI:
"""Build the application.
Returns:
Application with session routes and the connector at ``/``.
"""
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
app.add_middleware(
SessionMiddleware, secret_key=SECRET, max_age=SESSION_MAX_AGE
)
@app.get("/login/{role}")
def login(
request: Request, role: Literal["guest", "editor", "admin"]
) -> dict[str, str]:
request.session["userRole"] = role
return {"role": role}
@app.get("/logout")
def logout(request: Request) -> dict[str, str]:
request.session.clear()
return {"role": "guest"}
@app.get("/whoami")
def whoami(request: Request) -> dict[str, str]:
return {"role": role_from_session(request)}
# Own routes first: the connector serves every other /{action}.
app.include_router(
create_router(CONFIG, check_authentication=role_from_session)
)
return app
The whole program is examples/session_auth.py.
5. Several instances¶
Each create_router() call is an isolated instance with its own configuration, authentication and caches:
def public_role(_request: Request) -> str:
"""Treat every visitor of the public connector as a guest.
Returns:
``guest``.
"""
return "guest"
def admin_role(request: Request) -> str:
"""Grant ``admin`` to requests with the right token.
Args:
request: Incoming request.
Returns:
``admin`` or ``anonymous``.
"""
token = request.headers.get("x-admin-token", "")
if secrets.compare_digest(token.encode(), ADMIN_TOKEN.encode()):
return "admin"
return "anonymous"
def build_app() -> FastAPI:
"""Build the application.
Returns:
Application serving both connectors.
"""
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
app.include_router(
create_router(
CONFIG_DIR / "public.json", check_authentication=public_role
),
prefix="/public",
)
app.include_router(
create_router(
CONFIG_DIR / "admin.json", check_authentication=admin_role
),
prefix="/admin",
)
return app
public.json denies every modifying action; admin.json gives nothing to the anonymous default role and everything to admin:
{
"title": "Public files",
"accessControl": [
{"FILE_UPLOAD": false, "FILE_UPLOAD_REMOTE": false, "FILE_REMOVE": false,
"FILE_MOVE": false, "FILE_RENAME": false, "FOLDER_CREATE": false,
"FOLDER_REMOVE": false, "FOLDER_MOVE": false, "FOLDER_RENAME": false,
"IMAGE_RESIZE": false, "IMAGE_CROP": false, "IMAGE_SAVE": false}
],
"sources": {
"public": {
"title": "Public",
"root": "./files/public",
"baseurl": "http://localhost:8080/files/public/"
}
}
}
{
"title": "Admin files",
"defaultRole": "anonymous",
"accessControl": [
{"role": "anonymous", "FILES": false, "FOLDERS": false, "PERMISSIONS": false},
{"role": "admin", "FILES": true, "FOLDERS": true, "PERMISSIONS": true}
],
"sources": {
"private": {
"title": "Private",
"root": "./files/private",
"baseurl": "http://localhost:8080/files/private/"
}
}
}
Instances never share state, which a test checks under concurrent requests to both.
Key points¶
- Every instance has its own configuration, sources, access rules and callbacks.
- Middleware of the host application applies to all instances.
- Callbacks are arguments of
create_router(), so instances in one application can authenticate differently.