Writing APIs

Even if its not its primary goal, fastlife includes helpers to expose a rest API using the configurator and using a class based view.

Setup api information

The configurator expose a Configurator.set_api_documentation_info method that serve to create the OpenAPI informations and optionally expose the documentation url using Swagger UI or Redoc.

from fastlife import Configurator, configure

@configure
def includeme(config: Configurator):
    config.set_api_documentation_info(
        "Dummy API",
        "4.2",
        "Description of the API that support **markdown**.",
        summary="API for dummies",
        swagger_ui_url="/api/doc",
        redoc_url="/api/redoc",
    )

Writing a single route

We can use the configurator method

Configurator.add_api_route to add routes.

Tip

This method is compatible with the FastAPI router, there is only a permission parameter added in order to be used with a SecurityPolicy.

It differs from the Configurator.add_route, the method that register classical views with templates.

Here is a simple example

from pydantic import BaseModel

from fastlife import Configurator, configure


class Info(BaseModel):
    build: str


def info() -> Info:
    return Info(build="00d94f2")


@configure
def includeme(config: Configurator):
    config.add_api_route(
        "home",
        "/api",
        info,
        methods=["GET"],
        summary="Retrieve Build Information",
        description="Return application build information",
        response_description="Build Info",
        tags=["monitoring"],
    )

Writing a resource

Fastlife expose a decorator to groups a set of method under a tag directly, this is an opinionated way to write APIs in a rest resource style.

from typing import Annotated

from fastapi import Body, Path, Response
from pydantic import BaseModel

from fastlife import resource, resource_view
from fastlife.config.openapiextra import ExternalDocs


class Ok(BaseModel):
    message: str = "Ok"


class Foo(BaseModel):
    name: str


@resource(
    "foos",
    collection_path="/foos",
    path="/foos/{name}",
    description="Manage foos, not bars.",
    external_docs=ExternalDocs(
        description="Discover what foos are at http://example.net/",
        url="http://example.net/",
    ),
)
class Foos:
    @resource_view(
        permission="foos:read",
        summary="API For foos",
        description="Fetch a list of foos.",
        response_description="foo collection",
    )
    async def collection_get(self, response: Response) -> list[Foo]:
        ...

    @resource_view(
        permission="foos:write",
        summary="Register a foo.",
        description="The more the merrier.",
        response_description="ok",
    )
    async def collection_post(self, foo: Annotated[Foo, Body(...)]) -> Ok:
        ...

    @resource_view(
        permission="foos:read",
        summary="Get one foo by its name",
    )
    async def get(self, name: Annotated[str, Path(...)]) -> Foo:
        ...

    @resource_view(
        summary="CORS preflight request",
        include_in_schema=False,
    )
    async def options(self, name: Annotated[str, Path(...)]) -> Ok:
        ...

The @resource <fastlife.config.resources.resource> decorator is used to decorate a class that have HTTP methods.

There is no base class here. To add a ‘POST’ method a post method has to be added to the class. it is a class based view.

The path used is set on the @resource <fastlife.config.resources.resource> decorator.

A resource also have a collection_path and have appropriate collection_* methods too.

All the http verb are supported so get, post, put, patch, delete, head and options, and has to be prefixed by collection for the collection_path set on the @resource <fastlife.config.resources.resource> decorator ( collection_get, collection_post, collection_put, collection_patch, collection_delete, collection_head and collection_options).

Usually, the basic crud operation in a json/rest style api ares:

  • a collection_post is implemented to add an object to the collection (store a new resource).

  • a collection_get is implemented to retrieve a list partial objects, and have fields.

  • a get is implemented to retrieve a single and full object.

  • a patch or put is implemented to update a resource.

  • a delete is implemented to remove a resource from the collection.

Other methods handle personal needs.

A basic crud example, included from the test suite is in the fastlife APIs documentation as a reference: fastlife.config.resources.

Security Policy

Usually, an API is private, it requires a secret to be consumed, it can be an api key or an OAuth2. token, it usually requires an Authorization header.

To handle security, in fastlife, you have to register a Security Policy. The security policy is per “route_prefix”.

It means that the app can have many polices depending on tht included path. It can have public pages without any policy, an admin part with a specific policy and an api with another policy.

While including the pages with route_prefix

from fastlife import Configurator, configure

@configure
def includeme(config: Configurator):
    config.include('.public_pages')
    config.include('.admin', route_prefix='/admin')
    config.include('.api', route_prefix='/api')

Caution

Event if the /api route exists, and the swagger_ui_url point to “/api/doc”, it will always be in include withoute the route prefix.

Now imagine that the api module contains a security policy, it will applied only to the api routes.

For example:

from pydantic import BaseModel

from fastapi import Depends
from fastapi.security.oauth2 import OAuth2PasswordBearer

from fastlife import Configurator, configure, DefaultRegistry, Request
from fastlife.security.policy import AbstractSecurityPolicy, Allowed, Unauthorized


class AuthenticatedUser(BaseModel):
    user_id: str


# the the auto_error to False to avoid the Depends raise a error it self,
# it is controlled by the `has_permission` alled made while checking the permission
oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="http://token", scopes={"foo": "Foos resources"}, auto_error=False
)


class MySecurityPolicy(AbstractSecurityPolicy[AuthenticatedUser, DefaultRegistry]):

    def __init__(
        self,
        request: Request,
        token: Annotated[str | None, Depends(oauth2_scheme)]
    ):
        super().__init__(request)
        self.token = token

    async def identity(self) -> AuthenticatedUser | None:
        if token is None:
            return None
        return AuthenticatedUser(user_id=token)  # Don't do that in the real world

    @abc.abstractmethod
    async def authenticated_userid(self) -> str | None:
        return self.token  # Don't do that in the real world

    async def has_permission(
        self, permission: str
    ) -> type[HasPermission]:
        return Allowed if self.token else Unauthorized

    async def remember(self, user: TUser) -> None:
        return None

    async def forget(self) -> None:
        return None


@configure
def includeme(config: Configurator):
    config.set_security_policy(MySecurityPolicy)

The security policy constructor accept any FastAPI dependency and is made to receive FastAPI security dependency to properly build the doc. But for fine grained check, it is better to always set the auto_error to False to give the maximum control of the security policy.

Hint

In FastAPI, many things must be done in the proper order to work.

In fastlife it just doesn’t matter, because fastlife will build the FastAPI app in the proper order.