fastapi-toolsets 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/PKG-INFO +2 -1
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/pyproject.toml +2 -1
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/__init__.py +1 -1
- fastapi_toolsets-0.4.0/src/fastapi_toolsets/crud/__init__.py +17 -0
- fastapi_toolsets-0.3.0/src/fastapi_toolsets/crud.py → fastapi_toolsets-0.4.0/src/fastapi_toolsets/crud/factory.py +67 -29
- fastapi_toolsets-0.4.0/src/fastapi_toolsets/crud/search.py +145 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/exceptions/__init__.py +2 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/exceptions/exceptions.py +19 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/LICENSE +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/README.md +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/cli/__init__.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/cli/app.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/db.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/exceptions/handler.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/fixtures/enum.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/fixtures/registry.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/fixtures/utils.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/py.typed +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/pytest/__init__.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/pytest/plugin.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/pytest/utils.py +0 -0
- {fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/schemas.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-toolsets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL
|
|
5
5
|
Keywords: fastapi,sqlalchemy,postgresql
|
|
6
6
|
Author: d3vyce
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
25
|
Classifier: Topic :: Software Development :: Libraries
|
|
25
26
|
Classifier: Topic :: Software Development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-toolsets"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -24,6 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Programming Language :: Python :: 3.11",
|
|
25
25
|
"Programming Language :: Python :: 3.12",
|
|
26
26
|
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
27
28
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
29
|
"Topic :: Software Development :: Libraries",
|
|
29
30
|
"Topic :: Software Development",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Generic async CRUD operations for SQLAlchemy models."""
|
|
2
|
+
|
|
3
|
+
from ..exceptions import NoSearchableFieldsError
|
|
4
|
+
from .factory import CrudFactory
|
|
5
|
+
from .search import (
|
|
6
|
+
SearchConfig,
|
|
7
|
+
SearchFieldType,
|
|
8
|
+
get_searchable_fields,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CrudFactory",
|
|
13
|
+
"get_searchable_fields",
|
|
14
|
+
"NoSearchableFieldsError",
|
|
15
|
+
"SearchConfig",
|
|
16
|
+
"SearchFieldType",
|
|
17
|
+
]
|
|
@@ -12,13 +12,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
12
12
|
from sqlalchemy.orm import DeclarativeBase
|
|
13
13
|
from sqlalchemy.sql.roles import WhereHavingRole
|
|
14
14
|
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
|
|
18
|
-
__all__ = [
|
|
19
|
-
"AsyncCrud",
|
|
20
|
-
"CrudFactory",
|
|
21
|
-
]
|
|
15
|
+
from ..db import get_transaction
|
|
16
|
+
from ..exceptions import NotFoundError
|
|
17
|
+
from .search import SearchConfig, SearchFieldType, build_search_filters
|
|
22
18
|
|
|
23
19
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
|
24
20
|
|
|
@@ -27,20 +23,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
27
23
|
"""Generic async CRUD operations for SQLAlchemy models.
|
|
28
24
|
|
|
29
25
|
Subclass this and set the `model` class variable, or use `CrudFactory`.
|
|
30
|
-
|
|
31
|
-
Example:
|
|
32
|
-
class UserCrud(AsyncCrud[User]):
|
|
33
|
-
model = User
|
|
34
|
-
|
|
35
|
-
# Or use the factory:
|
|
36
|
-
UserCrud = CrudFactory(User)
|
|
37
|
-
|
|
38
|
-
# Then use it:
|
|
39
|
-
user = await UserCrud.get(session, [User.id == 1])
|
|
40
|
-
users = await UserCrud.get_multi(session, limit=10)
|
|
41
26
|
"""
|
|
42
27
|
|
|
43
28
|
model: ClassVar[type[DeclarativeBase]]
|
|
29
|
+
searchable_fields: ClassVar[Sequence[SearchFieldType] | None] = None
|
|
44
30
|
|
|
45
31
|
@classmethod
|
|
46
32
|
async def create(
|
|
@@ -313,6 +299,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
313
299
|
order_by: Any | None = None,
|
|
314
300
|
page: int = 1,
|
|
315
301
|
items_per_page: int = 20,
|
|
302
|
+
search: str | SearchConfig | None = None,
|
|
303
|
+
search_fields: Sequence[SearchFieldType] | None = None,
|
|
316
304
|
) -> dict[str, Any]:
|
|
317
305
|
"""Get paginated results with metadata.
|
|
318
306
|
|
|
@@ -323,23 +311,54 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
323
311
|
order_by: Column or list of columns to order by
|
|
324
312
|
page: Page number (1-indexed)
|
|
325
313
|
items_per_page: Number of items per page
|
|
314
|
+
search: Search query string or SearchConfig object
|
|
315
|
+
search_fields: Fields to search in (overrides class default)
|
|
326
316
|
|
|
327
317
|
Returns:
|
|
328
318
|
Dict with 'data' and 'pagination' keys
|
|
329
319
|
"""
|
|
330
|
-
filters = filters
|
|
320
|
+
filters = list(filters) if filters else []
|
|
331
321
|
offset = (page - 1) * items_per_page
|
|
322
|
+
joins: list[Any] = []
|
|
323
|
+
|
|
324
|
+
# Build search filters
|
|
325
|
+
if search:
|
|
326
|
+
search_filters, search_joins = build_search_filters(
|
|
327
|
+
cls.model,
|
|
328
|
+
search,
|
|
329
|
+
search_fields=search_fields,
|
|
330
|
+
default_fields=cls.searchable_fields,
|
|
331
|
+
)
|
|
332
|
+
filters.extend(search_filters)
|
|
333
|
+
joins.extend(search_joins)
|
|
332
334
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
order_by=order_by,
|
|
338
|
-
limit=items_per_page,
|
|
339
|
-
offset=offset,
|
|
340
|
-
)
|
|
335
|
+
# Build query with joins
|
|
336
|
+
q = select(cls.model)
|
|
337
|
+
for join_rel in joins:
|
|
338
|
+
q = q.outerjoin(join_rel)
|
|
341
339
|
|
|
342
|
-
|
|
340
|
+
if filters:
|
|
341
|
+
q = q.where(and_(*filters))
|
|
342
|
+
if load_options:
|
|
343
|
+
q = q.options(*load_options)
|
|
344
|
+
if order_by is not None:
|
|
345
|
+
q = q.order_by(order_by)
|
|
346
|
+
|
|
347
|
+
q = q.offset(offset).limit(items_per_page)
|
|
348
|
+
result = await session.execute(q)
|
|
349
|
+
items = result.unique().scalars().all()
|
|
350
|
+
|
|
351
|
+
# Count query (with same joins and filters)
|
|
352
|
+
pk_col = cls.model.__mapper__.primary_key[0]
|
|
353
|
+
count_q = select(func.count(func.distinct(getattr(cls.model, pk_col.name))))
|
|
354
|
+
count_q = count_q.select_from(cls.model)
|
|
355
|
+
for join_rel in joins:
|
|
356
|
+
count_q = count_q.outerjoin(join_rel)
|
|
357
|
+
if filters:
|
|
358
|
+
count_q = count_q.where(and_(*filters))
|
|
359
|
+
|
|
360
|
+
count_result = await session.execute(count_q)
|
|
361
|
+
total_count = count_result.scalar_one()
|
|
343
362
|
|
|
344
363
|
return {
|
|
345
364
|
"data": items,
|
|
@@ -354,11 +373,14 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
354
373
|
|
|
355
374
|
def CrudFactory(
|
|
356
375
|
model: type[ModelType],
|
|
376
|
+
*,
|
|
377
|
+
searchable_fields: Sequence[SearchFieldType] | None = None,
|
|
357
378
|
) -> type[AsyncCrud[ModelType]]:
|
|
358
379
|
"""Create a CRUD class for a specific model.
|
|
359
380
|
|
|
360
381
|
Args:
|
|
361
382
|
model: SQLAlchemy model class
|
|
383
|
+
searchable_fields: Optional list of searchable fields
|
|
362
384
|
|
|
363
385
|
Returns:
|
|
364
386
|
AsyncCrud subclass bound to the model
|
|
@@ -370,9 +392,25 @@ def CrudFactory(
|
|
|
370
392
|
UserCrud = CrudFactory(User)
|
|
371
393
|
PostCrud = CrudFactory(Post)
|
|
372
394
|
|
|
395
|
+
# With searchable fields:
|
|
396
|
+
UserCrud = CrudFactory(
|
|
397
|
+
User,
|
|
398
|
+
searchable_fields=[User.username, User.email, (User.role, Role.name)]
|
|
399
|
+
)
|
|
400
|
+
|
|
373
401
|
# Usage
|
|
374
402
|
user = await UserCrud.get(session, [User.id == 1])
|
|
375
403
|
posts = await PostCrud.get_multi(session, filters=[Post.user_id == user.id])
|
|
404
|
+
|
|
405
|
+
# With search
|
|
406
|
+
result = await UserCrud.paginate(session, search="john")
|
|
376
407
|
"""
|
|
377
|
-
cls = type(
|
|
408
|
+
cls = type(
|
|
409
|
+
f"Async{model.__name__}Crud",
|
|
410
|
+
(AsyncCrud,),
|
|
411
|
+
{
|
|
412
|
+
"model": model,
|
|
413
|
+
"searchable_fields": searchable_fields,
|
|
414
|
+
},
|
|
415
|
+
)
|
|
378
416
|
return cast(type[AsyncCrud[ModelType]], cls)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Search utilities for AsyncCrud."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import String, or_
|
|
8
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
9
|
+
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
10
|
+
|
|
11
|
+
from ..exceptions import NoSearchableFieldsError
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
15
|
+
|
|
16
|
+
SearchFieldType = InstrumentedAttribute[Any] | tuple[InstrumentedAttribute[Any], ...]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SearchConfig:
|
|
21
|
+
"""Advanced search configuration.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
query: The search string
|
|
25
|
+
fields: Fields to search (columns or tuples for relationships)
|
|
26
|
+
case_sensitive: Case-sensitive search (default: False)
|
|
27
|
+
match_mode: "any" (OR) or "all" (AND) to combine fields
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
query: str
|
|
31
|
+
fields: Sequence[SearchFieldType] | None = None
|
|
32
|
+
case_sensitive: bool = False
|
|
33
|
+
match_mode: Literal["any", "all"] = "any"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_searchable_fields(
|
|
37
|
+
model: type[DeclarativeBase],
|
|
38
|
+
*,
|
|
39
|
+
include_relationships: bool = True,
|
|
40
|
+
max_depth: int = 1,
|
|
41
|
+
) -> list[SearchFieldType]:
|
|
42
|
+
"""Auto-detect String fields on a model and its relationships.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
model: SQLAlchemy model class
|
|
46
|
+
include_relationships: Include fields from many-to-one/one-to-one relationships
|
|
47
|
+
max_depth: Max depth for relationship traversal (default: 1)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of columns and tuples (relationship, column)
|
|
51
|
+
"""
|
|
52
|
+
fields: list[SearchFieldType] = []
|
|
53
|
+
mapper = model.__mapper__
|
|
54
|
+
|
|
55
|
+
# Direct String columns
|
|
56
|
+
for col in mapper.columns:
|
|
57
|
+
if isinstance(col.type, String):
|
|
58
|
+
fields.append(getattr(model, col.key))
|
|
59
|
+
|
|
60
|
+
# Relationships (one-to-one, many-to-one only)
|
|
61
|
+
if include_relationships and max_depth > 0:
|
|
62
|
+
for rel_name, rel_prop in mapper.relationships.items():
|
|
63
|
+
if rel_prop.uselist: # Skip collections (one-to-many, many-to-many)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
rel_attr = getattr(model, rel_name)
|
|
67
|
+
related_model = rel_prop.mapper.class_
|
|
68
|
+
|
|
69
|
+
for col in related_model.__mapper__.columns:
|
|
70
|
+
if isinstance(col.type, String):
|
|
71
|
+
fields.append((rel_attr, getattr(related_model, col.key)))
|
|
72
|
+
|
|
73
|
+
return fields
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_search_filters(
|
|
77
|
+
model: type[DeclarativeBase],
|
|
78
|
+
search: str | SearchConfig,
|
|
79
|
+
search_fields: Sequence[SearchFieldType] | None = None,
|
|
80
|
+
default_fields: Sequence[SearchFieldType] | None = None,
|
|
81
|
+
) -> tuple[list["ColumnElement[bool]"], list[InstrumentedAttribute[Any]]]:
|
|
82
|
+
"""Build SQLAlchemy filter conditions for search.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
model: SQLAlchemy model class
|
|
86
|
+
search: Search string or SearchConfig
|
|
87
|
+
search_fields: Fields specified per-call (takes priority)
|
|
88
|
+
default_fields: Default fields (from ClassVar)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (filter_conditions, joins_needed)
|
|
92
|
+
"""
|
|
93
|
+
# Normalize input
|
|
94
|
+
if isinstance(search, str):
|
|
95
|
+
config = SearchConfig(query=search, fields=search_fields)
|
|
96
|
+
else:
|
|
97
|
+
config = search
|
|
98
|
+
if search_fields is not None:
|
|
99
|
+
config = SearchConfig(
|
|
100
|
+
query=config.query,
|
|
101
|
+
fields=search_fields,
|
|
102
|
+
case_sensitive=config.case_sensitive,
|
|
103
|
+
match_mode=config.match_mode,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if not config.query or not config.query.strip():
|
|
107
|
+
return [], []
|
|
108
|
+
|
|
109
|
+
# Determine which fields to search
|
|
110
|
+
fields = config.fields or default_fields or get_searchable_fields(model)
|
|
111
|
+
|
|
112
|
+
if not fields:
|
|
113
|
+
raise NoSearchableFieldsError(model)
|
|
114
|
+
|
|
115
|
+
query = config.query.strip()
|
|
116
|
+
filters: list[ColumnElement[bool]] = []
|
|
117
|
+
joins: list[InstrumentedAttribute[Any]] = []
|
|
118
|
+
added_joins: set[str] = set()
|
|
119
|
+
|
|
120
|
+
for field in fields:
|
|
121
|
+
if isinstance(field, tuple):
|
|
122
|
+
# Relationship: (User.role, Role.name) or deeper
|
|
123
|
+
for rel in field[:-1]:
|
|
124
|
+
rel_key = str(rel)
|
|
125
|
+
if rel_key not in added_joins:
|
|
126
|
+
joins.append(rel)
|
|
127
|
+
added_joins.add(rel_key)
|
|
128
|
+
column = field[-1]
|
|
129
|
+
else:
|
|
130
|
+
column = field
|
|
131
|
+
|
|
132
|
+
# Build the filter
|
|
133
|
+
if config.case_sensitive:
|
|
134
|
+
filters.append(column.like(f"%{query}%"))
|
|
135
|
+
else:
|
|
136
|
+
filters.append(column.ilike(f"%{query}%"))
|
|
137
|
+
|
|
138
|
+
if not filters:
|
|
139
|
+
return [], []
|
|
140
|
+
|
|
141
|
+
# Combine based on match_mode
|
|
142
|
+
if config.match_mode == "any":
|
|
143
|
+
return [or_(*filters)], joins
|
|
144
|
+
else:
|
|
145
|
+
return filters, joins
|
{fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/exceptions/__init__.py
RENAMED
|
@@ -2,6 +2,7 @@ from .exceptions import (
|
|
|
2
2
|
ApiException,
|
|
3
3
|
ConflictError,
|
|
4
4
|
ForbiddenError,
|
|
5
|
+
NoSearchableFieldsError,
|
|
5
6
|
NotFoundError,
|
|
6
7
|
UnauthorizedError,
|
|
7
8
|
generate_error_responses,
|
|
@@ -14,6 +15,7 @@ __all__ = [
|
|
|
14
15
|
"ApiException",
|
|
15
16
|
"ConflictError",
|
|
16
17
|
"ForbiddenError",
|
|
18
|
+
"NoSearchableFieldsError",
|
|
17
19
|
"NotFoundError",
|
|
18
20
|
"UnauthorizedError",
|
|
19
21
|
]
|
{fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/exceptions/exceptions.py
RENAMED
|
@@ -119,6 +119,25 @@ class RoleNotFoundError(NotFoundError):
|
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
class NoSearchableFieldsError(ApiException):
|
|
123
|
+
"""Raised when search is requested but no searchable fields are available."""
|
|
124
|
+
|
|
125
|
+
api_error = ApiError(
|
|
126
|
+
code=400,
|
|
127
|
+
msg="No Searchable Fields",
|
|
128
|
+
desc="No searchable fields configured for this resource.",
|
|
129
|
+
err_code="SEARCH-400",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def __init__(self, model: type) -> None:
|
|
133
|
+
self.model = model
|
|
134
|
+
detail = (
|
|
135
|
+
f"No searchable fields found for model '{model.__name__}'. "
|
|
136
|
+
"Provide 'search_fields' parameter or set 'searchable_fields' on the CRUD class."
|
|
137
|
+
)
|
|
138
|
+
super().__init__(detail)
|
|
139
|
+
|
|
140
|
+
|
|
122
141
|
def generate_error_responses(
|
|
123
142
|
*errors: type[ApiException],
|
|
124
143
|
) -> dict[int | str, dict[str, Any]]:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/cli/commands/__init__.py
RENAMED
|
File without changes
|
{fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/cli/commands/fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-0.3.0 → fastapi_toolsets-0.4.0}/src/fastapi_toolsets/exceptions/handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|