Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions fastapi_startkit/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ dev = [
"langchain>=1.0.0",
"langchain-core>=1.0.0",
"pyright>=1.1.411",
"pytest-benchmark>=5.2.3",
]


Expand Down Expand Up @@ -135,6 +136,10 @@ pythonVersion = "3.12"
[tool.pytest.ini_options]
asyncio_mode = "auto"
norecursedirs = [".venv", "dist", "build", "__pycache__"]
markers = [
"benchmark: performance benchmark, excluded by default (run with -m benchmark)",
]
addopts = "-m 'not benchmark'"

[tool.coverage.run]
source = ["src/fastapi_startkit"]
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Benchmarks for Application boot and provider register/boot (task #1045).

Deselected by default (see `addopts` in pyproject.toml); run explicitly with:

uv run pytest -m benchmark tests/fastapi/benchmarks/test_application_boot_benchmark.py
"""

import pytest
from fastapi import FastAPI

from fastapi_startkit.application import Application
from fastapi_startkit.container.container import Container
from fastapi_startkit.fastapi.providers.fastapi_provider import FastAPIProvider


@pytest.fixture(autouse=True)
def restore_container_instance():
"""Application() replaces the global Container singleton; restore it after each round."""
original = Container._instance
yield
Container._instance = original


@pytest.mark.benchmark(group="application-boot")
class TestApplicationBootBenchmarks:
def test_boot_default_providers(self, benchmark, tmp_path):
"""Full Application() init: environment resolution, config, default providers register+boot."""
benchmark(Application, base_path=tmp_path, env="testing")

def test_boot_with_fastapi_provider(self, benchmark, tmp_path):
"""Same as above, plus FastAPIProvider register+boot (FastAPI instance, exception handlers, command)."""
benchmark(Application, base_path=tmp_path, env="testing", providers=[FastAPIProvider])

def test_boot_raw_fastapi_baseline(self, benchmark):
"""Raw FastAPI() instantiation — the floor that Application's boot overhead is measured against."""
benchmark(FastAPI)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Benchmarks for request handling through the FastAPI integration layer (task #1045).

Deselected by default (see `addopts` in pyproject.toml); run explicitly with:

uv run pytest -m benchmark tests/fastapi/benchmarks/test_request_handling_benchmark.py
"""

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from fastapi_startkit.fastapi.routers.router import Router


def ping():
return {"ok": True}


def show_post(post: int):
return {"id": post}


class PostController:
def show(self, post):
return show_post(post)


@pytest.fixture(scope="module")
def router_based_client():
"""A trivial GET endpoint registered through the Router wrapper."""
router = Router()
router.get("/ping", ping)
app = FastAPI()
app.include_router(router.router)
with TestClient(app) as client:
yield client


@pytest.fixture(scope="module")
def raw_fastapi_client():
"""The same endpoint registered directly on FastAPI, bypassing the Router wrapper."""
app = FastAPI()
app.get("/ping")(ping)
with TestClient(app) as client:
yield client


@pytest.fixture(scope="module")
def resource_client():
"""A resource()-expanded route with a path parameter, exercising Starlette's path matching."""
router = Router()
router.resource("posts", PostController)
app = FastAPI()
app.include_router(router.router)
with TestClient(app) as client:
yield client


@pytest.mark.benchmark(group="request-handling-json-get")
class TestRequestHandlingBenchmarks:
def test_router_based_request(self, benchmark, router_based_client):
benchmark(router_based_client.get, "/ping")

def test_raw_fastapi_request_baseline(self, benchmark, raw_fastapi_client):
benchmark(raw_fastapi_client.get, "/ping")

def test_resource_route_with_path_param(self, benchmark, resource_client):
benchmark(resource_client.get, "/posts/1")
71 changes: 71 additions & 0 deletions fastapi_startkit/tests/fastapi/benchmarks/test_router_benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Benchmarks for Router route registration and resource() CRUD expansion (task #1045).

Deselected by default (see `addopts` in pyproject.toml); run explicitly with:

uv run pytest -m benchmark tests/fastapi/benchmarks/test_router_benchmark.py
"""

import pytest
from fastapi import APIRouter

from fastapi_startkit.fastapi.routers.router import Router


def endpoint():
return {"ok": True}


class PostController:
def index(self):
return []

def create(self):
return {}

def store(self):
return {}

def show(self, post):
return {"id": post}

def edit(self, post):
return {"id": post}

def update(self, post):
return {"id": post}

def destroy(self, post):
return {"id": post}


@pytest.mark.benchmark(group="router-single-route")
class TestSingleRouteRegistrationBenchmarks:
def test_router_get(self, benchmark):
benchmark(lambda: Router().get("/items", endpoint))

def test_raw_apirouter_get_baseline(self, benchmark):
"""Registering the same route directly on APIRouter, bypassing the Router wrapper."""
benchmark(lambda: APIRouter().add_api_route("/items", endpoint, methods=["GET"]))


@pytest.mark.benchmark(group="router-resource-expansion")
class TestResourceExpansionBenchmarks:
def test_resource_full_crud(self, benchmark):
"""Router.resource() expanding a controller into all 7 CRUD routes."""
benchmark(lambda: Router().resource("posts", PostController))

def test_raw_apirouter_equivalent_crud_baseline(self, benchmark):
"""Hand-registering the same 7 routes directly — the floor resource() is measured against."""

def register():
router = APIRouter()
controller = PostController()
router.add_api_route("/posts", controller.index, methods=["GET"], name="posts")
router.add_api_route("/posts/create", controller.create, methods=["GET"], name="posts.create")
router.add_api_route("/posts", controller.store, methods=["POST"], name="posts.store")
router.add_api_route("/posts/{post}", controller.show, methods=["GET"], name="posts.show")
router.add_api_route("/posts/{post}/edit", controller.edit, methods=["GET"], name="posts.edit")
router.add_api_route("/posts/{post}", controller.update, methods=["PUT"], name="posts.update")
router.add_api_route("/posts/{post}", controller.destroy, methods=["DELETE"], name="posts.destroy")

benchmark(register)
24 changes: 24 additions & 0 deletions fastapi_startkit/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading