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.
Files changed (146) hide show
  1. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/PKG-INFO +3 -3
  2. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/controller/base.py +1 -1
  3. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/repository/base.py +63 -12
  4. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/service/base.py +60 -26
  5. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/controller/base.py +175 -69
  6. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py +1 -1
  7. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/repository/base.py +79 -16
  8. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/service/base.py +48 -10
  9. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/repository/base.py +105 -22
  10. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/service/base.py +36 -11
  11. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/handler.py +15 -0
  12. fastapi_basekit-0.5.0/fastapi_basekit/py.typed +1 -0
  13. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/thrid/jwt.py +42 -7
  14. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/PKG-INFO +3 -3
  15. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/SOURCES.txt +21 -1
  16. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/requires.txt +2 -2
  17. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/pyproject.toml +4 -3
  18. fastapi_basekit-0.5.0/tests/test_beanie_repository_regression.py +261 -0
  19. fastapi_basekit-0.5.0/tests/test_beanie_service_regression.py +244 -0
  20. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_exception_handlers.py +30 -0
  21. fastapi_basekit-0.5.0/tests/test_generics_optional_and_cache.py +99 -0
  22. fastapi_basekit-0.5.0/tests/test_generics_wiring.py +100 -0
  23. fastapi_basekit-0.5.0/tests/test_jwt_config_robustness.py +61 -0
  24. fastapi_basekit-0.5.0/tests/test_params_classification.py +193 -0
  25. fastapi_basekit-0.5.0/tests/test_params_coercion_matrix.py +110 -0
  26. fastapi_basekit-0.5.0/tests/test_params_coercion_unit.py +165 -0
  27. fastapi_basekit-0.5.0/tests/test_params_http_matrix_beanie.py +109 -0
  28. fastapi_basekit-0.5.0/tests/test_params_http_matrix_sqlalchemy.py +114 -0
  29. fastapi_basekit-0.5.0/tests/test_params_rich_type_coercion.py +79 -0
  30. fastapi_basekit-0.5.0/tests/test_path_filter_mixin_compat.py +179 -0
  31. fastapi_basekit-0.5.0/tests/test_post_process_list_hook.py +154 -0
  32. fastapi_basekit-0.5.0/tests/test_pulbot_cbv_pattern_beanie.py +146 -0
  33. fastapi_basekit-0.5.0/tests/test_repo_signature_aliases.py +74 -0
  34. fastapi_basekit-0.5.0/tests/test_sqlalchemy_repository_regression.py +173 -0
  35. fastapi_basekit-0.5.0/tests/test_sqlalchemy_service_regression.py +154 -0
  36. fastapi_basekit-0.5.0/tests/test_sqlmodel_filter_operators.py +92 -0
  37. fastapi_basekit-0.5.0/tests/test_typed_filter_coercion.py +114 -0
  38. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/LICENSE +0 -0
  39. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/README.md +0 -0
  40. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/__init__.py +0 -0
  41. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/__init__.py +0 -0
  42. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/__init__.py +0 -0
  43. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
  44. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
  45. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
  46. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/controller/__init__.py +0 -0
  47. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/permissions/__init__.py +0 -0
  48. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/permissions/base.py +0 -0
  49. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
  50. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
  51. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
  52. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
  53. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
  54. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
  55. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
  56. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
  57. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
  58. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
  59. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/cli/__init__.py +0 -0
  60. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/cli/main.py +0 -0
  61. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/__init__.py +0 -0
  62. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
  63. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/domain.py +0 -0
  64. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/openapi.py +0 -0
  65. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/__init__.py +0 -0
  66. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/base.py +0 -0
  67. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/jwt.py +0 -0
  68. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/schema.py +0 -0
  69. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/__init__.py +0 -0
  70. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
  71. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
  72. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
  73. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
  74. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
  75. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
  76. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
  77. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
  78. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
  79. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
  80. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
  81. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
  82. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
  83. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
  84. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
  85. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
  92. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
  93. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
  94. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
  95. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
  96. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
  97. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
  98. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
  99. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
  100. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
  101. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
  102. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
  103. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
  104. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
  105. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
  106. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
  107. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
  108. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
  109. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
  110. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
  111. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
  112. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
  113. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
  114. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
  115. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
  116. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
  117. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
  118. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
  119. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
  120. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
  121. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
  122. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
  123. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
  124. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
  125. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
  126. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
  127. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/entry_points.txt +0 -0
  128. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/top_level.txt +0 -0
  129. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/setup.cfg +0 -0
  130. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_action_param_leak.py +0 -0
  131. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_api_exceptions.py +0 -0
  132. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_base_response.py +0 -0
  133. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_base_service.py +0 -0
  134. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_aggregation_hooks.py +0 -0
  135. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_aggregation_integration.py +0 -0
  136. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_keyset_pagination.py +0 -0
  137. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_beanie_service_mutable_defaults.py +0 -0
  138. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_controller_auto_permissions.py +0 -0
  139. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_crud_beanie_controller.py +0 -0
  140. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_crud_controller.py +0 -0
  141. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_crud_sqlmodel_repository_service.py +0 -0
  142. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_filter_operators.py +0 -0
  143. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_jwt_service.py +0 -0
  144. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_openapi_simplify.py +0 -0
  145. {fastapi_basekit-0.4.3 → fastapi_basekit-0.5.0}/tests/test_sql_queryset_override.py +0 -0
  146. {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.4.3
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"
@@ -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(skip_frames=2)
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
@@ -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
- """Run an aggregation pipeline with `$facet` pagination.
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[Document]:
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[Document]:
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[Document]:
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[Document]:
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[Document, Dict[str, Any]]) -> Document:
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: Document, data: Dict[str, Any]) -> Document:
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: Document) -> None:
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
- repository: BaseRepository
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
- return await self.repository.paginate_pipeline(
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
- # FindMany path
170
- order_list = None
171
- if order_str:
172
- direction = -1 if order_str.startswith("-") else 1
173
- field = order_str.lstrip("-")
174
- order_list = [(field, direction)]
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
- query = self.build_list_queryset(
177
- search=search,
178
- search_fields=self.search_fields,
179
- filters=applied_filters,
180
- order_by=order_list,
181
- **kwargs,
182
- )
183
- return await self.repository.paginate(
184
- query, page, count, order_by=order_list
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
- ) -> Any:
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) -> Any:
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
- from typing import Any, ClassVar, Dict, List, Optional, Type, Set
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
- Auto-called by ``ControllerMeta`` for every public async method on
62
- the controller. Idempotent within one invocation: if the same
63
- ``action_name`` has already been prepared on this instance,
64
- subsequent calls are no-ops. This lets custom methods opt into
65
- calling ``await self.prepare_action(...)`` explicitly without
66
- double-firing permission checks (when the metaclass already ran
67
- before entering the method body).
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
- Keep working for users who haven't migrated to ``permission_classes``
106
- + auto-wrapping yet. New code should declare ``permission_classes``
107
- on the controller and let the metaclass run permissions.
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
- Extrae parámetros automáticamente usando introspección.
210
-
211
- Usa query_params como fuente de verdad para determinar QUÉ parámetros
212
- existen, y luego intenta obtener sus VALORES validados desde el frame
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
- # Intentar obtener valores validados del frame local
233
- frame = inspect.currentframe()
234
- caller_locals = {}
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 = final_value
315
+ search = value
273
316
  elif param_name == "order_by":
274
- order_by = final_value
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
- # Es un filtro
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
@@ -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(skip_frames=2)
52
+ params = self._params()
53
53
  service_params = {
54
54
  **params,
55
55
  "use_or": use_or,