Skip to content

FastAPI Integration

1. Standalone application

from jcpy import create_app

app = create_app("config.json")

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

app.include_router(create_router("config.json"), prefix="/api/files")
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.