chore: cleanup before v1 (#73)

* chore: move dependencies module to the project root

* chore:update README

* chore: clean conftest

* chore: remove old code + comment

* fix: uv.lock dependencies
This commit is contained in:
d3vyce
2026-02-19 11:49:57 +01:00
committed by GitHub
parent 32ed36e102
commit 73fae04333
7 changed files with 12 additions and 139 deletions

View File

@@ -1,6 +1,6 @@
# FastAPI Toolsets # FastAPI Toolsets
FastAPI Toolsets provides production-ready utilities for FastAPI applications built with async SQLAlchemy and PostgreSQL. It includes generic CRUD operations, a fixture system with dependency resolution, a Django-like CLI, standardized API responses, and structured exception handling with automatic OpenAPI documentation. A modular collection of production-ready utilities for FastAPI. Install only what you need — from async CRUD and database helpers to CLI tooling, Prometheus metrics, and pytest fixtures. Each module is independently installable via optional extras, keeping your dependency footprint minimal.
[![CI](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml) [![CI](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/d3vyce/fastapi-toolsets/graph/badge.svg)](https://codecov.io/gh/d3vyce/fastapi-toolsets) [![codecov](https://codecov.io/gh/d3vyce/fastapi-toolsets/graph/badge.svg)](https://codecov.io/gh/d3vyce/fastapi-toolsets)

View File

@@ -8,7 +8,9 @@ from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from ..crud import CrudFactory from .crud import CrudFactory
__all__ = ["BodyDependency", "PathDependency"]
ModelType = TypeVar("ModelType", bound=DeclarativeBase) ModelType = TypeVar("ModelType", bound=DeclarativeBase)
SessionDependency = Callable[[], AsyncGenerator[AsyncSession, None]] SessionDependency = Callable[[], AsyncGenerator[AsyncSession, None]]

View File

@@ -1,5 +0,0 @@
"""FastAPI dependency factories for database objects."""
from .factory import BodyDependency, PathDependency
__all__ = ["BodyDependency", "PathDependency"]

View File

@@ -76,55 +76,6 @@ class ConflictError(ApiException):
) )
class InsufficientRolesError(ForbiddenError):
"""User does not have the required roles."""
api_error = ApiError(
code=403,
msg="Insufficient Roles",
desc="You do not have the required roles to access this resource.",
err_code="RBAC-403",
)
def __init__(self, required_roles: list[str], user_roles: set[str] | None = None):
"""Initialize the exception.
Args:
required_roles: Roles needed to access the resource
user_roles: Roles the current user has, if known
"""
self.required_roles = required_roles
self.user_roles = user_roles
desc = f"Required roles: {', '.join(required_roles)}"
if user_roles is not None:
desc += f". User has: {', '.join(user_roles) if user_roles else 'no roles'}"
super().__init__(desc)
class UserNotFoundError(NotFoundError):
"""User was not found."""
api_error = ApiError(
code=404,
msg="User Not Found",
desc="The requested user was not found.",
err_code="USER-404",
)
class RoleNotFoundError(NotFoundError):
"""Role was not found."""
api_error = ApiError(
code=404,
msg="Role Not Found",
desc="The requested role was not found.",
err_code="ROLE-404",
)
class NoSearchableFieldsError(ApiException): class NoSearchableFieldsError(ApiException):
"""Raised when search is requested but no searchable fields are available.""" """Raised when search is requested but no searchable fields are available."""

View File

@@ -1,55 +1,4 @@
"""Pytest plugin for using FixtureRegistry fixtures in tests. """Pytest plugin for using FixtureRegistry fixtures in tests."""
This module provides utilities to automatically generate pytest fixtures
from your FixtureRegistry, with proper dependency resolution.
Example:
# conftest.py
import pytest
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.fixtures import fixtures # Your FixtureRegistry
from app.models import Base
from fastapi_toolsets.pytest_plugin import register_fixtures
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
@pytest.fixture
async def engine():
engine = create_async_engine(DATABASE_URL)
yield engine
await engine.dispose()
@pytest.fixture
async def db_session(engine):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
session_factory = async_sessionmaker(engine, expire_on_commit=False)
session = session_factory()
try:
yield session
finally:
await session.close()
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
# Automatically generate pytest fixtures from registry
# Creates: fixture_roles, fixture_users, fixture_posts, etc.
register_fixtures(fixtures, globals())
Usage in tests:
# test_users.py
async def test_user_count(db_session, fixture_users):
# fixture_users automatically loads fixture_roles first (if dependency)
# and returns the list of User models
assert len(fixture_users) > 0
async def test_user_role(db_session, fixture_users):
user = fixture_users[0]
assert user.role_id is not None
"""
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from typing import Any from typing import Any

View File

@@ -11,18 +11,12 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from fastapi_toolsets.crud import CrudFactory from fastapi_toolsets.crud import CrudFactory
# PostgreSQL connection URL from environment or default for local development DATABASE_URL = os.getenv(
DATABASE_URL = os.getenv("DATABASE_URL") or os.getenv( key="DATABASE_URL",
"TEST_DATABASE_URL", default="postgresql+asyncpg://postgres:postgres@localhost:5432/postgres",
"postgresql+asyncpg://postgres:postgres@localhost:5432/fastapi_toolsets_test",
) )
# =============================================================================
# Test Models
# =============================================================================
class Base(DeclarativeBase): class Base(DeclarativeBase):
"""Base class for test models.""" """Base class for test models."""
@@ -89,11 +83,6 @@ class Post(Base):
tags: Mapped[list[Tag]] = relationship(secondary=post_tags) tags: Mapped[list[Tag]] = relationship(secondary=post_tags)
# =============================================================================
# Test Schemas
# =============================================================================
class RoleCreate(BaseModel): class RoleCreate(BaseModel):
"""Schema for creating a role.""" """Schema for creating a role."""
@@ -171,10 +160,6 @@ class PostM2MUpdate(BaseModel):
tag_ids: list[uuid.UUID] | None = None tag_ids: list[uuid.UUID] | None = None
# =============================================================================
# CRUD Classes
# =============================================================================
RoleCrud = CrudFactory(Role) RoleCrud = CrudFactory(Role)
UserCrud = CrudFactory(User) UserCrud = CrudFactory(User)
PostCrud = CrudFactory(Post) PostCrud = CrudFactory(Post)
@@ -182,11 +167,6 @@ TagCrud = CrudFactory(Tag)
PostM2MCrud = CrudFactory(Post, m2m_fields={"tag_ids": Post.tags}) PostM2MCrud = CrudFactory(Post, m2m_fields={"tag_ids": Post.tags})
# =============================================================================
# Fixtures
# =============================================================================
@pytest.fixture @pytest.fixture
def anyio_backend(): def anyio_backend():
"""Use asyncio for async tests.""" """Use asyncio for async tests."""

12
uv.lock generated
View File

@@ -245,6 +245,7 @@ name = "fastapi-toolsets"
version = "0.10.0" version = "0.10.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "asyncpg" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "sqlalchemy", extra = ["asyncio"] }, { name = "sqlalchemy", extra = ["asyncio"] },
@@ -252,21 +253,16 @@ dependencies = [
[package.optional-dependencies] [package.optional-dependencies]
all = [ all = [
{ name = "asyncpg" },
{ name = "httpx" }, { name = "httpx" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-xdist" }, { name = "pytest-xdist" },
{ name = "typer" }, { name = "typer" },
] ]
asyncpg = [
{ name = "asyncpg" },
]
cli = [ cli = [
{ name = "typer" }, { name = "typer" },
] ]
dev = [ dev = [
{ name = "asyncpg" },
{ name = "coverage" }, { name = "coverage" },
{ name = "httpx" }, { name = "httpx" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
@@ -297,11 +293,11 @@ test = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "asyncpg", marker = "extra == 'asyncpg'", specifier = ">=0.29.0" }, { name = "asyncpg", specifier = ">=0.29.0" },
{ name = "coverage", marker = "extra == 'test'", specifier = ">=7.0.0" }, { name = "coverage", marker = "extra == 'test'", specifier = ">=7.0.0" },
{ name = "fastapi", specifier = ">=0.100.0" }, { name = "fastapi", specifier = ">=0.100.0" },
{ name = "fastapi-toolsets", extras = ["all", "test"], marker = "extra == 'dev'" }, { name = "fastapi-toolsets", extras = ["all", "test"], marker = "extra == 'dev'" },
{ name = "fastapi-toolsets", extras = ["asyncpg", "cli", "metrics", "pytest"], marker = "extra == 'all'" }, { name = "fastapi-toolsets", extras = ["cli", "metrics", "pytest"], marker = "extra == 'all'" },
{ name = "fastapi-toolsets", extras = ["pytest"], marker = "extra == 'test'" }, { name = "fastapi-toolsets", extras = ["pytest"], marker = "extra == 'test'" },
{ name = "httpx", marker = "extra == 'pytest'", specifier = ">=0.25.0" }, { name = "httpx", marker = "extra == 'pytest'", specifier = ">=0.25.0" },
{ name = "prometheus-client", marker = "extra == 'metrics'", specifier = ">=0.20.0" }, { name = "prometheus-client", marker = "extra == 'metrics'", specifier = ">=0.20.0" },
@@ -315,7 +311,7 @@ requires-dist = [
{ name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.1a0" }, { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.1a0" },
{ name = "typer", marker = "extra == 'cli'", specifier = ">=0.9.0" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.9.0" },
] ]
provides-extras = ["asyncpg", "cli", "metrics", "pytest", "all", "test", "dev"] provides-extras = ["cli", "metrics", "pytest", "all", "test", "dev"]
[[package]] [[package]]
name = "greenlet" name = "greenlet"