Usage¶
CLI¶
This package exposes simple CLI for easier interaction:
$ example --help
Usage: example [OPTIONS] COMMAND [ARGS]...
Example CLI root.
Options:
-v, --verbose Enable verbose logging.
--help Show this message and exit.
Commands:
serve example CLI serve command.
$ example serve --help
Usage: example serve [OPTIONS]
Run production gunicorn (WSGI) server with uvicorn (ASGI) workers.
Options:
--bind TEXT Host to bind.
-w, --workers INTEGER RANGE The number of worker processes for handling
requests.
-D, --daemon Daemonize the Gunicorn process.
-e, --env TEXT Set environment variables in the execution
environment.
--pid PATH Specifies the PID file.
--help Show this message and exit.
Note
Maximum number of workers may be different in your case, it’s limited to multiprocessing.cpu_count()
WSGI + ASGI production server¶
To run production unicorn + uvicorn (WSGI + ASGI) server you can use project CLI serve command:
example serve
[2022-04-23 20:21:49 +0000] [4769] [INFO] Start gunicorn WSGI with ASGI workers.
[2022-04-23 20:21:49 +0000] [4769] [INFO] Starting gunicorn 20.1.0
[2022-04-23 20:21:49 +0000] [4769] [INFO] Listening at: http://127.0.0.1:8000 (4769)
[2022-04-23 20:21:49 +0000] [4769] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2022-04-23 20:21:49 +0000] [4769] [INFO] Server is ready. Spawning workers
[2022-04-23 20:21:49 +0000] [4771] [INFO] Booting worker with pid: 4771
[2022-04-23 20:21:49 +0000] [4771] [INFO] Worker spawned (pid: 4771)
[2022-04-23 20:21:49 +0000] [4771] [INFO] Started server process [4771]
[2022-04-23 20:21:49 +0000] [4771] [INFO] Waiting for application startup.
[2022-04-23 20:21:49 +0000] [4771] [INFO] Application startup complete.
[2022-04-23 20:21:49 +0000] [4772] [INFO] Booting worker with pid: 4772
[2022-04-23 20:21:49 +0000] [4772] [INFO] Worker spawned (pid: 4772)
[2022-04-23 20:21:49 +0000] [4772] [INFO] Started server process [4772]
[2022-04-23 20:21:49 +0000] [4772] [INFO] Waiting for application startup.
[2022-04-23 20:21:49 +0000] [4772] [INFO] Application startup complete.
To confirm it’s working:
$ curl localhost:8000/api/ready
{"status":"ok"}
Dockerfile¶
This project provides Dockerfile for containerized environment.
$ make image
$ podman run -dit --name example -p 8000:8000 example:$(cat TAG)
f41e5fa7ffd512aea8f1aad1c12157bf1e66f961aeb707f51993e9ac343f7a4b
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f41e5fa7ffd5 localhost/example:0.1.0 /usr/bin/fastapi ... 2 seconds ago Up 3 seconds ago 0.0.0.0:8000->8000/tcp example
$ curl localhost:8000/api/ready
{"status":"ok"}
Note
Replace podman with docker if it’s yours containerization engine.
Development¶
You can implement your own web routes logic straight away in example.controllers
submodule. For more information please see FastAPI documentation.
Makefile¶
Provided Makefile is a starting point for application and infrastructure development:
Usage:
make <target>
help Display this help
image Build example image
clean-image Clean example image
install Install example with poetry
metrics Run example metrics checks
unit-test Run example unit tests
integration-test Run example integration tests
docs Build example documentation
dev-env Start a local Kubernetes cluster using minikube and deploy application
clean Remove .cache directory and cached minikube
Utilities¶
Available utilities:
RedisClient
example.app.utils.redis
AiohttpClient
example.app.utils.aiohttp_client
They’re initialized in asgi.py
on FastAPI startup event handler:
async def on_startup():
"""Fastapi startup event handler.
Creates RedisClient and AiohttpClient session.
"""
log.debug("Execute FastAPI startup event handler.")
# Initialize utilities for whole FastAPI application without passing object
# instances within the logic. Feel free to disable it if you don't need it.
if settings.USE_REDIS:
await RedisClient.open_redis_client()
AiohttpClient.get_aiohttp_client()
async def on_shutdown():
"""Fastapi shutdown event handler.
Destroys RedisClient and AiohttpClient session.
"""
log.debug("Execute FastAPI shutdown event handler.")
# Gracefully close utilities.
if settings.USE_REDIS:
await RedisClient.close_redis_client()
await AiohttpClient.close_aiohttp_client()
and are available for whole application scope without passing object instances. In order to utilize it just execute classmethods directly.
Example:
from example.app.utils import RedisClient
response = RedisClient.get("Key")
Exceptions¶
HTTPException and handler
"""Application implementation - custom FastAPI HTTP exception with handler."""
from typing import Any, Optional, Dict
from fastapi import Request
from fastapi.responses import JSONResponse
class HTTPException(Exception):
"""Define custom HTTPException class definition.
This exception combined with exception_handler method allows you to use it
the same manner as you'd use FastAPI.HTTPException with one difference. You
have freedom to define returned response body, whereas in
FastAPI.HTTPException content is returned under "detail" JSON key.
FastAPI.HTTPException source:
https://github.com/tiangolo/fastapi/blob/master/fastapi/exceptions.py
"""
def __init__(
self,
status_code: int,
content: Any = None,
headers: Optional[Dict[str, Any]] = None,
) -> None:
"""Initialize HTTPException class object instance.
Args:
status_code (int): HTTP error status code.
content (Any): Response body.
headers (Optional[Dict[str, Any]]): Additional response headers.
"""
self.status_code = status_code
self.content = content
self.headers = headers
def __repr__(self) -> str:
"""Class custom __repr__ method implementation.
Returns:
str: HTTPException string object.
"""
kwargs = []
for key, value in self.__dict__.items():
if not key.startswith("_"):
kwargs.append(f"{key}={value!r}")
return f"{self.__class__.__name__}({', '.join(kwargs)})"
async def http_exception_handler(
request: Request, exception: HTTPException
) -> JSONResponse:
"""Define custom HTTPException handler.
In this application custom handler is added in asgi.py while initializing
FastAPI application. This is needed in order to handle custom HTTException
globally.
More details:
https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers
Args:
request (starlette.requests.Request): Request class object instance.
More details: https://www.starlette.io/requests/
exception (HTTPException): Custom HTTPException class object instance.
Returns:
FastAPI.response.JSONResponse class object instance initialized with
kwargs from custom HTTPException.
"""
return JSONResponse(
status_code=exception.status_code,
content=exception.content,
headers=exception.headers,
)
This exception combined with http_exception_handler
method allows you to use it the same manner as you’d use FastAPI.HTTPException
with one difference.
You have freedom to define returned response body, whereas in FastAPI.HTTPException
content is returned under “detail” JSON key.
In this application custom handler is added in asgi.py
while initializing FastAPI application. This is needed in order to handle it globally.
Web Routes¶
All routes documentation is available on:
/
with Swagger/redoc
or ReDoc.
Configuration¶
This application provides flexibility of configuration. All significant settings are defined by the environment variables, each with the default value. Moreover, package CLI allows overriding core ones: host, port, workers. You can modify all other available configuration settings in the gunicorn.conf.py file.
Priority of overriding configuration:
cli
environment variables
gunicorn.py
All application configuration is available in example.config
submodule.
Environment variables¶
Application configuration
Key |
Default |
Description |
---|---|---|
FASTAPI_BIND |
|
The socket to bind. A string of the form: ‘HOST’, ‘HOST:PORT’, ‘unix:PATH’. An IP is a valid HOST. |
FASTAPI_WORKERS |
|
Number of gunicorn workers (uvicorn.workers.UvicornWorker). |
FASTAPI_DEBUG |
|
FastAPI logging level. You should disable this for production. |
FASTAPI_PROJECT_NAME |
|
FastAPI project name. |
FASTAPI_VERSION |
|
Application version. |
FASTAPI_DOCS_URL |
|
Path where swagger ui will be served at. |
FASTAPI_USE_REDIS |
|
Whether or not to use Redis. |
FASTAPI_GUNICORN_LOG_LEVEL |
|
The granularity of gunicorn log output. |
FASTAPI_GUNICORN_LOG_FORMAT |
|
Gunicorn log format. |
Redis configuration
Key |
Default |
Description |
---|---|---|
FASTAPI_REDIS_HOTS |
|
Redis host. |
FASTAPI_REDIS_PORT |
|
Redis port. |
FASTAPI_REDIS_USERNAME |
|
Redis username. |
FASTAPI_REDIS_PASSWORD |
|
Redis password. |
FASTAPI_REDIS_USE_SENTINEL |
|
If provided Redis config is for Sentinel. |
Gunicorn¶
Gunicorn configuration file documentation
"""Gunicorn configuration file.
Resources:
1. https://docs.gunicorn.org/en/20.1.0/settings.html
"""
import os
# Server socket
#
# bind - The socket to bind.
#
# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.
# An IP is a valid HOST.
#
# backlog - The number of pending connections. This refers
# to the number of clients that can be waiting to be
# served. Exceeding this number results in the client
# getting an error when attempting to connect. It should
# only affect servers under significant load.
#
# Must be a positive integer. Generally set in the 64-2048
# range.
#
bind = os.getenv("FASTAPI_BIND", "127.0.0.1:8000")
backlog = 2048
#
# Worker processes
#
# workers - The number of worker processes that this server
# should keep alive for handling requests.
#
# A positive integer generally in the 2-4 x $(NUM_CORES)
# range. You'll want to vary this a bit to find the best
# for your particular application's work load.
#
# worker_class - The type of workers to use. The default
# sync class should handle most 'normal' types of work
# loads. You'll want to read
# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type
# for information on when you might want to choose one
# of the other worker classes.
#
# A string referring to a Python path to a subclass of
# gunicorn.workers.base.Worker. The default provided values
# can be seen at
# http://docs.gunicorn.org/en/latest/settings.html#worker-class
#
# worker_connections - For the eventlet and gevent worker classes
# this limits the maximum number of simultaneous clients that
# a single process can handle.
#
# A positive integer generally set to around 1000.
#
# timeout - If a worker does not notify the master process in this
# number of seconds it is killed and a new worker is spawned
# to replace it.
#
# Generally set to thirty seconds. Only set this noticeably
# higher if you're sure of the repercussions for sync workers.
# For the non sync workers it just means that the worker
# process is still communicating and is not tied to the length
# of time required to handle a single request.
#
# keepalive - The number of seconds to wait for the next request
# on a Keep-Alive HTTP connection.
#
# A positive integer. Generally set in the 1-5 seconds range.
#
# reload - Restart workers when code changes.
#
# True or False
workers = int(os.getenv("FASTAPI_WORKERS", 2))
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
timeout = 30
keepalive = 2
reload = False
#
# spew - Install a trace function that spews every line of Python
# that is executed when running the server. This is the
# nuclear option.
#
# True or False
#
spew = False
#
# Server mechanics
#
# daemon - Detach the main Gunicorn process from the controlling
# terminal with a standard fork/fork sequence.
#
# True or False
#
# raw_env - Pass environment variables to the execution environment.
#
# pidfile - The path to a pid file to write
#
# A path string or None to not write a pid file.
#
# user - Switch worker processes to run as this user.
#
# A valid user id (as an integer) or the name of a user that
# can be retrieved with a call to pwd.getpwnam(value) or None
# to not change the worker process user.
#
# group - Switch worker process to run as this group.
#
# A valid group id (as an integer) or the name of a user that
# can be retrieved with a call to pwd.getgrnam(value) or None
# to change the worker processes group.
#
# umask - A mask for file permissions written by Gunicorn. Note that
# this affects unix socket permissions.
#
# A valid value for the os.umask(mode) call or a string
# compatible with int(value, 0) (0 means Python guesses
# the base, so values like "0", "0xFF", "0022" are valid
# for decimal, hex, and octal representations)
#
# tmp_upload_dir - A directory to store temporary request data when
# requests are read. This will most likely be disappearing soon.
#
# A path to a directory where the process owner can write. Or
# None to signal that Python should choose one on its own.
#
daemon = False
# raw_env = [
# 'DJANGO_SECRET_KEY=something',
# 'SPAM=eggs',
# ]
pidfile = None
umask = 0
user = None
group = None
tmp_upload_dir = None
#
# Logging
#
# logfile - The path to a log file to write to.
#
# A path string. "-" means log to stdout.
#
# loglevel - The granularity of log output
#
# A string of "debug", "info", "warning", "error", "critical"
#
errorlog = "-"
loglevel = os.getenv("FASTAPI_GUNICORN_LOG_LEVEL", "info")
accesslog = "-"
access_log_format = os.getenv(
"FASTAPI_GUNICORN_LOG_FORMAT",
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"',
)
#
# Process naming
#
# proc_name - A base to use with setproctitle to change the way
# that Gunicorn processes are reported in the system process
# table. This affects things like 'ps' and 'top'. If you're
# going to be running more than one instance of Gunicorn you'll
# probably want to set a name to tell them apart. This requires
# that you install the setproctitle module.
#
# A string or None to choose a default of something like 'gunicorn'.
#
proc_name = None
#
# Server hooks
#
# post_fork - Called just after a worker has been forked.
#
# A callable that takes a server and worker instance
# as arguments.
#
# pre_fork - Called just prior to forking the worker subprocess.
#
# A callable that accepts the same arguments as after_fork
#
# pre_exec - Called just prior to forking off a secondary
# master process during things like config reloading.
#
# A callable that takes a server instance as the sole argument.
#
def post_fork(server, worker):
"""Execute after a worker is forked."""
server.log.info("Worker spawned (pid: %s)", worker.pid)
def pre_fork(server, worker):
"""Execute before a worker is forked."""
pass
def pre_exec(server):
"""Execute before a new master process is forked."""
server.log.info("Forked child, re-executing.")
def when_ready(server):
"""Execute just after the server is started."""
server.log.info("Server is ready. Spawning workers")
def worker_int(worker):
"""Execute just after a worker exited on SIGINT or SIGQUIT."""
worker.log.info("worker received INT or QUIT signal")
# get traceback info
import threading
import sys
import traceback
id2name = {th.ident: th.name for th in threading.enumerate()}
code = []
for threadId, stack in sys._current_frames().items():
code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId))
for filename, lineno, name, line in traceback.extract_stack(stack):
code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
if line:
code.append(" %s" % (line.strip()))
worker.log.debug("\n".join(code))
def worker_abort(worker):
"""Execute when worker received the SIGABRT signal."""
worker.log.info("worker received SIGABRT signal")
Routes¶
Endpoints are defined in example.app.router
submodule. Just simply import your controller and include it to FastAPI router:
"""Application configuration - root APIRouter.
Defines all FastAPI application endpoints.
Resources:
1. https://fastapi.tiangolo.com/tutorial/bigger-applications
"""
from fastapi import APIRouter
from example.app.controllers import ready
root_api_router = APIRouter(prefix="/api")
root_api_router.include_router(ready.router, tags=["ready"])