fastapi-basekit 0.3.1__tar.gz → 0.3.2__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_basekit-0.3.1 → fastapi_basekit-0.3.2}/PKG-INFO +1 -1
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/base.py +138 -58
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/service/base.py +81 -30
- fastapi_basekit-0.3.2/fastapi_basekit/exceptions/__init__.py +3 -0
- fastapi_basekit-0.3.2/fastapi_basekit/exceptions/domain.py +57 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/PKG-INFO +1 -1
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/SOURCES.txt +1 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/pyproject.toml +1 -1
- fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/LICENSE +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/README.md +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/controller/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/repository/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/cli/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/cli/main.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/exceptions/handler.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/exceptions → fastapi_basekit-0.3.2/fastapi_basekit/schema}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/schema.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/schema → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app}/__init__.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api}/__init__.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1}/__init__.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1 → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories}/__init__.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/entry_points.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/requires.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/setup.cfg +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_base_service.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_controller_auto_permissions.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_jwt_service.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/tests/test_sqlalchemy_base_service_order.py +0 -0
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/base.py
RENAMED
|
@@ -207,27 +207,42 @@ class BaseRepository:
|
|
|
207
207
|
# Apply ordering if provided and not already applied
|
|
208
208
|
if order_by:
|
|
209
209
|
query = query.sort(order_by)
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
total = await query.count()
|
|
212
212
|
items = await query.skip(count * (page - 1)).limit(count).to_list()
|
|
213
213
|
return items, total
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
|
|
215
|
+
def build_list_queryset(
|
|
216
|
+
self,
|
|
217
|
+
search: Optional[str] = None,
|
|
218
|
+
search_fields: Optional[List[str]] = None,
|
|
219
|
+
filters: Optional[dict] = None,
|
|
220
|
+
order_by: Optional[List[tuple]] = None,
|
|
221
|
+
**kwargs,
|
|
222
|
+
) -> FindMany[Document]:
|
|
223
|
+
"""Hook: returns the FindMany query used by `list` endpoints.
|
|
224
|
+
|
|
225
|
+
Beanie equivalent of SQLAlchemy `build_list_queryset`. Override at
|
|
226
|
+
repository OR service level (`BaseService.build_list_queryset`) to
|
|
227
|
+
customize filters, projections, or query options before pagination.
|
|
228
|
+
Default implementation delegates to `build_filter_query`.
|
|
229
|
+
"""
|
|
230
|
+
return self.build_filter_query(
|
|
231
|
+
search=search,
|
|
232
|
+
search_fields=search_fields or [],
|
|
233
|
+
filters=filters or {},
|
|
234
|
+
order_by=order_by,
|
|
235
|
+
**kwargs,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _build_match_stage(
|
|
216
239
|
self,
|
|
217
240
|
search: Optional[str],
|
|
218
|
-
search_fields: List[str],
|
|
219
|
-
filters: dict,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
**kwargs
|
|
224
|
-
) -> tuple[List[Document], int]:
|
|
225
|
-
"""List with support for nested ordering using aggregation ($facet optimized)."""
|
|
226
|
-
field_path, direction, is_nested = self._parse_order_field(order_by)
|
|
227
|
-
pipeline = []
|
|
228
|
-
|
|
229
|
-
# 1. Match stage (same as before)
|
|
230
|
-
match_conditions = {}
|
|
241
|
+
search_fields: Optional[List[str]],
|
|
242
|
+
filters: Optional[dict],
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
"""Build a `$match` stage dict (without the `$match` wrapper)."""
|
|
245
|
+
match_conditions: Dict[str, Any] = {}
|
|
231
246
|
if filters:
|
|
232
247
|
for key, value in filters.items():
|
|
233
248
|
if isinstance(value, ObjectId):
|
|
@@ -236,7 +251,7 @@ class BaseRepository:
|
|
|
236
251
|
match_conditions[f"{key}.$id"] = value.id
|
|
237
252
|
else:
|
|
238
253
|
match_conditions[key] = value
|
|
239
|
-
|
|
254
|
+
|
|
240
255
|
if search and search_fields:
|
|
241
256
|
search_conditions = [
|
|
242
257
|
{field: {"$regex": f".*{search}.*", "$options": "i"}}
|
|
@@ -244,22 +259,47 @@ class BaseRepository:
|
|
|
244
259
|
]
|
|
245
260
|
if search_conditions:
|
|
246
261
|
if match_conditions:
|
|
247
|
-
match_conditions = {
|
|
262
|
+
match_conditions = {
|
|
263
|
+
"$and": [
|
|
264
|
+
match_conditions,
|
|
265
|
+
{"$or": search_conditions},
|
|
266
|
+
]
|
|
267
|
+
}
|
|
248
268
|
else:
|
|
249
269
|
match_conditions = {"$or": search_conditions}
|
|
250
|
-
|
|
270
|
+
return match_conditions
|
|
271
|
+
|
|
272
|
+
def build_list_pipeline(
|
|
273
|
+
self,
|
|
274
|
+
search: Optional[str] = None,
|
|
275
|
+
search_fields: Optional[List[str]] = None,
|
|
276
|
+
filters: Optional[dict] = None,
|
|
277
|
+
order_by: Optional[str] = None,
|
|
278
|
+
**kwargs,
|
|
279
|
+
) -> List[Dict[str, Any]]:
|
|
280
|
+
"""Hook: returns the aggregation pipeline used by `list` endpoints.
|
|
281
|
+
|
|
282
|
+
Beanie equivalent of SQL subqueries / JOINs. Override at repository
|
|
283
|
+
OR service level (`BaseService.build_list_pipeline`) to add `$lookup`,
|
|
284
|
+
`$project`, `$group`, etc. Default builds `$match` + optional `$sort`
|
|
285
|
+
(with auto `$lookup` for nested-link ordering). The `$facet` pagination
|
|
286
|
+
stage is appended later by `paginate_pipeline`.
|
|
287
|
+
"""
|
|
288
|
+
pipeline: List[Dict[str, Any]] = []
|
|
289
|
+
|
|
290
|
+
match_conditions = self._build_match_stage(search, search_fields, filters)
|
|
251
291
|
if match_conditions:
|
|
252
292
|
pipeline.append({"$match": match_conditions})
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
293
|
+
|
|
294
|
+
if not order_by:
|
|
295
|
+
return pipeline
|
|
296
|
+
|
|
297
|
+
field_path, direction, is_nested = self._parse_order_field(order_by)
|
|
298
|
+
|
|
258
299
|
if is_nested:
|
|
259
300
|
parts = field_path.split(".")
|
|
260
301
|
first_field = parts[0]
|
|
261
302
|
collection_name = self._get_collection_name_from_field(first_field)
|
|
262
|
-
|
|
263
303
|
if collection_name:
|
|
264
304
|
pipeline.extend([
|
|
265
305
|
{
|
|
@@ -267,65 +307,105 @@ class BaseRepository:
|
|
|
267
307
|
"from": collection_name,
|
|
268
308
|
"localField": f"{first_field}.$id",
|
|
269
309
|
"foreignField": "_id",
|
|
270
|
-
"as": f"{first_field}_data"
|
|
310
|
+
"as": f"{first_field}_data",
|
|
271
311
|
}
|
|
272
312
|
},
|
|
273
313
|
{
|
|
274
314
|
"$unwind": {
|
|
275
315
|
"path": f"${first_field}_data",
|
|
276
|
-
"preserveNullAndEmptyArrays": True
|
|
316
|
+
"preserveNullAndEmptyArrays": True,
|
|
277
317
|
}
|
|
278
|
-
}
|
|
318
|
+
},
|
|
279
319
|
])
|
|
280
320
|
remaining_path = ".".join(parts[1:]) if len(parts) > 1 else ""
|
|
281
|
-
sort_field =
|
|
321
|
+
sort_field = (
|
|
322
|
+
f"{first_field}_data.{remaining_path}"
|
|
323
|
+
if remaining_path
|
|
324
|
+
else f"{first_field}_data"
|
|
325
|
+
)
|
|
282
326
|
else:
|
|
283
327
|
sort_field = field_path
|
|
284
328
|
else:
|
|
285
329
|
sort_field = field_path
|
|
286
|
-
|
|
330
|
+
|
|
287
331
|
pipeline.append({"$sort": {sort_field: direction}})
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
332
|
+
return pipeline
|
|
333
|
+
|
|
334
|
+
async def paginate_pipeline(
|
|
335
|
+
self,
|
|
336
|
+
pipeline: List[Dict[str, Any]],
|
|
337
|
+
page: int,
|
|
338
|
+
count: int,
|
|
339
|
+
validate: bool = True,
|
|
340
|
+
) -> tuple[List[Any], int]:
|
|
341
|
+
"""Run an aggregation pipeline with `$facet` pagination.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
pipeline: Pipeline stages (without the final `$facet`).
|
|
345
|
+
page, count: Pagination params.
|
|
346
|
+
validate: If True, validates each row against `self.model`.
|
|
347
|
+
Set False when the pipeline projects a non-model shape (e.g.
|
|
348
|
+
joined columns) — the raw dicts are returned untouched.
|
|
349
|
+
"""
|
|
350
|
+
full_pipeline = list(pipeline) + [
|
|
351
|
+
{
|
|
352
|
+
"$facet": {
|
|
353
|
+
"metadata": [{"$count": "total"}],
|
|
354
|
+
"data": [
|
|
355
|
+
{"$skip": count * (page - 1)},
|
|
356
|
+
{"$limit": count},
|
|
357
|
+
],
|
|
358
|
+
}
|
|
299
359
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if project_exclusion:
|
|
304
|
-
facet_stage["$facet"]["data"].append({"$project": project_exclusion})
|
|
305
|
-
|
|
306
|
-
pipeline.append(facet_stage)
|
|
307
|
-
|
|
308
|
-
# Execute Pipeline
|
|
309
|
-
results = await self.model.aggregate(pipeline).to_list()
|
|
310
|
-
|
|
311
|
-
# Process Results
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
results = await self.model.aggregate(full_pipeline).to_list()
|
|
312
363
|
if not results or not results[0].get("metadata"):
|
|
313
364
|
return [], 0
|
|
314
|
-
|
|
365
|
+
|
|
315
366
|
data = results[0]
|
|
316
367
|
total = data["metadata"][0]["total"] if data["metadata"] else 0
|
|
317
368
|
items_raw = data["data"]
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
369
|
+
|
|
370
|
+
if not validate:
|
|
371
|
+
return items_raw, total
|
|
372
|
+
|
|
373
|
+
items: List[Any] = []
|
|
321
374
|
for raw_item in items_raw:
|
|
322
375
|
try:
|
|
323
376
|
items.append(self.model.model_validate(raw_item))
|
|
324
377
|
except Exception:
|
|
325
378
|
continue
|
|
326
|
-
|
|
327
379
|
return items, total
|
|
328
380
|
|
|
381
|
+
async def list_with_aggregation(
|
|
382
|
+
self,
|
|
383
|
+
search: Optional[str],
|
|
384
|
+
search_fields: List[str],
|
|
385
|
+
filters: dict,
|
|
386
|
+
order_by: str,
|
|
387
|
+
page: int,
|
|
388
|
+
count: int,
|
|
389
|
+
**kwargs,
|
|
390
|
+
) -> tuple[List[Document], int]:
|
|
391
|
+
"""Backward-compat wrapper: delegates to `build_list_pipeline` +
|
|
392
|
+
`paginate_pipeline`. Kept for callers that hit it directly.
|
|
393
|
+
"""
|
|
394
|
+
pipeline = self.build_list_pipeline(
|
|
395
|
+
search=search,
|
|
396
|
+
search_fields=search_fields,
|
|
397
|
+
filters=filters,
|
|
398
|
+
order_by=order_by,
|
|
399
|
+
**kwargs,
|
|
400
|
+
)
|
|
401
|
+
# Strip join artifacts when default sort-lookup added them
|
|
402
|
+
field_path, _, is_nested = self._parse_order_field(order_by) if order_by else ("", 0, False)
|
|
403
|
+
if is_nested:
|
|
404
|
+
first_field = field_path.split(".")[0]
|
|
405
|
+
if self._get_collection_name_from_field(first_field):
|
|
406
|
+
pipeline.append({"$project": {f"{first_field}_data": 0}})
|
|
407
|
+
return await self.paginate_pipeline(pipeline, page, count, validate=True)
|
|
408
|
+
|
|
329
409
|
async def get_by_id(
|
|
330
410
|
self,
|
|
331
411
|
obj_id: Union[str, ObjectId],
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Optional, Union
|
|
2
2
|
|
|
3
|
+
from beanie import Document
|
|
4
|
+
from beanie.odm.queries.find import FindMany
|
|
3
5
|
from fastapi import Request
|
|
4
6
|
from pydantic import BaseModel
|
|
5
7
|
|
|
@@ -20,6 +22,8 @@ class BaseService:
|
|
|
20
22
|
kwargs_query: Dict[str, Union[str, int]] = {}
|
|
21
23
|
action: str = ""
|
|
22
24
|
order_by: Optional[List[tuple]] = None
|
|
25
|
+
use_aggregation: bool = False
|
|
26
|
+
aggregation_validate: bool = True
|
|
23
27
|
|
|
24
28
|
def __init__(
|
|
25
29
|
self, repository: BaseRepository, request: Optional[Request] = None
|
|
@@ -69,61 +73,108 @@ class BaseService:
|
|
|
69
73
|
raise NotFoundException(f"id={id} no encontrado")
|
|
70
74
|
return obj
|
|
71
75
|
|
|
76
|
+
def build_list_queryset(
|
|
77
|
+
self,
|
|
78
|
+
search: Optional[str] = None,
|
|
79
|
+
search_fields: Optional[List[str]] = None,
|
|
80
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
81
|
+
order_by: Optional[List[tuple]] = None,
|
|
82
|
+
**kwargs,
|
|
83
|
+
) -> FindMany[Document]:
|
|
84
|
+
"""Service-level hook over `repository.build_list_queryset`.
|
|
85
|
+
|
|
86
|
+
Override here to compose query options across repositories or to
|
|
87
|
+
decorate the FindMany before pagination.
|
|
88
|
+
"""
|
|
89
|
+
return self.repository.build_list_queryset(
|
|
90
|
+
search=search,
|
|
91
|
+
search_fields=search_fields or self.search_fields,
|
|
92
|
+
filters=filters,
|
|
93
|
+
order_by=order_by,
|
|
94
|
+
**kwargs,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def build_list_pipeline(
|
|
98
|
+
self,
|
|
99
|
+
search: Optional[str] = None,
|
|
100
|
+
search_fields: Optional[List[str]] = None,
|
|
101
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
102
|
+
order_by: Optional[str] = None,
|
|
103
|
+
**kwargs,
|
|
104
|
+
) -> List[Dict[str, Any]]:
|
|
105
|
+
"""Service-level hook over `repository.build_list_pipeline`.
|
|
106
|
+
|
|
107
|
+
Override here to add `$lookup`, `$project`, `$group`, etc. for
|
|
108
|
+
cross-collection joins (subquery-like). Set `use_aggregation = True`
|
|
109
|
+
on the service to force the pipeline path even without nested order.
|
|
110
|
+
Set `aggregation_validate = False` if the projection produces a
|
|
111
|
+
non-model shape (joined columns / flattened rows).
|
|
112
|
+
"""
|
|
113
|
+
return self.repository.build_list_pipeline(
|
|
114
|
+
search=search,
|
|
115
|
+
search_fields=search_fields or self.search_fields,
|
|
116
|
+
filters=filters,
|
|
117
|
+
order_by=order_by,
|
|
118
|
+
**kwargs,
|
|
119
|
+
)
|
|
120
|
+
|
|
72
121
|
async def list(
|
|
73
122
|
self,
|
|
74
123
|
search: Optional[str] = None,
|
|
75
124
|
page: int = 1,
|
|
76
125
|
count: int = 25,
|
|
77
126
|
filters: Optional[Dict[str, Any]] = None,
|
|
78
|
-
order_by: Optional[str] = None, #
|
|
127
|
+
order_by: Optional[str] = None, # Dynamic ordering (e.g., "-created_at" or "tool__name")
|
|
79
128
|
):
|
|
80
129
|
kwargs = self.get_kwargs_query()
|
|
81
130
|
applied_filters = self.get_filters(filters)
|
|
82
|
-
|
|
83
|
-
#
|
|
131
|
+
|
|
132
|
+
# Resolve ordering
|
|
84
133
|
if order_by:
|
|
85
|
-
# Dynamic ordering from parameter (takes precedence)
|
|
86
134
|
order_str = order_by
|
|
87
135
|
else:
|
|
88
|
-
# Fall back to service-level ordering
|
|
89
136
|
default_order = self.get_order()
|
|
90
137
|
if default_order:
|
|
91
|
-
# Convert list of tuples to string format
|
|
92
|
-
# e.g., [("created_at", -1)] -> "-created_at"
|
|
93
138
|
field, direction = default_order[0]
|
|
94
139
|
order_str = f"{'-' if direction == -1 else ''}{field}"
|
|
95
140
|
else:
|
|
96
141
|
order_str = None
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
|
|
143
|
+
nested_order = bool(order_str and ("__" in order_str or "." in order_str))
|
|
144
|
+
use_pipeline = self.use_aggregation or nested_order
|
|
145
|
+
|
|
146
|
+
if use_pipeline:
|
|
147
|
+
pipeline = self.build_list_pipeline(
|
|
102
148
|
search=search,
|
|
103
149
|
search_fields=self.search_fields,
|
|
104
150
|
filters=applied_filters,
|
|
105
151
|
order_by=order_str,
|
|
106
|
-
page=page,
|
|
107
|
-
count=count,
|
|
108
152
|
**kwargs,
|
|
109
153
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
direction = -1 if order_str.startswith("-") else 1
|
|
116
|
-
field = order_str.lstrip("-")
|
|
117
|
-
order_list = [(field, direction)]
|
|
118
|
-
|
|
119
|
-
query = self.repository.build_filter_query(
|
|
120
|
-
search=search,
|
|
121
|
-
search_fields=self.search_fields,
|
|
122
|
-
filters=applied_filters,
|
|
123
|
-
order_by=order_list,
|
|
124
|
-
**kwargs,
|
|
154
|
+
return await self.repository.paginate_pipeline(
|
|
155
|
+
pipeline,
|
|
156
|
+
page=page,
|
|
157
|
+
count=count,
|
|
158
|
+
validate=self.aggregation_validate,
|
|
125
159
|
)
|
|
126
|
-
|
|
160
|
+
|
|
161
|
+
# FindMany path
|
|
162
|
+
order_list = None
|
|
163
|
+
if order_str:
|
|
164
|
+
direction = -1 if order_str.startswith("-") else 1
|
|
165
|
+
field = order_str.lstrip("-")
|
|
166
|
+
order_list = [(field, direction)]
|
|
167
|
+
|
|
168
|
+
query = self.build_list_queryset(
|
|
169
|
+
search=search,
|
|
170
|
+
search_fields=self.search_fields,
|
|
171
|
+
filters=applied_filters,
|
|
172
|
+
order_by=order_list,
|
|
173
|
+
**kwargs,
|
|
174
|
+
)
|
|
175
|
+
return await self.repository.paginate(
|
|
176
|
+
query, page, count, order_by=order_list
|
|
177
|
+
)
|
|
127
178
|
|
|
128
179
|
async def create(
|
|
129
180
|
self, payload: BaseModel, check_fields: Optional[List[str]] = None
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Generic domain-error base class with declarative HTTP mapping.
|
|
2
|
+
|
|
3
|
+
Services raise `DomainError` (or a subclass) with a stable string `code` and
|
|
4
|
+
a human `message`. Endpoints catch and re-raise as HTTPException via
|
|
5
|
+
`exc.to_http()`. The status code per `code` is declared on the subclass via
|
|
6
|
+
`STATUS_CODE_MAP`, so each domain owns its mapping in one place — no inline
|
|
7
|
+
`if/elif` translator helpers in endpoint files.
|
|
8
|
+
|
|
9
|
+
Pattern:
|
|
10
|
+
|
|
11
|
+
class MyDomainError(DomainError):
|
|
12
|
+
STATUS_CODE_MAP = {
|
|
13
|
+
"unauthorized": status.HTTP_401_UNAUTHORIZED,
|
|
14
|
+
"not_found": status.HTTP_404_NOT_FOUND,
|
|
15
|
+
"conflict": status.HTTP_409_CONFLICT,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Service:
|
|
19
|
+
raise MyDomainError("not_found", "Recurso no encontrado")
|
|
20
|
+
|
|
21
|
+
# Endpoint:
|
|
22
|
+
try:
|
|
23
|
+
await service.do_thing()
|
|
24
|
+
except MyDomainError as exc:
|
|
25
|
+
raise exc.to_http()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from typing import ClassVar
|
|
29
|
+
|
|
30
|
+
from fastapi import HTTPException, status
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DomainError(Exception):
|
|
34
|
+
"""Base for service-layer domain errors with HTTP mapping baked in.
|
|
35
|
+
|
|
36
|
+
Subclasses override `STATUS_CODE_MAP` to map their codes to HTTP
|
|
37
|
+
statuses. Codes not present fall back to `DEFAULT_STATUS`.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
DEFAULT_STATUS: ClassVar[int] = status.HTTP_400_BAD_REQUEST
|
|
41
|
+
STATUS_CODE_MAP: ClassVar[dict[str, int]] = {}
|
|
42
|
+
|
|
43
|
+
def __init__(self, code: str, message: str):
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.code = code
|
|
46
|
+
self.message = message
|
|
47
|
+
|
|
48
|
+
def http_status(self) -> int:
|
|
49
|
+
return self.STATUS_CODE_MAP.get(self.code, self.DEFAULT_STATUS)
|
|
50
|
+
|
|
51
|
+
def to_http(self) -> HTTPException:
|
|
52
|
+
return HTTPException(
|
|
53
|
+
status_code=self.http_status(), detail=self.message
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def __str__(self) -> str:
|
|
57
|
+
return f"{type(self).__name__}[{self.code}]: {self.message}"
|
|
@@ -39,6 +39,7 @@ fastapi_basekit/cli/__init__.py
|
|
|
39
39
|
fastapi_basekit/cli/main.py
|
|
40
40
|
fastapi_basekit/exceptions/__init__.py
|
|
41
41
|
fastapi_basekit/exceptions/api_exceptions.py
|
|
42
|
+
fastapi_basekit/exceptions/domain.py
|
|
42
43
|
fastapi_basekit/exceptions/handler.py
|
|
43
44
|
fastapi_basekit/schema/__init__.py
|
|
44
45
|
fastapi_basekit/schema/base.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-basekit"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Utilities and base classes for FastAPI async projects (Beanie, SQLAlchemy or SQLModel)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/service/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/controller/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/repository/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/exceptions/api_exceptions.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
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/cookiecutter.json
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/dependency_links.txt
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|