fastapi-toolsets 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ """Pytest plugin for using FixtureRegistry fixtures in tests.
2
+
3
+ This module provides utilities to automatically generate pytest fixtures
4
+ from your FixtureRegistry, with proper dependency resolution.
5
+
6
+ Example:
7
+ # conftest.py
8
+ import pytest
9
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
10
+
11
+ from app.fixtures import fixtures # Your FixtureRegistry
12
+ from app.models import Base
13
+ from fastapi_toolsets.pytest_plugin import register_fixtures
14
+
15
+ DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
16
+
17
+ @pytest.fixture
18
+ async def engine():
19
+ engine = create_async_engine(DATABASE_URL)
20
+ yield engine
21
+ await engine.dispose()
22
+
23
+ @pytest.fixture
24
+ async def db_session(engine):
25
+ async with engine.begin() as conn:
26
+ await conn.run_sync(Base.metadata.create_all)
27
+
28
+ session_factory = async_sessionmaker(engine, expire_on_commit=False)
29
+ session = session_factory()
30
+
31
+ try:
32
+ yield session
33
+ finally:
34
+ await session.close()
35
+ async with engine.begin() as conn:
36
+ await conn.run_sync(Base.metadata.drop_all)
37
+
38
+ # Automatically generate pytest fixtures from registry
39
+ # Creates: fixture_roles, fixture_users, fixture_posts, etc.
40
+ register_fixtures(fixtures, globals())
41
+
42
+ Usage in tests:
43
+ # test_users.py
44
+ async def test_user_count(db_session, fixture_users):
45
+ # fixture_users automatically loads fixture_roles first (if dependency)
46
+ # and returns the list of User models
47
+ assert len(fixture_users) > 0
48
+
49
+ async def test_user_role(db_session, fixture_users):
50
+ user = fixture_users[0]
51
+ assert user.role_id is not None
52
+ """
53
+
54
+ from collections.abc import Callable, Sequence
55
+ from typing import Any
56
+
57
+ import pytest
58
+ from sqlalchemy.ext.asyncio import AsyncSession
59
+ from sqlalchemy.orm import DeclarativeBase
60
+
61
+ from ..db import get_transaction
62
+ from .fixtures import FixtureRegistry, LoadStrategy
63
+
64
+
65
+ def register_fixtures(
66
+ registry: FixtureRegistry,
67
+ namespace: dict[str, Any],
68
+ *,
69
+ prefix: str = "fixture_",
70
+ session_fixture: str = "db_session",
71
+ strategy: LoadStrategy = LoadStrategy.MERGE,
72
+ ) -> list[str]:
73
+ """Register pytest fixtures from a FixtureRegistry.
74
+
75
+ Automatically creates pytest fixtures for each fixture in the registry.
76
+ Dependencies are resolved via pytest fixture dependencies.
77
+
78
+ Args:
79
+ registry: The FixtureRegistry containing fixtures
80
+ namespace: The module's globals() dict to add fixtures to
81
+ prefix: Prefix for generated fixture names (default: "fixture_")
82
+ session_fixture: Name of the db session fixture (default: "db_session")
83
+ strategy: Loading strategy for fixtures (default: MERGE)
84
+
85
+ Returns:
86
+ List of created fixture names
87
+
88
+ Example:
89
+ # conftest.py
90
+ from app.fixtures import fixtures
91
+ from fastapi_toolsets.pytest_plugin import register_fixtures
92
+
93
+ register_fixtures(fixtures, globals())
94
+
95
+ # Creates fixtures like:
96
+ # - fixture_roles
97
+ # - fixture_users (depends on fixture_roles if users depends on roles)
98
+ # - fixture_posts (depends on fixture_users if posts depends on users)
99
+ """
100
+ created_fixtures: list[str] = []
101
+
102
+ for fixture in registry.get_all():
103
+ fixture_name = f"{prefix}{fixture.name}"
104
+
105
+ # Build list of pytest fixture dependencies
106
+ pytest_deps = [session_fixture]
107
+ for dep in fixture.depends_on:
108
+ pytest_deps.append(f"{prefix}{dep}")
109
+
110
+ # Create the fixture function
111
+ fixture_func = _create_fixture_function(
112
+ registry=registry,
113
+ fixture_name=fixture.name,
114
+ dependencies=pytest_deps,
115
+ strategy=strategy,
116
+ )
117
+
118
+ # Apply pytest.fixture decorator
119
+ decorated = pytest.fixture(fixture_func)
120
+
121
+ # Add to namespace
122
+ namespace[fixture_name] = decorated
123
+ created_fixtures.append(fixture_name)
124
+
125
+ return created_fixtures
126
+
127
+
128
+ def _create_fixture_function(
129
+ registry: FixtureRegistry,
130
+ fixture_name: str,
131
+ dependencies: list[str],
132
+ strategy: LoadStrategy,
133
+ ) -> Callable[..., Any]:
134
+ """Create a fixture function with the correct signature.
135
+
136
+ The function signature must include all dependencies as parameters
137
+ for pytest to resolve them correctly.
138
+ """
139
+ # Get the fixture definition
140
+ fixture_def = registry.get(fixture_name)
141
+
142
+ # Build the function dynamically with correct parameters
143
+ # We need the session as first param, then all dependencies
144
+ async def fixture_func(**kwargs: Any) -> Sequence[DeclarativeBase]:
145
+ # Get session from kwargs (first dependency)
146
+ session: AsyncSession = kwargs[dependencies[0]]
147
+
148
+ # Load the fixture data
149
+ instances = list(fixture_def.func())
150
+
151
+ if not instances:
152
+ return []
153
+
154
+ loaded: list[DeclarativeBase] = []
155
+
156
+ async with get_transaction(session):
157
+ for instance in instances:
158
+ if strategy == LoadStrategy.INSERT:
159
+ session.add(instance)
160
+ loaded.append(instance)
161
+ elif strategy == LoadStrategy.MERGE:
162
+ merged = await session.merge(instance)
163
+ loaded.append(merged)
164
+ elif strategy == LoadStrategy.SKIP_EXISTING:
165
+ pk = _get_primary_key(instance)
166
+ if pk is not None:
167
+ existing = await session.get(type(instance), pk)
168
+ if existing is None:
169
+ session.add(instance)
170
+ loaded.append(instance)
171
+ else:
172
+ loaded.append(existing)
173
+ else:
174
+ session.add(instance)
175
+ loaded.append(instance)
176
+
177
+ return loaded
178
+
179
+ # Update function signature to include dependencies
180
+ # This is needed for pytest to inject the right fixtures
181
+ params = ", ".join(dependencies)
182
+ code = f"async def {fixture_name}_fixture({params}):\n return await _impl({', '.join(f'{d}={d}' for d in dependencies)})"
183
+
184
+ local_ns: dict[str, Any] = {"_impl": fixture_func}
185
+ exec(code, local_ns) # noqa: S102
186
+
187
+ created_func = local_ns[f"{fixture_name}_fixture"]
188
+ created_func.__doc__ = f"Load {fixture_name} fixture data."
189
+
190
+ return created_func
191
+
192
+
193
+ def _get_primary_key(instance: DeclarativeBase) -> Any | None:
194
+ """Get the primary key value of a model instance."""
195
+ mapper = instance.__class__.__mapper__
196
+ pk_cols = mapper.primary_key
197
+
198
+ if len(pk_cols) == 1:
199
+ return getattr(instance, pk_cols[0].name, None)
200
+
201
+ pk_values = tuple(getattr(instance, col.name, None) for col in pk_cols)
202
+ if all(v is not None for v in pk_values):
203
+ return pk_values
204
+ return None
File without changes
@@ -0,0 +1,116 @@
1
+ """Base Pydantic schemas for API responses."""
2
+
3
+ from enum import Enum
4
+ from typing import ClassVar, Generic, TypeVar
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+ __all__ = [
9
+ "ApiError",
10
+ "ErrorResponse",
11
+ "Pagination",
12
+ "PaginatedResponse",
13
+ "Response",
14
+ "ResponseStatus",
15
+ ]
16
+
17
+ DataT = TypeVar("DataT")
18
+
19
+
20
+ class PydanticBase(BaseModel):
21
+ """Base class for all Pydantic models with common configuration."""
22
+
23
+ model_config: ClassVar[ConfigDict] = ConfigDict(
24
+ from_attributes=True,
25
+ validate_assignment=True,
26
+ )
27
+
28
+
29
+ class ResponseStatus(str, Enum):
30
+ """Standard API response status."""
31
+
32
+ SUCCESS = "SUCCESS"
33
+ FAIL = "FAIL"
34
+
35
+
36
+ class ApiError(PydanticBase):
37
+ """Structured API error definition.
38
+
39
+ Used to define standard error responses with consistent format.
40
+
41
+ Attributes:
42
+ code: HTTP status code
43
+ msg: Short error message
44
+ desc: Detailed error description
45
+ err_code: Application-specific error code (e.g., "AUTH-401")
46
+ """
47
+
48
+ code: int
49
+ msg: str
50
+ desc: str
51
+ err_code: str
52
+
53
+
54
+ class BaseResponse(PydanticBase):
55
+ """Base response structure for all API responses.
56
+
57
+ Attributes:
58
+ status: SUCCESS or FAIL
59
+ message: Human-readable message
60
+ error_code: Error code if status is FAIL, None otherwise
61
+ """
62
+
63
+ status: ResponseStatus = ResponseStatus.SUCCESS
64
+ message: str = "Success"
65
+ error_code: str | None = None
66
+
67
+
68
+ class Response(BaseResponse, Generic[DataT]):
69
+ """Generic API response with data payload.
70
+
71
+ Example:
72
+ Response[UserRead](data=user, message="User retrieved")
73
+ """
74
+
75
+ data: DataT | None = None
76
+
77
+
78
+ class ErrorResponse(BaseResponse):
79
+ """Error response with additional description field.
80
+
81
+ Used for error responses that need more context.
82
+ """
83
+
84
+ status: ResponseStatus = ResponseStatus.FAIL
85
+ description: str | None = None
86
+ data: None = None
87
+
88
+
89
+ class Pagination(PydanticBase):
90
+ """Pagination metadata for list responses.
91
+
92
+ Attributes:
93
+ total_count: Total number of items across all pages
94
+ items_per_page: Number of items per page
95
+ page: Current page number (1-indexed)
96
+ has_more: Whether there are more pages
97
+ """
98
+
99
+ total_count: int
100
+ items_per_page: int
101
+ page: int
102
+ has_more: bool
103
+
104
+
105
+ class PaginatedResponse(BaseResponse, Generic[DataT]):
106
+ """Paginated API response for list endpoints.
107
+
108
+ Example:
109
+ PaginatedResponse[UserRead](
110
+ data=users,
111
+ pagination=Pagination(total_count=100, items_per_page=10, page=1, has_more=True)
112
+ )
113
+ """
114
+
115
+ data: list[DataT]
116
+ pagination: Pagination
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-toolsets
3
+ Version: 0.1.0
4
+ Summary: Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL
5
+ Keywords: fastapi,sqlalchemy,postgresql
6
+ Author: d3vyce
7
+ Author-email: d3vyce <contact@d3vyce.fr>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Framework :: Pydantic
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Topic :: Software Development
26
+ Classifier: Typing :: Typed
27
+ Requires-Dist: fastapi>=0.100.0
28
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
29
+ Requires-Dist: asyncpg>=0.29.0
30
+ Requires-Dist: pydantic>=2.0
31
+ Requires-Dist: typer>=0.9.0
32
+ Requires-Dist: httpx>=0.25.0
33
+ Requires-Dist: fastapi-toolsets[test] ; extra == 'dev'
34
+ Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
35
+ Requires-Dist: ty>=0.0.1a0 ; extra == 'dev'
36
+ Requires-Dist: pytest>=8.0.0 ; extra == 'test'
37
+ Requires-Dist: pytest-anyio>=0.0.0 ; extra == 'test'
38
+ Requires-Dist: coverage>=7.0.0 ; extra == 'test'
39
+ Requires-Dist: pytest-cov>=4.0.0 ; extra == 'test'
40
+ Requires-Python: >=3.11
41
+ Project-URL: Homepage, https://github.com/d3vyce/fastapi-toolsets
42
+ Project-URL: Documentation, https://fastapi-toolsets.d3vyce.fr/
43
+ Project-URL: Repository, https://github.com/d3vyce/fastapi-toolsets
44
+ Project-URL: Issues, https://github.com/d3vyce/fastapi-toolsets/issues
45
+ Provides-Extra: dev
46
+ Provides-Extra: test
47
+ Description-Content-Type: text/markdown
48
+
49
+ # FastAPI Toolsets
50
+
51
+ 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.
52
+
53
+ [![CI](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml)
54
+ [![codecov](https://codecov.io/gh/d3vyce/fastapi-toolsets/graph/badge.svg)](https://codecov.io/gh/d3vyce/fastapi-toolsets)
55
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
56
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
57
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
58
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
59
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
60
+
61
+ ---
62
+
63
+ **Documentation**: [https://fastapi-toolsets.d3vyce.fr](https://fastapi-toolsets.d3vyce.fr)
64
+
65
+ **Source Code**: [https://github.com/d3vyce/fastapi-toolsets](https://github.com/d3vyce/fastapi-toolsets)
66
+
67
+ ---
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ uv add fastapi-toolsets
73
+ ```
74
+
75
+ ## Features
76
+
77
+ - **CRUD**: Generic async CRUD operations with `CrudFactory`
78
+ - **Fixtures**: Fixture system with dependency management, context support and pytest integration
79
+ - **CLI**: Django-like command-line interface for fixtures and custom commands
80
+ - **Standardized API Responses**: Consistent response format across your API
81
+ - **Exception Handling**: Structured error responses with automatic OpenAPI documentation
82
+
83
+ ## License
84
+
85
+ MIT License - see [LICENSE](LICENSE) for details.
86
+
87
+ ## Contributing
88
+
89
+ Contributions are welcome! Please feel free to submit issues and pull requests.
@@ -0,0 +1,20 @@
1
+ fastapi_toolsets/__init__.py,sha256=bLVbdgVNhQdN9LhAvb4FPNrgoIqHUjLx9TvgJzm46Jc,820
2
+ fastapi_toolsets/cli/__init__.py,sha256=QAcrenphE7D5toid_Kn777Cy1icSOWiEBjSE_oCuU4o,111
3
+ fastapi_toolsets/cli/app.py,sha256=G3y3PNN3biHs8GtiiGsrX2X8nXNjkrsUhHLXQo2-MXc,2588
4
+ fastapi_toolsets/cli/commands/__init__.py,sha256=BogehmsY6olwLdfIBzviuppXP1LLl9znnxtmji3eLwI,29
5
+ fastapi_toolsets/cli/commands/fixtures.py,sha256=qiC2dcrJ_Rb1PRzx6EycTFQQXwa_GQUVoptpx4chf9k,6679
6
+ fastapi_toolsets/crud.py,sha256=pD26V91y0Z5f9ft0I6ggY1EJ-Oh-QATgANgzEyj4EHU,11370
7
+ fastapi_toolsets/db.py,sha256=YUj5CrxCnREg7AqpJLNrLR2RDIOCS7stQCNOSS3cRho,5619
8
+ fastapi_toolsets/exceptions/__init__.py,sha256=PDiTg4NpEUhGi5p9Sfvn1FtxdlD9-Y4OPhWCQz6fW9g,391
9
+ fastapi_toolsets/exceptions/exceptions.py,sha256=O6fqDbPfEFSj1oi_vo3-gnES7RPpSf-l_yXh4fTCBjg,4470
10
+ fastapi_toolsets/exceptions/handler.py,sha256=IXfKiIr_LPo-11PRpOIrNRAXBkeQ5TdLcu3Gy-r6ChU,5916
11
+ fastapi_toolsets/fixtures/__init__.py,sha256=EWODe3vwjyrnZ_4jEpDdQ4NFLmYvdFqvvvrRaKq6Bsk,323
12
+ fastapi_toolsets/fixtures/fixtures.py,sha256=RB073yojMQoRCxP_-pvZWSW_viPQsD3RRJ5NhEZ2wZQ,9720
13
+ fastapi_toolsets/fixtures/pytest_plugin.py,sha256=J2PjasekQUxzurk7_MVncQRcIZ1AIawsO6OlqKqKYT8,6891
14
+ fastapi_toolsets/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ fastapi_toolsets/schemas.py,sha256=LBzrq4s5VWYeQqlUfOEvWDtpFdO8scgY0LRypk9KUAE,2639
16
+ fastapi_toolsets-0.1.0.dist-info/licenses/LICENSE,sha256=V2jCjI-VPB-veGY2Ktb0sU4vT_TldRciZ9lCE98bMoE,1063
17
+ fastapi_toolsets-0.1.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
18
+ fastapi_toolsets-0.1.0.dist-info/entry_points.txt,sha256=pNU38Nn_DXBgYd-nLZCizMvrrdaPhHmkRwouDoBqvzw,63
19
+ fastapi_toolsets-0.1.0.dist-info/METADATA,sha256=fRrFRvKtO4m2lbF0pq6qgbHxrHTPitfMSVkD6WlknXU,4170
20
+ fastapi_toolsets-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.26
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ fastapi-toolsets = fastapi_toolsets.cli:app
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 d3vyce
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.