Fastapi
Fastapi
FastAPI (https://fastapi.tiangolo.com/) is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
Fast: Very high performance, on par with NodeJS and Go (thanks to Starlette and Pydantic). One of the fastest Python frameworks available.
Fast to code: Increase the speed to develop features by about 200% to 300%.
Fewer bugs: Reduce about 40% of human (developer) induced errors.
Intuitive: Great editor support. Completion everywhere. Less time debugging.
Easy: Designed to be easy to use and learn. Less time reading docs.
Short: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
Robust: Get production-ready code. With automatic interactive documentation.
Standards-based: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema.
Authentication with JWT (https://fastapi.tiangolo.com/tutorial/security/first-steps/): with a super nice tutorial on how to set it up.
Installation
(https://fastapi.tiangolo.com/#installation)
pip install fastapi
You will also need an ASGI server, for production such as Uvicorn or Hypercorn.
Simple example
(https://fastapi.tiangolo.com/#installation)
Create a file main.py with:
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
return {"item_id": item_id, "q": q}
Open your browser at http://127.0.0.1:8000/items/5?q=somequery (http://127.0.0.1:8000/items/5?q=somequery). You will see the JSON response as:
{
"item_id": 5,
"q": "somequery"
}
You will see the automatic interactive API documentation (provided by Swagger UI):
To send simple data use the first two, to send complex or sensitive data, use the last.
It also supports sending data through cookies (https://fastapi.tiangolo.com/tutorial/cookie-params/) and headers (https://fastapi.tiangolo.com/tutorial/header-params/).
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
If you define the type hints of the function arguments, FastAPI will use pydantic (pydantic.md) data validation.
If you need to use a Linux path as an argument, check this workaround (https://fastapi.tiangolo.com/tutorial/path-params/#path-parameters-containing-paths), but be aware that it's
not supported by OpenAPI.
@app.get("/users/me")
async def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
return {"user_id": user_id}
Otherwise, the path for /users/{user_id} would match also for /users/me, "thinking" that it's receiving a parameter user_id with a value of "me".
@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
if model_name == ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}
These are the basics, FastAPI supports more complex path parameters and string validations (https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/).
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]
The query is the set of key-value pairs that go after the ? in a URL, separated by & characters.
These are the basics, FastAPI supports more complex query parameters and string validations (https://fastapi.tiangolo.com/tutorial/query-params-str-validations/).
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
@app.post("/items/")
async def create_item(item: Item):
return item
These are the basics, FastAPI supports more complex patterns such as:
By default, FastAPI would automatically convert that return value to JSON using the jsonable_encoder.
To return custom responses such as a direct string, xml or html use Response (https://fastapi.tiangolo.com/advanced/response-directly/#returning-a-custom-response):
app = FastAPI()
@app.get("/legacy/")
def get_legacy_data():
data = """<?xml version="1.0"?>
<shampoo>
<Header>
Apply shampoo here.
</Header>
<Body>
You'll have to use soap here.
</Body>
</shampoo>
"""
return Response(content=data, media_type="application/xml")
Handling errors
(https://fastapi.tiangolo.com/tutorial/handling-
errors/)
There are many situations in where you need to notify an error to a client that is using your API.
In these cases, you would normally return an HTTP status code in the range of 400 (from 400 to 499).
This is similar to the 200 HTTP status codes (from 200 to 299). Those "200" status codes mean that somehow there was a "success" in the request.
To return HTTP responses with errors to the client you use HTTPException.
from fastapi import HTTPException
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
Updating data
(https://fastapi.tiangolo.com/tutorial/body-
updates/)
Update replacing with PUT
(https://fastapi.tiangolo.com/tutorial/body-updates/#update-
replacing-with-put)
To update an item you can use the HTTP PUT operation.
You can use the jsonable_encoder (https://fastapi.tiangolo.com/tutorial/encoder/) to convert the input data to data that can be stored as JSON (e.g. with a NoSQL database). For
example, converting datetime to str.
class Item(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
update_item_encoded = jsonable_encoder(item)
items[item_id] = update_item_encoded
return update_item_encoded
Partial updates with PATCH
(https://fastapi.tiangolo.com/tutorial/body-updates/#partial-
updates-with-patch)
You can also use the HTTP PATCH operation to partially update data.
This means that you can send only the data that you want to update, leaving the rest intact.
Configuration
Application configuration
(https://fastapi.tiangolo.com/advanced/settings/)
In many cases your application could need some external settings or configurations, for example secret keys, database credentials, credentials for email services, etc.
You can load these configurations through environmental variables (https://fastapi.tiangolo.com/advanced/settings/#environment-variables), or you can use the awesome Pydantic
settings management (https://pydantic-docs.helpmanual.io/usage/settings/), whose advantages are:
First you define the Settings class with all the fields:
File: config.py:
class Settings(BaseSettings):
verbose: bool = True
database_url: str = "tinydb://~/.local/share/pyscrobbler/database.tinydb"
File: api.py:
app = FastAPI()
@lru_cache()
def get_settings() -> Settings:
"""Configure the program settings."""
return Settings()
@app.get("/verbose")
def verbose(settings: Settings = Depends(get_settings)) -> bool:
return settings.verbose
Where:
get_settings is the dependency function that configures the Settings object. The endpoint verbose is dependant of get_settings
(https://fastapi.tiangolo.com/tutorial/dependencies/).
The @lru_cache decorator (https://fastapi.tiangolo.com/advanced/settings/#creating-the-settings-only-once-with-lru_cache) changes the function it decorates to return the
same value that was returned the first time, instead of computing it again, executing the code of the function every time.
So, the function will be executed once for each combination of arguments. And then the values returned by each of those combinations of arguments will be used again and
again whenever the function is called with exactly the same combination of arguments.
Creating the Settings object is a costly operation as it needs to check the environment variables or read a file, so we want to do it just once, not on each request.
This setup makes it easy to inject testing configuration so as not to break production code.
OpenAPI configuration
Define title, description and version (https://fastapi.tiangolo.com/tutorial/metadata/#title-
description-and-version)
app = FastAPI(
title="My Super Project",
description="This is a very fancy project, with auto docs for the API and everything",
version="2.5.0",
)
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = []
@app.get("/items/", tags=["items"])
async def read_items():
return [{"name": "Foo", "price": 42}]
@app.get("/users/", tags=["users"])
async def read_users():
return [{"username": "johndoe"}]
They will be added to the OpenAPI schema and used by the automatic documentation interfaces.
app = FastAPI(openapi_tags=tags_metadata)
@app.post(
"/items/",
response_description="The created item",
)
async def create_item(item: Item):
return item
Go to the project directory (in where your Dockerfile is, containing your app directory).
Now you have an optimized FastAPI server in a Docker container. Auto-tuned for your current server (and number of CPU cores).
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
To make things simpler make the app variable available on the root of your package, so you can do from program_name import app instead of from
program_name.entrypoints.api import app. To do that we need to add app to the __all__ internal python variable of the __init__.py file of our package.
File: src/program_name/__init__.py:
from .entrypoints.ap
import app
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
ENV MODULE_NAME="program_name"
Testing
(https://fastapi.tiangolo.com/tutorial/testing/)
FastAPI gives a TestClient object (https://www.starlette.io/testclient/) borrowed from Starlette (https://www.starlette.io) to do the integration tests on your application.
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
@pytest.fixture(name="client")
def client_() -> TestClient:
"""Configure FastAPI TestClient."""
return TestClient(app)
Imagine you have a db_tinydb fixture (pytest.md#fixtures) that sets up the testing database:
@pytest.fixture(name="db_tinydb")
def db_tinydb_(tmp_path: Path) -> str:
"""Create an TinyDB database engine.
Returns:
database_url: Url used to connect to the database.
"""
tinydb_file_path = str(tmp_path / "tinydb.db")
return f"tinydb:///{tinydb_file_path}"
@pytest.fixture(name="client")
def client_(db_tinydb: str) -> TestClient:
"""Configure FastAPI TestClient."""
app.dependency_overrides[get_settings] = override_settings
return TestClient(app)
Add endpoints only on testing environment
(https://github.com/tiangolo/fastapi/issues/552)
Sometimes you want to have some API endpoints to populate the database for end to end testing the frontend. If your app config has the environment attribute, you could try to do:
app = FastAPI()
@lru_cache()
def get_config() -> Config:
"""Configure the program settings."""
# no cover: the dependency are injected in the tests
log.info("Loading the config")
return Config() # pragma: no cover
if get_config().environment == "testing":
@app.get("/seed", status_code=201)
def seed_data(
repo: Repository = Depends(get_repo),
empty: bool = True,
num_articles: int = 3,
num_sources: int = 2,
) -> None:
"""Add seed data for the end to end tests.
Args:
repo: Repository to store the data.
"""
services.seed(
repo=repo, empty=empty, num_articles=num_articles, num_sources=num_sources
)
repo.close()
But the injection of the dependencies is only done inside the functions, so get_config().environment will always be the default value. I ended up doing that check inside the
endpoint, which is not ideal.
@app.get("/seed", status_code=201)
def seed_data(
config: Config = Depends(get_config),
repo: Repository = Depends(get_repo),
empty: bool = True,
num_articles: int = 3,
num_sources: int = 2,
) -> None:
"""Add seed data for the end to end tests.
Args:
repo: Repository to store the data.
"""
if config.environment != "testing":
repo.close()
raise HTTPException(status_code=404)
...
app = FastAPI()
@app.get("/typer")
async def read_typer():
return RedirectResponse("https://typer.tiangolo.com")
Note: The command is assuming that your app is available at the root of your package, look at the deploy section if you feel lost.
# client: TestClient
result = client.post(
"/source/add",
json={"body": body},
)
result.text
# '{"detail":[{"loc":["query","url"],"msg":"field required","type":"value_error.missing"}]}'
Logging
By default the application log messages are not shown in the uvicorn log (https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker/issues/19), you need to add the next lines to the
file where your app is defined:
File: src/program_name/entrypoints/api.py:
from fastapi import FastAPI
from fastapi.logger import logger
import logging
log = logging.getLogger("gunicorn.error")
logger.handlers = log.handlers
if __name__ != "main":
logger.setLevel(log.level)
else:
logger.setLevel(logging.DEBUG)
app = FastAPI()
Logging to Sentry
FastAPI can integrate with Sentry (https://philstories.medium.com/integrate-sentry-to-fastapi-7250603c070f) or similar application loggers (python_logging.md) through the ASGI
middleware (https://fastapi.tiangolo.com/advanced/middleware/#other-middlewares).
File: tests/api_server.py:
app = FastAPI()
@app.get("/existent")
async def existent():
return {"msg": "exists!"}
@app.get("/inexistent")
async def inexistent():
raise HTTPException(status_code=404, detail="It doesn't exist")
File: tests/conftest.py:
from multiprocessing import Process
@pytest.fixture()
def _server() -> Generator[None, None, None]:
"""Start the fake api server."""
proc = Process(target=run_server, args=(), daemon=True)
proc.start()
yield
proc.kill() # Cleanup after test
Now you can use the server: None fixture in your tests and run your queries against http://localhost:8000.
Issues
FastAPI does not log messages (https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker/issues/19): update pyscrobbler and any other maintained applications and
remove the snippet defined in the logging section.
References
Docs (https://fastapi.tiangolo.com/)
Git (https://github.com/tiangolo/fastapi)