fastapi-basekit 0.4.3__tar.gz → 0.5.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_basekit-0.4.3 → fastapi_basekit-0.5.0}/PKG-INFO +3 -3
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/controller/base.py +1 -1
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/repository/base.py +63 -12
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/service/base.py +60 -26
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/controller/base.py +175 -69
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py +1 -1
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/repository/base.py +79 -16
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/service/base.py +48 -10
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/repository/base.py +105 -22
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/service/base.py +36 -11
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/handler.py +15 -0
- fastapi_basekit-0.5.0/fastapi_basekit/py.typed +1 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/thrid/jwt.py +42 -7
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/PKG-INFO +3 -3
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/SOURCES.txt +21 -1
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/requires.txt +2 -2
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/pyproject.toml +4 -3
- fastapi_basekit-0.5.0/tests/test_beanie_repository_regression.py +261 -0
- fastapi_basekit-0.5.0/tests/test_beanie_service_regression.py +244 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_exception_handlers.py +30 -0
- fastapi_basekit-0.5.0/tests/test_generics_optional_and_cache.py +99 -0
- fastapi_basekit-0.5.0/tests/test_generics_wiring.py +100 -0
- fastapi_basekit-0.5.0/tests/test_jwt_config_robustness.py +61 -0
- fastapi_basekit-0.5.0/tests/test_params_classification.py +193 -0
- fastapi_basekit-0.5.0/tests/test_params_coercion_matrix.py +110 -0
- fastapi_basekit-0.5.0/tests/test_params_coercion_unit.py +165 -0
- fastapi_basekit-0.5.0/tests/test_params_http_matrix_beanie.py +109 -0
- fastapi_basekit-0.5.0/tests/test_params_http_matrix_sqlalchemy.py +114 -0
- fastapi_basekit-0.5.0/tests/test_params_rich_type_coercion.py +79 -0
- fastapi_basekit-0.5.0/tests/test_path_filter_mixin_compat.py +179 -0
- fastapi_basekit-0.5.0/tests/test_post_process_list_hook.py +154 -0
- fastapi_basekit-0.5.0/tests/test_pulbot_cbv_pattern_beanie.py +146 -0
- fastapi_basekit-0.5.0/tests/test_repo_signature_aliases.py +74 -0
- fastapi_basekit-0.5.0/tests/test_sqlalchemy_repository_regression.py +173 -0
- fastapi_basekit-0.5.0/tests/test_sqlalchemy_service_regression.py +154 -0
- fastapi_basekit-0.5.0/tests/test_sqlmodel_filter_operators.py +92 -0
- fastapi_basekit-0.5.0/tests/test_typed_filter_coercion.py +114 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/LICENSE +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/README.md +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/cli/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/cli/main.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/domain.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/openapi.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/base.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/schema.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/entry_points.txt +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/setup.cfg +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_action_param_leak.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_base_service.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_aggregation_hooks.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_aggregation_integration.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_keyset_pagination.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_service_mutable_defaults.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_controller_auto_permissions.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_crud_sqlmodel_repository_service.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_filter_operators.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_jwt_service.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_openapi_simplify.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_sql_queryset_override.py +0 -0
- {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_sqlalchemy_base_service_order.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-basekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Utilities and base classes for FastAPI async projects (Beanie, SQLAlchemy or SQLModel)
|
|
5
5
|
Author-email: Jerson Moreno <jerson.ml820@hotmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -23,7 +23,7 @@ Provides-Extra: beanie
|
|
|
23
23
|
Requires-Dist: beanie<3,>=2.0; extra == "beanie"
|
|
24
24
|
Provides-Extra: sqlalchemy
|
|
25
25
|
Requires-Dist: SQLAlchemy[asyncio]<3,>=2.0.30; extra == "sqlalchemy"
|
|
26
|
-
Requires-Dist: psycopg2>=2.9.0; extra == "sqlalchemy"
|
|
26
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "sqlalchemy"
|
|
27
27
|
Provides-Extra: sqlmodel
|
|
28
28
|
Requires-Dist: sqlmodel>=0.0.37; extra == "sqlmodel"
|
|
29
29
|
Provides-Extra: init
|
|
@@ -37,7 +37,7 @@ Requires-Dist: mike>=2.1.0; extra == "docs"
|
|
|
37
37
|
Provides-Extra: all
|
|
38
38
|
Requires-Dist: beanie<3,>=2.0; extra == "all"
|
|
39
39
|
Requires-Dist: SQLAlchemy[asyncio]<3,>=2.0.30; extra == "all"
|
|
40
|
-
Requires-Dist: psycopg2>=2.9.0; extra == "all"
|
|
40
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
41
41
|
Requires-Dist: sqlmodel>=0.0.37; extra == "all"
|
|
42
42
|
Requires-Dist: cookiecutter>=2.5.0; extra == "all"
|
|
43
43
|
Requires-Dist: mkdocs>=1.6.0; extra == "all"
|
{fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/controller/base.py
RENAMED
|
@@ -19,7 +19,7 @@ class BeanieBaseController(BaseController):
|
|
|
19
19
|
async def list(self):
|
|
20
20
|
"""Lista documentos con paginación usando Beanie."""
|
|
21
21
|
await self.prepare_action("list")
|
|
22
|
-
params = self._params(
|
|
22
|
+
params = self._params()
|
|
23
23
|
items, total = await self.service.list(**params)
|
|
24
24
|
count = params.get("count") or 0
|
|
25
25
|
page = params.get("page") or 1
|
{fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/repository/base.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Any, Dict, List, Optional, Type, Union
|
|
2
|
+
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
|
|
3
3
|
from typing import get_args, get_origin
|
|
4
4
|
import re
|
|
5
5
|
|
|
@@ -11,10 +11,31 @@ from beanie.operators import Or, RegEx
|
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
+
#: Tipo del Document Beanie que gestiona el repositorio. Parametrízalo al
|
|
15
|
+
#: declarar la subclase — ``class UserRepository(BaseRepository[User])`` — para
|
|
16
|
+
#: que ``get_by_id``/``create``/``update``/``list_all`` devuelvan el Document
|
|
17
|
+
#: tipado (autocompletado + mypy para consumidores e IAs).
|
|
18
|
+
ModelT = TypeVar("ModelT", bound=Document)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseRepository(Generic[ModelT]):
|
|
22
|
+
"""Repositorio base para Beanie ODM (MongoDB), parametrizado por el modelo.
|
|
23
|
+
|
|
24
|
+
OJO — las firmas difieren del repo SQLAlchemy (misma clase, distinto ORM):
|
|
25
|
+
- get por id: `get_by_id(id)` (SQL usa `get(id)`)
|
|
26
|
+
- update: `update(obj, data)` — el 1er arg es el **Document**, no el
|
|
27
|
+
id (SQL usa `update(id, dict)`). Fetch primero con
|
|
28
|
+
`get_by_id`, luego `update(obj, data)`.
|
|
29
|
+
- delete: `delete(obj)` — recibe el Document (SQL usa `delete(id)`).
|
|
30
|
+
- create: `create(obj | dict)`.
|
|
31
|
+
Listado: `build_list_queryset(...)`→FindMany + `paginate(...)`, o
|
|
32
|
+
`build_list_pipeline(...)` + `paginate_pipeline(...)` para joins (`$lookup`).
|
|
33
|
+
Preferí llamar el SERVICE (API unificada) en vez del repo directo; llamar
|
|
34
|
+
`get_by_id` directo bypassea el scoping de `get_filters()`.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
model: Type[ModelT]
|
|
14
38
|
|
|
15
|
-
class BaseRepository:
|
|
16
|
-
model: Type[Document]
|
|
17
|
-
|
|
18
39
|
def _parse_order_field(self, order_by: str) -> tuple[str, int, bool]:
|
|
19
40
|
"""Parse order_by string into components.
|
|
20
41
|
|
|
@@ -207,6 +228,17 @@ class BaseRepository:
|
|
|
207
228
|
async def paginate(
|
|
208
229
|
self, query: FindMany[Document], page: int, count: int, order_by: Optional[List[tuple]] = None
|
|
209
230
|
) -> tuple[List[Document], int]:
|
|
231
|
+
"""MOTOR DE PAGINACIÓN (FindMany) — NO LO REIMPLEMENTES.
|
|
232
|
+
|
|
233
|
+
Único loop de paginación offset (count + skip/limit). Para personalizar
|
|
234
|
+
el listado, override el HOOK correcto, no copies skip/limit/count:
|
|
235
|
+
- query/filtros/scoping/orden → `build_list_queryset` (o `build_filter_query`)
|
|
236
|
+
- join `$lookup` / shape plano → `use_aggregation=True` + `build_list_pipeline`
|
|
237
|
+
- filtros por usuario/acción → `Service.get_filters`
|
|
238
|
+
- orden por defecto → `Service.get_order`
|
|
239
|
+
- enriquecer items de la página→ `Service.post_process_list`
|
|
240
|
+
- scroll infinito / cursor → `paginate_keyset` (método+endpoint aparte)
|
|
241
|
+
"""
|
|
210
242
|
# Apply ordering if provided and not already applied
|
|
211
243
|
if order_by:
|
|
212
244
|
query = query.sort(order_by)
|
|
@@ -379,7 +411,13 @@ class BaseRepository:
|
|
|
379
411
|
count: int,
|
|
380
412
|
validate: bool = True,
|
|
381
413
|
) -> tuple[List[Any], int]:
|
|
382
|
-
"""
|
|
414
|
+
"""MOTOR DE PAGINACIÓN (aggregation `$facet`) — NO LO REIMPLEMENTES.
|
|
415
|
+
|
|
416
|
+
Ejecuta el pipeline con paginación `$facet`. Para un listado agregado
|
|
417
|
+
NO copies este `$facet`/skip/limit: override `Service.build_list_pipeline`
|
|
418
|
+
(agrega `$lookup`/`$project`/`$group`) y activa `use_aggregation=True`;
|
|
419
|
+
este método le pone la paginación. Si la proyección no es el modelo,
|
|
420
|
+
`aggregation_validate=False`.
|
|
383
421
|
|
|
384
422
|
Args:
|
|
385
423
|
pipeline: Pipeline stages (without the final `$facet`).
|
|
@@ -471,7 +509,7 @@ class BaseRepository:
|
|
|
471
509
|
self,
|
|
472
510
|
obj_id: Union[str, ObjectId],
|
|
473
511
|
**kwargs,
|
|
474
|
-
) -> Optional[
|
|
512
|
+
) -> Optional[ModelT]:
|
|
475
513
|
if not isinstance(obj_id, ObjectId):
|
|
476
514
|
obj_id = ObjectId(obj_id)
|
|
477
515
|
return await self.model.find_one(
|
|
@@ -479,12 +517,19 @@ class BaseRepository:
|
|
|
479
517
|
**self._get_query_kwargs(**kwargs),
|
|
480
518
|
)
|
|
481
519
|
|
|
520
|
+
async def get(
|
|
521
|
+
self, obj_id: Union[str, ObjectId], **kwargs
|
|
522
|
+
) -> Optional[ModelT]:
|
|
523
|
+
"""Alias de `get_by_id(id)` — nombre unificado con los repos SQL
|
|
524
|
+
(`get`) para que las lecturas por id sean portables entre ORMs."""
|
|
525
|
+
return await self.get_by_id(obj_id, **kwargs)
|
|
526
|
+
|
|
482
527
|
async def get_by_field(
|
|
483
528
|
self,
|
|
484
529
|
field_name: str,
|
|
485
530
|
value: Any,
|
|
486
531
|
**kwargs,
|
|
487
|
-
) -> Optional[
|
|
532
|
+
) -> Optional[ModelT]:
|
|
488
533
|
if not hasattr(self.model, field_name):
|
|
489
534
|
raise AttributeError(
|
|
490
535
|
f"{self.model.__name__} no tiene el campo '{field_name}'"
|
|
@@ -498,7 +543,7 @@ class BaseRepository:
|
|
|
498
543
|
self,
|
|
499
544
|
filters: Dict[str, Any],
|
|
500
545
|
**kwargs,
|
|
501
|
-
) -> Optional[
|
|
546
|
+
) -> Optional[ModelT]:
|
|
502
547
|
exprs = [
|
|
503
548
|
getattr(self.model, f) == v
|
|
504
549
|
for f, v in filters.items()
|
|
@@ -513,21 +558,27 @@ class BaseRepository:
|
|
|
513
558
|
async def list_all(
|
|
514
559
|
self,
|
|
515
560
|
**kwargs,
|
|
516
|
-
) -> List[
|
|
561
|
+
) -> List[ModelT]:
|
|
517
562
|
query = self.model.find_all(**self._get_query_kwargs(**kwargs))
|
|
518
563
|
return await query.to_list()
|
|
519
564
|
|
|
520
|
-
async def create(self, obj: Union[
|
|
565
|
+
async def create(self, obj: Union[ModelT, Dict[str, Any]]) -> ModelT:
|
|
521
566
|
if isinstance(obj, dict):
|
|
522
567
|
obj = self.model(**obj)
|
|
523
568
|
await obj.insert()
|
|
524
569
|
return obj
|
|
525
570
|
|
|
526
|
-
async def update(self, obj:
|
|
571
|
+
async def update(self, obj: ModelT, data: Dict[str, Any]) -> ModelT:
|
|
572
|
+
"""Actualiza un Document Beanie ya cargado.
|
|
573
|
+
|
|
574
|
+
Divergencia con SQL: acá el 1er arg es el **Document** (no el id) y SÍ
|
|
575
|
+
setea ``None``. El repo SQLAlchemy usa ``update(id, dict)`` y omite
|
|
576
|
+
``None``. Ver la tabla en el CLAUDE.md de la lib.
|
|
577
|
+
"""
|
|
527
578
|
for key, value in data.items():
|
|
528
579
|
setattr(obj, key, value)
|
|
529
580
|
await obj.save()
|
|
530
581
|
return obj
|
|
531
582
|
|
|
532
|
-
async def delete(self, obj:
|
|
583
|
+
async def delete(self, obj: ModelT) -> None:
|
|
533
584
|
await obj.delete()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Optional, Union
|
|
1
|
+
from typing import Any, Dict, Generic, List, Optional, Tuple, Union
|
|
2
2
|
|
|
3
3
|
from beanie import Document
|
|
4
4
|
from beanie.odm.queries.find import FindMany
|
|
@@ -6,17 +6,25 @@ from fastapi import Request
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
from ...beanie.repository.base import BaseRepository
|
|
9
|
+
from ...beanie.repository.base import BaseRepository, ModelT
|
|
10
10
|
from ....exceptions.api_exceptions import (
|
|
11
11
|
NotFoundException,
|
|
12
12
|
DatabaseIntegrityException,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class BaseService:
|
|
17
|
-
"""Servicio base específico para Beanie ODM (async).
|
|
16
|
+
class BaseService(Generic[ModelT]):
|
|
17
|
+
"""Servicio base específico para Beanie ODM (async), parametrizado.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Declara el modelo vía el genérico para tipar el CRUD::
|
|
20
|
+
|
|
21
|
+
class UserService(BaseService[User]):
|
|
22
|
+
repository: UserRepository
|
|
23
|
+
|
|
24
|
+
Así ``retrieve``/``create``/``update`` devuelven ``User``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
repository: BaseRepository[ModelT]
|
|
20
28
|
search_fields: List[str] = []
|
|
21
29
|
duplicate_check_fields: List[str] = []
|
|
22
30
|
kwargs_query: Dict[str, Union[str, int]] = {}
|
|
@@ -74,7 +82,7 @@ class BaseService:
|
|
|
74
82
|
filters = filters or {}
|
|
75
83
|
return filters
|
|
76
84
|
|
|
77
|
-
async def retrieve(self, id: str):
|
|
85
|
+
async def retrieve(self, id: str) -> ModelT:
|
|
78
86
|
kwargs = self.get_kwargs_query()
|
|
79
87
|
obj = await self.repository.get_by_id(id, **kwargs)
|
|
80
88
|
if not obj:
|
|
@@ -133,7 +141,7 @@ class BaseService:
|
|
|
133
141
|
count: int = 25,
|
|
134
142
|
filters: Optional[Dict[str, Any]] = None,
|
|
135
143
|
order_by: Optional[str] = None, # Dynamic ordering (e.g., "-created_at" or "tool__name")
|
|
136
|
-
):
|
|
144
|
+
) -> Tuple[List[ModelT], int]:
|
|
137
145
|
kwargs = self.get_kwargs_query()
|
|
138
146
|
applied_filters = self.get_filters(filters)
|
|
139
147
|
|
|
@@ -159,34 +167,60 @@ class BaseService:
|
|
|
159
167
|
order_by=order_str,
|
|
160
168
|
**kwargs,
|
|
161
169
|
)
|
|
162
|
-
|
|
170
|
+
items, total = await self.repository.paginate_pipeline(
|
|
163
171
|
pipeline,
|
|
164
172
|
page=page,
|
|
165
173
|
count=count,
|
|
166
174
|
validate=self.aggregation_validate,
|
|
167
175
|
)
|
|
176
|
+
else:
|
|
177
|
+
# FindMany path
|
|
178
|
+
order_list = None
|
|
179
|
+
if order_str:
|
|
180
|
+
direction = -1 if order_str.startswith("-") else 1
|
|
181
|
+
field = order_str.lstrip("-")
|
|
182
|
+
order_list = [(field, direction)]
|
|
168
183
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
184
|
+
query = self.build_list_queryset(
|
|
185
|
+
search=search,
|
|
186
|
+
search_fields=self.search_fields,
|
|
187
|
+
filters=applied_filters,
|
|
188
|
+
order_by=order_list,
|
|
189
|
+
**kwargs,
|
|
190
|
+
)
|
|
191
|
+
items, total = await self.repository.paginate(
|
|
192
|
+
query, page, count, order_by=order_list
|
|
193
|
+
)
|
|
175
194
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
195
|
+
items = await self.post_process_list(items)
|
|
196
|
+
return items, total
|
|
197
|
+
|
|
198
|
+
async def post_process_list(self, items: List[ModelT]) -> List[ModelT]:
|
|
199
|
+
"""Hook: transforma/enriquece los items DE UNA PÁGINA ya paginada.
|
|
200
|
+
|
|
201
|
+
Este es el método para "hacer algo custom con los resultados" SIN
|
|
202
|
+
reescribir la paginación ni overridear `list()`. Corre después de
|
|
203
|
+
`paginate`/`paginate_pipeline`, sobre los items de la página actual
|
|
204
|
+
(no toda la colección), así que es seguro para un enrich con un `await`
|
|
205
|
+
extra por página (agrega un contador, resuelve un campo derivado, etc.).
|
|
206
|
+
|
|
207
|
+
Default: devuelve los items sin cambios. Ejemplo::
|
|
208
|
+
|
|
209
|
+
async def post_process_list(self, items):
|
|
210
|
+
ids = [c.id for c in items]
|
|
211
|
+
counts = await self.conv_repo.count_by_customer_ids(ids)
|
|
212
|
+
for c in items:
|
|
213
|
+
c.conversation_count = counts.get(c.id, 0)
|
|
214
|
+
return items
|
|
215
|
+
|
|
216
|
+
NO cambies el `total` acá (es el de la query completa). NO filtres items
|
|
217
|
+
(eso va en `get_filters`/`build_list_*`, o el total quedaría mal).
|
|
218
|
+
"""
|
|
219
|
+
return items
|
|
186
220
|
|
|
187
221
|
async def create(
|
|
188
222
|
self, payload: BaseModel, check_fields: Optional[List[str]] = None
|
|
189
|
-
) ->
|
|
223
|
+
) -> ModelT:
|
|
190
224
|
data = (
|
|
191
225
|
payload.model_dump() if not isinstance(payload, dict) else payload
|
|
192
226
|
)
|
|
@@ -205,7 +239,7 @@ class BaseService:
|
|
|
205
239
|
else created
|
|
206
240
|
)
|
|
207
241
|
|
|
208
|
-
async def update(self, id: str, data: BaseModel) ->
|
|
242
|
+
async def update(self, id: str, data: BaseModel) -> ModelT:
|
|
209
243
|
kwargs = self.get_kwargs_query()
|
|
210
244
|
obj = await self.repository.get_by_id(id, **kwargs)
|
|
211
245
|
if not obj:
|
|
@@ -1,8 +1,69 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
import inspect
|
|
2
|
-
|
|
3
|
+
import types
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
ClassVar,
|
|
7
|
+
Dict,
|
|
8
|
+
List,
|
|
9
|
+
Optional,
|
|
10
|
+
Type,
|
|
11
|
+
Set,
|
|
12
|
+
Union,
|
|
13
|
+
get_args,
|
|
14
|
+
get_origin,
|
|
15
|
+
)
|
|
3
16
|
from fastapi import Depends, Request
|
|
4
17
|
from pydantic import BaseModel, TypeAdapter
|
|
5
18
|
|
|
19
|
+
|
|
20
|
+
@functools.lru_cache(maxsize=512)
|
|
21
|
+
def _type_adapter_for(target: Any) -> Any:
|
|
22
|
+
"""Devuelve un ``TypeAdapter`` cacheado para coaccionar un valor de query a
|
|
23
|
+
``target`` con el MISMO motor que usa FastAPI (pydantic). Cubre date,
|
|
24
|
+
datetime, UUID, Decimal, Enum, etc. — todo lo que no sea bool/int/float/str.
|
|
25
|
+
``None`` si el tipo no es adaptable."""
|
|
26
|
+
try:
|
|
27
|
+
return TypeAdapter(target)
|
|
28
|
+
except Exception:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@functools.lru_cache(maxsize=1024)
|
|
33
|
+
def _endpoint_param_types_cached(endpoint: Any) -> Dict[str, Any]:
|
|
34
|
+
"""Mapea {nombre_param: anotación} desde la firma del endpoint, CACHEADO.
|
|
35
|
+
|
|
36
|
+
El endpoint (`request.scope["endpoint"]`) es el MISMO objeto callable en cada
|
|
37
|
+
request (se crea una vez al montar la ruta), así que su firma se computa una
|
|
38
|
+
sola vez por endpoint — no en cada request de listado. Antes cada listado
|
|
39
|
+
pagaba un `inspect.signature`; ahora es O(1) tras el primer hit.
|
|
40
|
+
|
|
41
|
+
El dict devuelto es compartido (cacheado): tratar como SOLO-LECTURA.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
parameters = inspect.signature(endpoint).parameters
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
return {}
|
|
47
|
+
return {
|
|
48
|
+
name: param.annotation
|
|
49
|
+
for name, param in parameters.items()
|
|
50
|
+
if param.annotation is not inspect.Parameter.empty
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _unwrap_optional(annotation: Any) -> Any:
|
|
55
|
+
"""Strip ``Optional[X]`` / ``Union[X, None]`` / ``X | None`` → ``X``.
|
|
56
|
+
|
|
57
|
+
Returns the annotation unchanged when it is not an optional/union, or when
|
|
58
|
+
the union has no single non-``None`` member.
|
|
59
|
+
"""
|
|
60
|
+
origin = get_origin(annotation)
|
|
61
|
+
if origin is Union or origin is getattr(types, "UnionType", ()): # X | None
|
|
62
|
+
non_none = [a for a in get_args(annotation) if a is not type(None)]
|
|
63
|
+
if len(non_none) == 1:
|
|
64
|
+
return non_none[0]
|
|
65
|
+
return annotation
|
|
66
|
+
|
|
6
67
|
from ..permissions.base import BasePermission
|
|
7
68
|
|
|
8
69
|
from ...schema.base import BasePaginationResponse, BaseResponse
|
|
@@ -58,13 +119,19 @@ class BaseController:
|
|
|
58
119
|
async def prepare_action(self, action_name: str) -> None:
|
|
59
120
|
"""Set the current action and run permission checks.
|
|
60
121
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
122
|
+
NO metaclass or auto-wrapping exists. This is called EXPLICITLY:
|
|
123
|
+
|
|
124
|
+
- The base CRUD methods (``list``/``retrieve``/``create``/``update``/
|
|
125
|
+
``delete``) call it for you — if your endpoint just does
|
|
126
|
+
``return await super().list()`` you are covered.
|
|
127
|
+
- A CUSTOM endpoint (any method you write yourself, e.g.
|
|
128
|
+
``async def approve(self, id: str)``) runs with NO permission check
|
|
129
|
+
unless you add ``await self.prepare_action("approve")`` as its first
|
|
130
|
+
line. Forgetting this silently skips ``permission_classes``.
|
|
131
|
+
|
|
132
|
+
Idempotent within one instance: if the same ``action_name`` is already
|
|
133
|
+
prepared, subsequent calls are no-ops, so calling it in a custom method
|
|
134
|
+
that also delegates to ``super().<crud>()`` won't double-fire.
|
|
68
135
|
"""
|
|
69
136
|
if getattr(self, "_basekit_prepared_action", None) == action_name:
|
|
70
137
|
return
|
|
@@ -102,9 +169,10 @@ class BaseController:
|
|
|
102
169
|
"""Backward-compat alias for ``check_permissions``.
|
|
103
170
|
|
|
104
171
|
Pre-0.3.2 controllers called this manually inside endpoint methods.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
172
|
+
Kept working for code that hasn't migrated. New code should declare
|
|
173
|
+
``permission_classes`` on the controller and call
|
|
174
|
+
``await self.prepare_action("<action>")`` at the top of each custom
|
|
175
|
+
endpoint (there is no metaclass that does this automatically).
|
|
108
176
|
"""
|
|
109
177
|
await self.check_permissions()
|
|
110
178
|
|
|
@@ -205,79 +273,53 @@ class BaseController:
|
|
|
205
273
|
return response_cls(**kwargs)
|
|
206
274
|
|
|
207
275
|
def _params(self, skip_frames: int = 1) -> Dict[str, Any]:
|
|
276
|
+
"""Extrae page/count/search/order_by/filters de ``request.query_params``.
|
|
277
|
+
|
|
278
|
+
Los valores llegan como strings; se coaccionan a los tipos DECLARADOS
|
|
279
|
+
en la firma del endpoint (leída de ``request.scope["endpoint"]``), de
|
|
280
|
+
forma DETERMINISTA — sin introspección de stack-frames. Un query param
|
|
281
|
+
que no está declarado en la firma se deja como string (idéntico a lo
|
|
282
|
+
que haría FastAPI si no lo tipara), evitando coacciones-adivinanza que
|
|
283
|
+
romperían columnas string con valores numéricos.
|
|
284
|
+
|
|
285
|
+
Reemplaza el viejo mecanismo basado en ``inspect.currentframe()`` +
|
|
286
|
+
``skip_frames`` (un número mágico distinto por capa de herencia, fuente
|
|
287
|
+
de listados vacíos difíciles de diagnosticar).
|
|
288
|
+
|
|
289
|
+
``skip_frames``: DEPRECADO e IGNORADO. Se conserva solo por
|
|
290
|
+
retro-compatibilidad — algunos consumidores overridean ``_params`` y
|
|
291
|
+
llaman ``super()._params(skip_frames + 1)`` (p.ej. mixins de path
|
|
292
|
+
params). Ya no se usa: la coerción no depende de frames. No lo pases en
|
|
293
|
+
código nuevo.
|
|
208
294
|
"""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
del método llamador (con tipos ya convertidos por FastAPI).
|
|
214
|
-
|
|
215
|
-
Args:
|
|
216
|
-
skip_frames: Número de frames a saltar (1 por defecto para
|
|
217
|
-
llamadas directas, 2 para controllers heredados)
|
|
218
|
-
"""
|
|
219
|
-
# Obtener query_params como fuente de verdad
|
|
220
|
-
query_params = self.request.query_params if self.request else {}
|
|
295
|
+
query_params = (
|
|
296
|
+
dict(self.request.query_params) if self.request else {}
|
|
297
|
+
)
|
|
298
|
+
declared = self._endpoint_param_types()
|
|
221
299
|
|
|
222
|
-
# Parámetros especiales de paginación y búsqueda
|
|
223
300
|
standard_params = {"page", "count", "search", "order_by"}
|
|
224
|
-
|
|
225
|
-
# Valores por defecto
|
|
226
301
|
page = 1
|
|
227
302
|
count = 10
|
|
228
303
|
search = None
|
|
229
304
|
order_by = None
|
|
230
|
-
filters = {}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if frame:
|
|
237
|
-
# Navegar hacia atrás en la pila según skip_frames
|
|
238
|
-
caller_frame = frame
|
|
239
|
-
for _ in range(skip_frames):
|
|
240
|
-
if caller_frame and caller_frame.f_back:
|
|
241
|
-
caller_frame = caller_frame.f_back
|
|
242
|
-
else:
|
|
243
|
-
break
|
|
244
|
-
|
|
245
|
-
if caller_frame:
|
|
246
|
-
caller_locals = caller_frame.f_locals
|
|
247
|
-
|
|
248
|
-
# Procesar cada parámetro de query_params
|
|
249
|
-
for param_name, param_value in query_params.items():
|
|
250
|
-
# Intentar obtener valor validado del frame local
|
|
251
|
-
validated_value = caller_locals.get(param_name)
|
|
252
|
-
|
|
253
|
-
# Si no existe en locals, usar el valor del query_param
|
|
254
|
-
final_value = (
|
|
255
|
-
validated_value if validated_value is not None else param_value
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
# Clasificar el parámetro
|
|
305
|
+
filters: Dict[str, Any] = {}
|
|
306
|
+
|
|
307
|
+
for param_name, raw_value in query_params.items():
|
|
308
|
+
value = self._coerce_param(raw_value, declared.get(param_name))
|
|
309
|
+
|
|
259
310
|
if param_name == "page":
|
|
260
|
-
page = (
|
|
261
|
-
int(final_value)
|
|
262
|
-
if not isinstance(final_value, int)
|
|
263
|
-
else final_value
|
|
264
|
-
)
|
|
311
|
+
page = self._as_int(value, 1)
|
|
265
312
|
elif param_name == "count":
|
|
266
|
-
count = (
|
|
267
|
-
int(final_value)
|
|
268
|
-
if not isinstance(final_value, int)
|
|
269
|
-
else final_value
|
|
270
|
-
)
|
|
313
|
+
count = self._as_int(value, 10)
|
|
271
314
|
elif param_name == "search":
|
|
272
|
-
search =
|
|
315
|
+
search = value
|
|
273
316
|
elif param_name == "order_by":
|
|
274
|
-
order_by =
|
|
317
|
+
order_by = value
|
|
275
318
|
elif (
|
|
276
319
|
param_name not in standard_params
|
|
277
320
|
and param_name not in self._params_excluded_fields
|
|
278
321
|
):
|
|
279
|
-
|
|
280
|
-
filters[param_name] = final_value
|
|
322
|
+
filters[param_name] = value
|
|
281
323
|
|
|
282
324
|
return {
|
|
283
325
|
"page": page,
|
|
@@ -287,6 +329,70 @@ class BaseController:
|
|
|
287
329
|
"filters": filters,
|
|
288
330
|
}
|
|
289
331
|
|
|
332
|
+
def _endpoint_param_types(self) -> Dict[str, Any]:
|
|
333
|
+
"""Mapea {nombre_param: anotación} desde la firma del endpoint activo.
|
|
334
|
+
|
|
335
|
+
Lee ``request.scope["endpoint"]`` (la función de ruta que FastAPI está
|
|
336
|
+
ejecutando) y delega en el cache por-endpoint. Devuelve ``{}`` si no hay
|
|
337
|
+
endpoint o su firma no es introspectable (los valores quedan como string).
|
|
338
|
+
"""
|
|
339
|
+
request = getattr(self, "request", None)
|
|
340
|
+
scope = getattr(request, "scope", None) if request is not None else None
|
|
341
|
+
endpoint = scope.get("endpoint") if isinstance(scope, dict) else None
|
|
342
|
+
if endpoint is None:
|
|
343
|
+
return {}
|
|
344
|
+
try:
|
|
345
|
+
return _endpoint_param_types_cached(endpoint)
|
|
346
|
+
except TypeError:
|
|
347
|
+
# endpoint no hasheable (raro) → sin coerción, valores string.
|
|
348
|
+
return {}
|
|
349
|
+
|
|
350
|
+
@staticmethod
|
|
351
|
+
def _coerce_param(raw: Any, annotation: Any) -> Any:
|
|
352
|
+
"""Coacciona un valor de query (string) al tipo declarado en la firma.
|
|
353
|
+
|
|
354
|
+
bool/int/float a mano; el RESTO (date, datetime, UUID, Decimal, Enum…)
|
|
355
|
+
vía pydantic ``TypeAdapter`` — el MISMO motor que usa FastAPI, para que
|
|
356
|
+
el filtro reciba el objeto tipado (ej. un `date` real que compara contra
|
|
357
|
+
una columna DATE) y no el string. Sin anotación, str, o si la coacción
|
|
358
|
+
falla → se devuelve el valor original sin romper.
|
|
359
|
+
|
|
360
|
+
Nota: FastAPI ya rechaza (422) un query param tipado con valor inválido
|
|
361
|
+
antes de llegar acá, así que en la práctica la coacción siempre aplica.
|
|
362
|
+
"""
|
|
363
|
+
if annotation is None or not isinstance(raw, str):
|
|
364
|
+
return raw
|
|
365
|
+
target = _unwrap_optional(annotation)
|
|
366
|
+
if target is bool:
|
|
367
|
+
return raw.strip().lower() in {"1", "true", "t", "yes", "on"}
|
|
368
|
+
if target is int:
|
|
369
|
+
try:
|
|
370
|
+
return int(raw)
|
|
371
|
+
except (TypeError, ValueError):
|
|
372
|
+
return raw
|
|
373
|
+
if target is float:
|
|
374
|
+
try:
|
|
375
|
+
return float(raw)
|
|
376
|
+
except (TypeError, ValueError):
|
|
377
|
+
return raw
|
|
378
|
+
if target is str or target is Any:
|
|
379
|
+
return raw
|
|
380
|
+
# date / datetime / UUID / Decimal / Enum / etc. → pydantic (FastAPI-parity)
|
|
381
|
+
adapter = _type_adapter_for(target)
|
|
382
|
+
if adapter is None:
|
|
383
|
+
return raw
|
|
384
|
+
try:
|
|
385
|
+
return adapter.validate_python(raw)
|
|
386
|
+
except Exception:
|
|
387
|
+
return raw
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _as_int(value: Any, default: int) -> int:
|
|
391
|
+
try:
|
|
392
|
+
return int(value)
|
|
393
|
+
except (TypeError, ValueError):
|
|
394
|
+
return default
|
|
395
|
+
|
|
290
396
|
def to_dict(self, obj: Any):
|
|
291
397
|
"""Helper para convertir modelos ORM/Pydantic a dict."""
|
|
292
398
|
if hasattr(obj, "model_dump"): # Pydantic v2
|
{fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py
RENAMED
|
@@ -49,7 +49,7 @@ class SQLAlchemyBaseController(BaseController):
|
|
|
49
49
|
order_by: Expresión de ordenamiento (ej: User.created_at.desc())
|
|
50
50
|
"""
|
|
51
51
|
await self.prepare_action("list")
|
|
52
|
-
params = self._params(
|
|
52
|
+
params = self._params()
|
|
53
53
|
service_params = {
|
|
54
54
|
**params,
|
|
55
55
|
"use_or": use_or,
|