fastapi-basekit 0.3.1__tar.gz → 0.3.3__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.3}/PKG-INFO +1 -1
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/controller/base.py +2 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/repository/base.py +138 -58
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/service/base.py +81 -30
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/controller/base.py +111 -41
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/controller/base.py +3 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlmodel/controller/base.py +3 -0
- fastapi_basekit-0.3.3/fastapi_basekit/exceptions/__init__.py +3 -0
- fastapi_basekit-0.3.3/fastapi_basekit/exceptions/domain.py +57 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit.egg-info/PKG-INFO +1 -1
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit.egg-info/SOURCES.txt +5 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/pyproject.toml +1 -1
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_base_service.py +18 -1
- fastapi_basekit-0.3.3/tests/test_beanie_aggregation_hooks.py +484 -0
- fastapi_basekit-0.3.3/tests/test_beanie_aggregation_integration.py +199 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_controller_auto_permissions.py +56 -71
- fastapi_basekit-0.3.3/tests/test_crud_sqlmodel_repository_service.py +242 -0
- fastapi_basekit-0.3.3/tests/test_sql_queryset_override.py +332 -0
- 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.3}/LICENSE +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/README.md +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/repository/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/service/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/cli/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/cli/main.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/exceptions/handler.py +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/exceptions → fastapi_basekit-0.3.3/fastapi_basekit/schema}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/schema/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/schema/schema.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
- {fastapi_basekit-0.3.1/fastapi_basekit/schema → fastapi_basekit-0.3.3/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.3/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.3/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3}/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.3}/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.3}/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.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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.3/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils}/__init__.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit.egg-info/entry_points.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit.egg-info/requires.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/setup.cfg +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_jwt_service.py +0 -0
- {fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/tests/test_sqlalchemy_base_service_order.py +0 -0
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/beanie/controller/base.py
RENAMED
|
@@ -18,6 +18,7 @@ class BeanieBaseController(BaseController):
|
|
|
18
18
|
|
|
19
19
|
async def list(self):
|
|
20
20
|
"""Lista documentos con paginación usando Beanie."""
|
|
21
|
+
await self.prepare_action("list")
|
|
21
22
|
params = self._params(skip_frames=2)
|
|
22
23
|
items, total = await self.service.list(**params)
|
|
23
24
|
count = params.get("count") or 0
|
|
@@ -39,6 +40,7 @@ class BeanieBaseController(BaseController):
|
|
|
39
40
|
check_fields: Optional[List[str]] = None,
|
|
40
41
|
):
|
|
41
42
|
"""Crea un nuevo documento con validación de campos únicos."""
|
|
43
|
+
await self.prepare_action("create")
|
|
42
44
|
result = await self.service.create(validated_data, check_fields)
|
|
43
45
|
return self.format_response(result, message="Creado exitosamente")
|
|
44
46
|
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/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
|
|
@@ -14,7 +14,11 @@ class BaseController:
|
|
|
14
14
|
|
|
15
15
|
service = Depends()
|
|
16
16
|
schema_class: ClassVar[Type[BaseModel]]
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
# DRF Style: Permisos globales por defecto
|
|
19
|
+
permission_classes: ClassVar[List[Type[BasePermission]]] = []
|
|
20
|
+
|
|
21
|
+
action: Optional[str] = None
|
|
18
22
|
request: Request
|
|
19
23
|
_params_excluded_fields: ClassVar[Set[str]] = {
|
|
20
24
|
"self",
|
|
@@ -32,34 +36,73 @@ class BaseController:
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
def __init__(self) -> None:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
"""Inicializa el controller."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def get_permissions(self) -> List[Type[BasePermission]]:
|
|
43
|
+
"""
|
|
44
|
+
Instancia y retorna la lista de permisos que esta vista requiere.
|
|
45
|
+
|
|
46
|
+
Sobrescribir esto permite lógica tipo DRF:
|
|
47
|
+
|
|
48
|
+
if self.action == 'list':
|
|
49
|
+
return [AllowAny]
|
|
50
|
+
return [IsAuthenticated]
|
|
51
|
+
"""
|
|
52
|
+
return self.permission_classes
|
|
53
|
+
|
|
54
|
+
async def prepare_action(self, action_name: str) -> None:
|
|
55
|
+
"""Set the current action and run permission checks.
|
|
56
|
+
|
|
57
|
+
Auto-called by ``ControllerMeta`` for every public async method on
|
|
58
|
+
the controller. Idempotent within one invocation: if the same
|
|
59
|
+
``action_name`` has already been prepared on this instance,
|
|
60
|
+
subsequent calls are no-ops. This lets custom methods opt into
|
|
61
|
+
calling ``await self.prepare_action(...)`` explicitly without
|
|
62
|
+
double-firing permission checks (when the metaclass already ran
|
|
63
|
+
before entering the method body).
|
|
64
|
+
"""
|
|
65
|
+
if getattr(self, "_basekit_prepared_action", None) == action_name:
|
|
66
|
+
return
|
|
67
|
+
self.action = action_name
|
|
68
|
+
self._basekit_prepared_action = action_name
|
|
69
|
+
await self.check_permissions()
|
|
70
|
+
|
|
71
|
+
async def check_permissions(self):
|
|
72
|
+
"""Run each declared permission. Raises ``PermissionException``
|
|
73
|
+
on the first denial.
|
|
74
|
+
"""
|
|
75
|
+
for permission_class in self.get_permissions():
|
|
76
|
+
permission = permission_class()
|
|
77
|
+
has_perm = await permission.has_permission(self.request)
|
|
78
|
+
if not has_perm:
|
|
79
|
+
message = getattr(
|
|
80
|
+
permission,
|
|
81
|
+
"message_exception",
|
|
82
|
+
"No tienes permiso para realizar esta acción.",
|
|
83
|
+
)
|
|
84
|
+
raise PermissionException(message)
|
|
85
|
+
|
|
86
|
+
async def check_permissions_class(self):
|
|
87
|
+
"""Backward-compat alias for ``check_permissions``.
|
|
88
|
+
|
|
89
|
+
Pre-0.3.2 controllers called this manually inside endpoint methods.
|
|
90
|
+
Keep working for users who haven't migrated to ``permission_classes``
|
|
91
|
+
+ auto-wrapping yet. New code should declare ``permission_classes``
|
|
92
|
+
on the controller and let the metaclass run permissions.
|
|
93
|
+
"""
|
|
94
|
+
await self.check_permissions()
|
|
41
95
|
|
|
42
96
|
def get_schema_class(self) -> Type[BaseModel]:
|
|
43
97
|
assert self.schema_class is not None, (
|
|
44
98
|
"'%s' should either include a `schema_class` attribute, "
|
|
45
|
-
"or override the `
|
|
99
|
+
"or override the `get_schema_class()` method."
|
|
46
100
|
% self.__class__.__name__
|
|
47
101
|
)
|
|
48
102
|
return self.schema_class
|
|
49
103
|
|
|
50
|
-
async def check_permissions_class(self):
|
|
51
|
-
permissions = self.check_permissions()
|
|
52
|
-
if permissions:
|
|
53
|
-
for permission in permissions:
|
|
54
|
-
obj = permission()
|
|
55
|
-
check = await obj.has_permission(self.request)
|
|
56
|
-
if not check:
|
|
57
|
-
raise PermissionException(obj.message_exception)
|
|
58
|
-
|
|
59
|
-
def check_permissions(self) -> List[Type[BasePermission]]:
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
104
|
async def list(self):
|
|
105
|
+
await self.prepare_action("list")
|
|
63
106
|
params = self._params()
|
|
64
107
|
items, total = await self.service.list(**params)
|
|
65
108
|
count = params.get("count") or 0
|
|
@@ -75,18 +118,22 @@ class BaseController:
|
|
|
75
118
|
return self.format_response(data=items, pagination=pagination)
|
|
76
119
|
|
|
77
120
|
async def retrieve(self, id: str):
|
|
121
|
+
await self.prepare_action("retrieve")
|
|
78
122
|
item = await self.service.retrieve(id)
|
|
79
123
|
return self.format_response(data=item)
|
|
80
124
|
|
|
81
125
|
async def create(self, validated_data: Any):
|
|
126
|
+
await self.prepare_action("create")
|
|
82
127
|
result = await self.service.create(validated_data)
|
|
83
128
|
return self.format_response(result, message="Creado exitosamente")
|
|
84
129
|
|
|
85
130
|
async def update(self, id: str, validated_data: Any):
|
|
131
|
+
await self.prepare_action("update")
|
|
86
132
|
result = await self.service.update(id, validated_data)
|
|
87
133
|
return self.format_response(result, message="Actualizado exitosamente")
|
|
88
134
|
|
|
89
135
|
async def delete(self, id: str):
|
|
136
|
+
await self.prepare_action("delete")
|
|
90
137
|
await self.service.delete(id)
|
|
91
138
|
return self.format_response(None, message="Eliminado exitosamente")
|
|
92
139
|
|
|
@@ -99,33 +146,48 @@ class BaseController:
|
|
|
99
146
|
) -> BaseModel:
|
|
100
147
|
schema = self.get_schema_class()
|
|
101
148
|
|
|
149
|
+
# Robust Pydantic v2 validation. Each branch falls back to the
|
|
150
|
+
# raw value when the schema doesn't fit (custom-action endpoints
|
|
151
|
+
# often return ad-hoc dicts that don't match the controller's
|
|
152
|
+
# default schema_class — those should pass through untouched).
|
|
102
153
|
if isinstance(data, list):
|
|
103
154
|
data_dicts = [self.to_dict(item) for item in data]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
data_parsed = schema.model_validate(data_parsed)
|
|
155
|
+
try:
|
|
156
|
+
adapter = TypeAdapter(List[schema])
|
|
157
|
+
data_parsed = adapter.validate_python(data_dicts)
|
|
158
|
+
except Exception:
|
|
159
|
+
data_parsed = data_dicts
|
|
160
|
+
|
|
111
161
|
elif isinstance(data, dict):
|
|
112
|
-
|
|
162
|
+
try:
|
|
163
|
+
data_parsed = schema.model_validate(data)
|
|
164
|
+
except Exception:
|
|
165
|
+
data_parsed = data
|
|
166
|
+
|
|
167
|
+
elif hasattr(data, "__dict__"):
|
|
168
|
+
data_dict = self.to_dict(data)
|
|
169
|
+
try:
|
|
170
|
+
data_parsed = schema.model_validate(data_dict)
|
|
171
|
+
except Exception:
|
|
172
|
+
data_parsed = data_dict
|
|
173
|
+
|
|
174
|
+
elif data is None:
|
|
175
|
+
data_parsed = None
|
|
113
176
|
else:
|
|
114
177
|
data_parsed = data
|
|
115
178
|
|
|
179
|
+
response_cls = BasePaginationResponse if pagination else BaseResponse
|
|
180
|
+
|
|
181
|
+
# Construcción dinámica de argumentos
|
|
182
|
+
kwargs = {
|
|
183
|
+
"data": data_parsed,
|
|
184
|
+
"message": message or "Operación exitosa",
|
|
185
|
+
"status": response_status,
|
|
186
|
+
}
|
|
116
187
|
if pagination:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
message=message or "Operación exitosa",
|
|
121
|
-
status=response_status,
|
|
122
|
-
)
|
|
123
|
-
else:
|
|
124
|
-
return BaseResponse(
|
|
125
|
-
data=data_parsed,
|
|
126
|
-
message=message or "Operación exitosa",
|
|
127
|
-
status=response_status,
|
|
128
|
-
)
|
|
188
|
+
kwargs["pagination"] = pagination
|
|
189
|
+
|
|
190
|
+
return response_cls(**kwargs)
|
|
129
191
|
|
|
130
192
|
def _params(self, skip_frames: int = 1) -> Dict[str, Any]:
|
|
131
193
|
"""
|
|
@@ -208,6 +270,14 @@ class BaseController:
|
|
|
208
270
|
}
|
|
209
271
|
|
|
210
272
|
def to_dict(self, obj: Any):
|
|
211
|
-
|
|
273
|
+
"""Helper para convertir modelos ORM/Pydantic a dict."""
|
|
274
|
+
if hasattr(obj, "model_dump"): # Pydantic v2
|
|
212
275
|
return obj.model_dump()
|
|
276
|
+
if hasattr(obj, "dict"): # Pydantic v1
|
|
277
|
+
return obj.dict()
|
|
278
|
+
if hasattr(obj, "__dict__"): # SQLAlchemy models (basic)
|
|
279
|
+
# Filtramos atributos privados de SQLAlchemy
|
|
280
|
+
return {
|
|
281
|
+
k: v for k, v in obj.__dict__.items() if not k.startswith("_")
|
|
282
|
+
}
|
|
213
283
|
return obj
|
{fastapi_basekit-0.3.1 → fastapi_basekit-0.3.3}/fastapi_basekit/aio/sqlalchemy/controller/base.py
RENAMED
|
@@ -48,6 +48,7 @@ class SQLAlchemyBaseController(BaseController):
|
|
|
48
48
|
joins: Lista de relaciones a hacer JOIN eager loading
|
|
49
49
|
order_by: Expresión de ordenamiento (ej: User.created_at.desc())
|
|
50
50
|
"""
|
|
51
|
+
await self.prepare_action("list")
|
|
51
52
|
params = self._params(skip_frames=2)
|
|
52
53
|
service_params = {
|
|
53
54
|
**params,
|
|
@@ -73,6 +74,7 @@ class SQLAlchemyBaseController(BaseController):
|
|
|
73
74
|
id: ID del registro
|
|
74
75
|
joins: Lista de relaciones a hacer JOIN eager loading
|
|
75
76
|
"""
|
|
77
|
+
await self.prepare_action("retrieve")
|
|
76
78
|
item = await self.service.retrieve(id, joins=joins)
|
|
77
79
|
return self.format_response(data=item)
|
|
78
80
|
|
|
@@ -89,6 +91,7 @@ class SQLAlchemyBaseController(BaseController):
|
|
|
89
91
|
validated_data: Datos validados para crear
|
|
90
92
|
check_fields: Campos a verificar por duplicados antes de crear
|
|
91
93
|
"""
|
|
94
|
+
await self.prepare_action("create")
|
|
92
95
|
result = await self.service.create(validated_data, check_fields)
|
|
93
96
|
return self.format_response(result, message="Creado exitosamente")
|
|
94
97
|
|