Skip to main content

Build FastAPI applications easily

Project description

Install from pypi

A pre-release has been published on pypi with the name fastapi-wire. To install it simply run:

python -m pip install fastapi-wire

Install from source

First clone the repository from github then install using one of the method below.

Install using poetry

poetry install

Install using script

python ./install.py

Install manually

  • Create a virtual environment:
python -m venv .venv
  • Update python package toolkit (within the virtual environment):
python -m pip install -U pip setuptools wheel build
  • Install the project in editable mode (within the virtual environment) with all extras:
python -m pip install -e .[dev,oidc,telemetry]

Run the app

  • Either use the wire module:
python -m wire examples/app.yaml -c examples/config.json
  • The command line interface:
wire --help
  • Or use uvicorn to start the application:
CONFIG_FILEPATH=examples/config.json uvicorn --factory demo_app.spec:spec.create_app

Note that server config won't be applied since uvicorn is started from command line and not within Python process in this case.

  • It's also possible to start the application with hot-reloading:
CONFIG_FILEPATH=examples/config.json uvicorn --factory demo_app.spec:spec.create_app --reload

Configure the app

Application can be configured using environment variables or file, or options when using the CLI:

App Container

Note: Environment variables take precedence over variables declared in file. For example, assuming the above configuration is declared in a file named config.json, when running: PORT=8000 CONFIG_FILE=./demo/config.json python -m demo_app, application will listen on port 8000 and not 7777.

Note: When using uvicorn, HOST and PORT are ignored and must be specified as command line arguments if required.

Design choices

Application is wrapped within a Container:

An Container is created from:

  • Some settings: settings are defined as pydantic models. When they are not provided directly, values are parsed from environment or file.

  • Some hooks: hooks are async context managers which can inject arbitrary resources into application state. In this application, a hook is used to add an Issuer instance to the application state. See documentation on Asynchronous Context Managers and @contextlib.asynccontextmanager to easily create context managers from coroutine functions. You can see how it's used in the hook used by the example application.

  • Some providers: providers are functions which must accept a single argument, an application container, and can add additional features to the application. They are executed before the FastAPI application is initialized, unlike hooks, which are started after application is initiliazed, but before it is started. In the repo example, providers are used for example to optionally enable prometheus metrics and opentelemetry traces. The CORS Provider is surely the most simple provider.

  • Some routers: routers are objects holding a bunch of API endpoints together. Those endpoints can share a prefix and some OpenAPI metadata.

In order to faciliate creation and declaration of application containers, the AppSpec class can be used as a container factory.

See usage in src/demo-app/demo_app/spec.py

Objectives

  • Distributable: Application can be distributed as a python package.

  • Configurable: The database used in the application must be configurable using either a file or an environment variable.

  • Configurable: The server configuration (host, port, debug mode) must be configurable using either a file or an environment variable.

  • User friendly: A command line script should provide a quick and simply way to configure and start the application.

  • Debug friendly: Application settings should be exposed on a /debug/settings endpoint when application is started on debug mode.

  • Observable: Adding metrics or tracing capabilities to the application should be straighforward and transparent.

  • Explicit: Router endpoints must not use global variables but instead explicitely declare dependencies to be injected (such as database client or settings). This enables efficient sharing of resources and facilitate eventual refactoring in the future.

  • Conposable: Including additional routers or features in the future should require minimal work.

    • Arbitrary hooks with access to application container within their scope can be registered. Those hooks are guaranteed to be started and stopped in order, and even if an exception is encountered during a hook exit, all remaining hooks will be closed before an exception is raised. It minimize risk of resource leak within the application. Hooks can be seen as contexts just like in the illustration below:

    • Arbitrary tasks can be started along the application. Tasks are similar to hooks, and are defined using a coroutine function which takes the application container as argument and can stay alive as long as application is alive. Unlike hooks, tasks have a status and can be:

      • stopped
      • started
      • restarted It's also possible to fetch the task status to create healthcheck handlers for example.
    • Arbitrary providers with access to application container within their scope can be registered. Those providers are executed once, before the application is created. They can be used to add optional features such as tracing or metrics.

    • Objects provided by hooks or providers can be accessed through dependency injection in the API endpoints. Check this example to see dependency injection in practice.

Below is an illustration of an hypothetic application lifecycle:

App Lifecycle

Declarative application

It's possible to declare application using YAML, JSON or INI files. An example application could be:

---
# Application metadata
meta:
  name: demo_app
  title: Demo App
  description: A declarative FastAPI application 🎉
  package: wire

# Custom settings model
settings: demo_app.settings.AppSettings

# Declare providers
# A few providers are available to use directly
# It's quite easy to add new providers
providers:
  - wire.providers.structured_logging_provider
  - wire.providers.prometheus_metrics_provider
  - wire.providers.openid_connect_provider
  - wire.providers.openelemetry_traces_provider
  - wire.providers.cors_provider
  - wire.providers.debug_provider
# It's possible to add routers
routers:
  - demo_app.routes.issuer_router
  - demo_app.routes.nats_router
  - demo_app.routes.demo_router
# Or hooks
hooks:
  - demo_app.hooks.issuer_hook
# Or tasks (not used in this example)
tasks: []
# # It's also possible to declare default config file
# config_file: ~/.quara.config.json

AppSpec container factory

It's also possible to define applications using a python object instead of a text file.

The AppSpec class can be used to create application containers according to an application specification.

Adding new hooks

Update the file demo_app/spec.py to add a new hook to your application.

The hooks argument of the AppSpec constructor can be used to specify a list of hooks used by the application.

Each hook must implement the AsyncContextManager protocol or be functions which might return None or an AsyncContextManager instance.

Object yielded by the hook is available in API endpoints using dependency injection.

Note: It's possible to access any container attribute within hooks.

Adding new routers

Update the file demo_app/spec.py to register a new router within your application.

The routers argument of the AppSpec constructor can be used to specify a list of routers used by the application.

Both fastapi.APIRouter and functions which might return None or an fastapi.APIRouter instance are accepted as list items.

Adding providers to the application

Providers are functions which can modify the FastAPI application before it is started.

They must accept an application container instance as unique argument, and can return a list of objects or None. When None is returned, it is assumed that provider is disabled. When a list is returned, each object present in the list will be available in API endpoints using dependency injection.

Example providers are located in src/quara-wiring/quara/wiring/providers/ directory and are registered in demo_app/spec.py.

Building the application

Run the following command to build the application:

python -m build .

Advantages of the src/ layout

This project uses a src/ layout. It means that all source code can be found under src/ directory. It might appear overkill at first, but it brings several benefits:

  • Without src you get messy editable installs ("pip install -e"). Having no separation (no src dir) will force setuptools to put your project's root on sys.path - with all the junk in it (e.g.: setup.py and other test or configuration scripts will unwittingly become importable).

  • You get import parity. The current directory is implicitly included in sys.path; but not so when installing & importing from site-packages.

  • You will be forced to test the installed code (e.g.: by installing in a virtualenv and performing an editable install). This will ensure that the deployed code works (it's packaged correctly) - otherwise your tests will fail.

  • Simpler packaging code and manifest. It makes manifests very simple to write (e.g.: root directory of project is never considered by setuptools or other packaging toolswhen bundling files into package). Also, zero fuss for large libraries that have multiple packages. Clear separation of code being packaged and code doing the packaging.

Telemetry

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi-wire-0.0.1a4.tar.gz (38.0 kB view hashes)

Uploaded Source

Built Distribution

fastapi_wire-0.0.1a4-py3-none-any.whl (43.5 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page