FastAPI Examples

This is an example of the Tortoise-ORM FastAPI integration

Usage:

uvicorn main:app --reload

Basic non-relational example

models.py

from tortoise import fields, models


class Users(models.Model):
    """
    The User model
    """

    id = fields.IntField(primary_key=True)
    #: This is a username
    username = fields.CharField(max_length=20, unique=True)
    name = fields.CharField(max_length=50, null=True)
    family_name = fields.CharField(max_length=50, null=True)
    category = fields.CharField(max_length=30, default="misc")
    password_hash = fields.CharField(max_length=128, null=True)
    created_at = fields.DatetimeField(auto_now_add=True)
    modified_at = fields.DatetimeField(auto_now=True)

    def full_name(self) -> str:
        """
        Returns the best name
        """
        if self.name or self.family_name:
            return f"{self.name or ''} {self.family_name or ''}".strip()
        return self.username

    class PydanticMeta:
        computed = ["full_name"]
        exclude = ["password_hash"]

tests.py

# mypy: no-disallow-untyped-decorators
# pylint: disable=E0611,E0401
import multiprocessing
import os
from concurrent.futures import ProcessPoolExecutor
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
from typing import AsyncGenerator, Tuple

import anyio
import pytest
import pytz
from asgi_lifespan import LifespanManager
from httpx import ASGITransport, AsyncClient

from tortoise.contrib.test import MEMORY_SQLITE
from tortoise.fields.data import JSON_LOADS

os.environ["DB_URL"] = MEMORY_SQLITE
try:
    from config import register_orm
    from main import app
    from main_custom_timezone import app as app_east
    from models import Users
    from schemas import User_Pydantic
except ImportError:
    if (cwd := Path.cwd()) == (parent := Path(__file__).parent):
        dirpath = "."
    else:
        dirpath = str(parent.relative_to(cwd))
    print(f"You may need to explicitly declare python path:\n\nexport PYTHONPATH={dirpath}\n")
    raise

ClientManagerType = AsyncGenerator[AsyncClient, None]


@pytest.fixture(scope="module")
def anyio_backend() -> str:
    return "asyncio"


@asynccontextmanager
async def client_manager(app, base_url="http://test", **kw) -> ClientManagerType:
    app.state.testing = True
    async with LifespanManager(app):
        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url=base_url, **kw) as c:
            yield c


@pytest.fixture(scope="module")
async def client() -> ClientManagerType:
    async with client_manager(app) as c:
        yield c


@pytest.fixture(scope="module")
async def client_east() -> ClientManagerType:
    async with client_manager(app_east) as c:
        yield c


class UserTester:
    async def create_user(self, async_client: AsyncClient) -> Users:
        response = await async_client.post("/users", json={"username": "admin"})
        assert response.status_code == 200, response.text
        data = response.json()
        assert data["username"] == "admin"
        assert "id" in data
        user_id = data["id"]

        user_obj = await Users.get(id=user_id)
        assert user_obj.id == user_id
        return user_obj

    async def user_list(self, async_client: AsyncClient) -> Tuple[datetime, Users, User_Pydantic]:
        utc_now = datetime.now(pytz.utc)
        user_obj = await Users.create(username="test")
        response = await async_client.get("/users")
        assert response.status_code == 200, response.text
        data = response.json()
        assert isinstance(data, list)
        item = await User_Pydantic.from_tortoise_orm(user_obj)
        assert JSON_LOADS(item.model_dump_json()) in data
        return utc_now, user_obj, item


class TestUser(UserTester):
    @pytest.mark.anyio
    async def test_create_user(self, client: AsyncClient) -> None:  # nosec
        await self.create_user(client)

    @pytest.mark.anyio
    async def test_user_list(self, client: AsyncClient) -> None:  # nosec
        await self.user_list(client)


class TestUserEast(UserTester):
    timezone = "Asia/Shanghai"
    delta_hours = 8

    @pytest.mark.anyio
    async def test_create_user_east(self, client_east: AsyncClient) -> None:  # nosec
        user_obj = await self.create_user(client_east)
        created_at = user_obj.created_at

        # Verify time zone
        asia_tz = pytz.timezone(self.timezone)
        asia_now = datetime.now(pytz.utc).astimezone(asia_tz)
        assert created_at.hour - asia_now.hour == 0

        # UTC timezone
        utc_tz = pytz.timezone("UTC")
        utc_now = datetime.now(pytz.utc).astimezone(utc_tz)
        assert (created_at.hour - utc_now.hour) in [self.delta_hours, self.delta_hours - 24]

    @pytest.mark.anyio
    async def test_user_list(self, client_east: AsyncClient) -> None:  # nosec
        time, user_obj, item = await self.user_list(client_east)
        created_at = user_obj.created_at
        assert (created_at.hour - time.hour) in [self.delta_hours, self.delta_hours - 24]
        assert item.model_dump()["created_at"].hour == created_at.hour


def query_without_app(pk: int) -> int:
    async def runner() -> bool:
        async with register_orm():
            return await Users.filter(id__gt=pk).count()

    return anyio.run(runner)


def test_query_without_app():
    multiprocessing.set_start_method("spawn")
    with ProcessPoolExecutor(max_workers=1) as executor:
        future = executor.submit(query_without_app, 0)
        assert future.result() >= 0

main.py

# pylint: disable=E0611,E0401
import os
from contextlib import asynccontextmanager
from typing import AsyncGenerator

from fastapi import FastAPI
from routers import router as users_router

from examples.fastapi.config import register_orm
from tortoise import Tortoise, generate_config
from tortoise.contrib.fastapi import RegisterTortoise


@asynccontextmanager
async def lifespan_test(app: FastAPI) -> AsyncGenerator[None, None]:
    config = generate_config(
        os.getenv("TORTOISE_TEST_DB", "sqlite://:memory:"),
        app_modules={"models": ["models"]},
        testing=True,
        connection_label="models",
    )
    async with RegisterTortoise(
        app=app,
        config=config,
        generate_schemas=True,
        add_exception_handlers=True,
        _create_db=True,
    ):
        # db connected
        yield
        # app teardown
    # db connections closed
    await Tortoise._drop_databases()


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    if getattr(app.state, "testing", None):
        async with lifespan_test(app) as _:
            yield
    else:
        # app startup
        async with register_orm(app):
            # db connected
            yield
            # app teardown
        # db connections closed


app = FastAPI(title="Tortoise ORM FastAPI example", lifespan=lifespan)
app.include_router(users_router, prefix="")