fastapi-basekit 0.4.1__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.1 → fastapi_basekit-0.5.0}/PKG-INFO +3 -3
  2. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/controller/base.py +1 -1
  3. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/repository/base.py +126 -14
  4. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/service/base.py +68 -26
  5. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/controller/base.py +175 -69
  6. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py +1 -1
  7. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/repository/base.py +89 -15
  8. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/service/base.py +106 -11
  9. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/repository/base.py +105 -22
  10. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/service/base.py +36 -11
  11. {fastapi_basekit-0.4.1 → 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.1 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/thrid/jwt.py +42 -7
  14. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/PKG-INFO +3 -3
  15. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/SOURCES.txt +23 -1
  16. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/requires.txt +2 -2
  17. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/pyproject.toml +4 -3
  18. fastapi_basekit-0.5.0/tests/test_beanie_keyset_pagination.py +75 -0
  19. fastapi_basekit-0.5.0/tests/test_beanie_repository_regression.py +261 -0
  20. fastapi_basekit-0.5.0/tests/test_beanie_service_mutable_defaults.py +29 -0
  21. fastapi_basekit-0.5.0/tests/test_beanie_service_regression.py +244 -0
  22. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_exception_handlers.py +30 -0
  23. fastapi_basekit-0.5.0/tests/test_generics_optional_and_cache.py +99 -0
  24. fastapi_basekit-0.5.0/tests/test_generics_wiring.py +100 -0
  25. fastapi_basekit-0.5.0/tests/test_jwt_config_robustness.py +61 -0
  26. fastapi_basekit-0.5.0/tests/test_params_classification.py +193 -0
  27. fastapi_basekit-0.5.0/tests/test_params_coercion_matrix.py +110 -0
  28. fastapi_basekit-0.5.0/tests/test_params_coercion_unit.py +165 -0
  29. fastapi_basekit-0.5.0/tests/test_params_http_matrix_beanie.py +109 -0
  30. fastapi_basekit-0.5.0/tests/test_params_http_matrix_sqlalchemy.py +114 -0
  31. fastapi_basekit-0.5.0/tests/test_params_rich_type_coercion.py +79 -0
  32. fastapi_basekit-0.5.0/tests/test_path_filter_mixin_compat.py +179 -0
  33. fastapi_basekit-0.5.0/tests/test_post_process_list_hook.py +154 -0
  34. fastapi_basekit-0.5.0/tests/test_pulbot_cbv_pattern_beanie.py +146 -0
  35. fastapi_basekit-0.5.0/tests/test_repo_signature_aliases.py +74 -0
  36. fastapi_basekit-0.5.0/tests/test_sqlalchemy_repository_regression.py +173 -0
  37. fastapi_basekit-0.5.0/tests/test_sqlalchemy_service_regression.py +154 -0
  38. fastapi_basekit-0.5.0/tests/test_sqlmodel_filter_operators.py +92 -0
  39. fastapi_basekit-0.5.0/tests/test_typed_filter_coercion.py +114 -0
  40. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/LICENSE +0 -0
  41. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/README.md +0 -0
  42. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/__init__.py +0 -0
  43. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/__init__.py +0 -0
  44. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/__init__.py +0 -0
  45. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
  46. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
  47. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
  48. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/controller/__init__.py +0 -0
  49. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/permissions/__init__.py +0 -0
  50. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/permissions/base.py +0 -0
  51. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
  52. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
  53. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
  54. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
  55. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
  56. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
  57. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
  58. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
  59. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
  60. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
  61. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/cli/__init__.py +0 -0
  62. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/cli/main.py +0 -0
  63. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/__init__.py +0 -0
  64. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
  65. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/exceptions/domain.py +0 -0
  66. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/openapi.py +0 -0
  67. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/__init__.py +0 -0
  68. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/base.py +0 -0
  69. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/jwt.py +0 -0
  70. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/schema/schema.py +0 -0
  71. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/__init__.py +0 -0
  72. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
  73. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
  74. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
  75. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
  76. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
  77. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
  78. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
  79. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
  80. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
  81. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
  82. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
  83. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
  84. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
  85. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
  86. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
  87. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
  88. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/__init__.py +0 -0
  89. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
  90. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
  91. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
  92. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
  93. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
  94. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
  95. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
  96. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
  97. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
  98. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
  99. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
  100. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
  101. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
  102. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
  103. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
  104. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
  105. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
  106. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
  107. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
  108. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
  109. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
  110. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
  111. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
  112. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
  113. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
  114. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
  115. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
  116. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
  117. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
  118. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
  119. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
  120. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
  121. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
  122. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
  123. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
  124. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
  125. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
  126. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
  127. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
  128. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
  129. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/entry_points.txt +0 -0
  130. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/fastapi_basekit.egg-info/top_level.txt +0 -0
  131. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/setup.cfg +0 -0
  132. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_action_param_leak.py +0 -0
  133. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_api_exceptions.py +0 -0
  134. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_base_response.py +0 -0
  135. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_base_service.py +0 -0
  136. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_beanie_aggregation_hooks.py +0 -0
  137. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_beanie_aggregation_integration.py +0 -0
  138. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_controller_auto_permissions.py +0 -0
  139. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_crud_beanie_controller.py +0 -0
  140. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_crud_controller.py +0 -0
  141. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_crud_sqlmodel_repository_service.py +0 -0
  142. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_filter_operators.py +0 -0
  143. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_jwt_service.py +0 -0
  144. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_openapi_simplify.py +0 -0
  145. {fastapi_basekit-0.4.1 → fastapi_basekit-0.5.0}/tests/test_sql_queryset_override.py +0 -0
  146. {fastapi_basekit-0.4.1 → 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.1
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,4 +1,5 @@
1
- from typing import Any, Dict, List, Optional, Type, Union
1
+ import logging
2
+ from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
2
3
  from typing import get_args, get_origin
3
4
  import re
4
5
 
@@ -8,10 +9,33 @@ from beanie import Document, Link
8
9
  from beanie.odm.queries.find import FindMany
9
10
  from beanie.operators import Or, RegEx
10
11
 
12
+ logger = logging.getLogger(__name__)
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]
11
38
 
12
- class BaseRepository:
13
- model: Type[Document]
14
-
15
39
  def _parse_order_field(self, order_by: str) -> tuple[str, int, bool]:
16
40
  """Parse order_by string into components.
17
41
 
@@ -204,6 +228,17 @@ class BaseRepository:
204
228
  async def paginate(
205
229
  self, query: FindMany[Document], page: int, count: int, order_by: Optional[List[tuple]] = None
206
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
+ """
207
242
  # Apply ordering if provided and not already applied
208
243
  if order_by:
209
244
  query = query.sort(order_by)
@@ -212,6 +247,44 @@ class BaseRepository:
212
247
  items = await query.skip(count * (page - 1)).limit(count).to_list()
213
248
  return items, total
214
249
 
250
+ async def paginate_keyset(
251
+ self,
252
+ query: FindMany[Document],
253
+ limit: int,
254
+ cursor_field: str,
255
+ cursor_value: Optional[Any] = None,
256
+ ascending: bool = False,
257
+ ) -> tuple[List[Document], bool]:
258
+ """Keyset (cursor) pagination — O(1) por página, sin `skip` ni `count`.
259
+
260
+ Alternativa a `paginate` para listados que escalan: en vez de contar
261
+ toda la colección y saltar `count*(page-1)` documentos (coste que crece
262
+ con la profundidad), filtra por un cursor sobre `cursor_field` y trae
263
+ `limit + 1` para saber si quedan más — sin un `count()`.
264
+
265
+ Args:
266
+ query: FindMany ya filtrado (los filtros base del listado).
267
+ limit: tamaño de página.
268
+ cursor_field: campo Mongo indexado del cursor (ej. "_id",
269
+ "created_at", "timestamp"). Debe existir un índice que lo cubra.
270
+ cursor_value: valor del último item de la página previa. None =
271
+ primera página.
272
+ ascending: True = orden ascendente (`> cursor`); False = descendente
273
+ (`< cursor`), útil para "más recientes primero".
274
+
275
+ Returns:
276
+ (items, has_more). El caller deriva el próximo cursor del último
277
+ item devuelto (`getattr(items[-1], cursor_field)`).
278
+ """
279
+ limit = max(1, int(limit))
280
+ op = "$gt" if ascending else "$lt"
281
+ if cursor_value is not None:
282
+ query = query.find({cursor_field: {op: cursor_value}})
283
+ query = query.sort((cursor_field, 1 if ascending else -1))
284
+ docs = await query.limit(limit + 1).to_list()
285
+ has_more = len(docs) > limit
286
+ return docs[:limit], has_more
287
+
215
288
  def build_list_queryset(
216
289
  self,
217
290
  search: Optional[str] = None,
@@ -338,7 +411,13 @@ class BaseRepository:
338
411
  count: int,
339
412
  validate: bool = True,
340
413
  ) -> tuple[List[Any], int]:
341
- """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`.
342
421
 
343
422
  Args:
344
423
  pipeline: Pipeline stages (without the final `$facet`).
@@ -371,11 +450,31 @@ class BaseRepository:
371
450
  return items_raw, total
372
451
 
373
452
  items: List[Any] = []
453
+ dropped = 0
374
454
  for raw_item in items_raw:
375
455
  try:
376
456
  items.append(self.model.model_validate(raw_item))
377
- except Exception:
378
- continue
457
+ except Exception as exc:
458
+ # No tragar en silencio: una fila que no valida se pierde de la
459
+ # respuesta sin aviso (menos items que el limit, total inflado).
460
+ # Se loguea para que sea diagnosticable; la fila igual se omite
461
+ # para no romper el listado completo por un doc corrupto.
462
+ dropped += 1
463
+ logger.warning(
464
+ "paginate_pipeline: fila descartada por fallo de "
465
+ "validación contra %s: %s",
466
+ getattr(self.model, "__name__", self.model),
467
+ exc,
468
+ )
469
+ if dropped:
470
+ logger.warning(
471
+ "paginate_pipeline: %d/%d filas descartadas en %s "
472
+ "(total reportado=%d)",
473
+ dropped,
474
+ len(items_raw),
475
+ getattr(self.model, "__name__", self.model),
476
+ total,
477
+ )
379
478
  return items, total
380
479
 
381
480
  async def list_with_aggregation(
@@ -410,7 +509,7 @@ class BaseRepository:
410
509
  self,
411
510
  obj_id: Union[str, ObjectId],
412
511
  **kwargs,
413
- ) -> Optional[Document]:
512
+ ) -> Optional[ModelT]:
414
513
  if not isinstance(obj_id, ObjectId):
415
514
  obj_id = ObjectId(obj_id)
416
515
  return await self.model.find_one(
@@ -418,12 +517,19 @@ class BaseRepository:
418
517
  **self._get_query_kwargs(**kwargs),
419
518
  )
420
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
+
421
527
  async def get_by_field(
422
528
  self,
423
529
  field_name: str,
424
530
  value: Any,
425
531
  **kwargs,
426
- ) -> Optional[Document]:
532
+ ) -> Optional[ModelT]:
427
533
  if not hasattr(self.model, field_name):
428
534
  raise AttributeError(
429
535
  f"{self.model.__name__} no tiene el campo '{field_name}'"
@@ -437,7 +543,7 @@ class BaseRepository:
437
543
  self,
438
544
  filters: Dict[str, Any],
439
545
  **kwargs,
440
- ) -> Optional[Document]:
546
+ ) -> Optional[ModelT]:
441
547
  exprs = [
442
548
  getattr(self.model, f) == v
443
549
  for f, v in filters.items()
@@ -452,21 +558,27 @@ class BaseRepository:
452
558
  async def list_all(
453
559
  self,
454
560
  **kwargs,
455
- ) -> List[Document]:
561
+ ) -> List[ModelT]:
456
562
  query = self.model.find_all(**self._get_query_kwargs(**kwargs))
457
563
  return await query.to_list()
458
564
 
459
- async def create(self, obj: Union[Document, Dict[str, Any]]) -> Document:
565
+ async def create(self, obj: Union[ModelT, Dict[str, Any]]) -> ModelT:
460
566
  if isinstance(obj, dict):
461
567
  obj = self.model(**obj)
462
568
  await obj.insert()
463
569
  return obj
464
570
 
465
- 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
+ """
466
578
  for key, value in data.items():
467
579
  setattr(obj, key, value)
468
580
  await obj.save()
469
581
  return obj
470
582
 
471
- async def delete(self, obj: Document) -> None:
583
+ async def delete(self, obj: ModelT) -> None:
472
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]] = {}
@@ -30,6 +38,14 @@ class BaseService:
30
38
  ):
31
39
  self.repository = repository
32
40
  self.request = request
41
+ # Copia por instancia de los defaults mutables (heredados como
42
+ # atributos de CLASE, compartidos por todo el proceso). Sin esto, una
43
+ # mutación en runtime (`self.search_fields.append(...)`) se filtraría a
44
+ # la clase y contaminaría otras requests. Respeta el override del
45
+ # subclass: `list(self.search_fields)` lee su atributo de clase.
46
+ self.search_fields = list(self.search_fields)
47
+ self.duplicate_check_fields = list(self.duplicate_check_fields)
48
+ self.kwargs_query = dict(self.kwargs_query)
33
49
  endpoint_func = (
34
50
  self.request.scope.get("endpoint") if self.request else None
35
51
  )
@@ -66,7 +82,7 @@ class BaseService:
66
82
  filters = filters or {}
67
83
  return filters
68
84
 
69
- async def retrieve(self, id: str):
85
+ async def retrieve(self, id: str) -> ModelT:
70
86
  kwargs = self.get_kwargs_query()
71
87
  obj = await self.repository.get_by_id(id, **kwargs)
72
88
  if not obj:
@@ -125,7 +141,7 @@ class BaseService:
125
141
  count: int = 25,
126
142
  filters: Optional[Dict[str, Any]] = None,
127
143
  order_by: Optional[str] = None, # Dynamic ordering (e.g., "-created_at" or "tool__name")
128
- ):
144
+ ) -> Tuple[List[ModelT], int]:
129
145
  kwargs = self.get_kwargs_query()
130
146
  applied_filters = self.get_filters(filters)
131
147
 
@@ -151,34 +167,60 @@ class BaseService:
151
167
  order_by=order_str,
152
168
  **kwargs,
153
169
  )
154
- return await self.repository.paginate_pipeline(
170
+ items, total = await self.repository.paginate_pipeline(
155
171
  pipeline,
156
172
  page=page,
157
173
  count=count,
158
174
  validate=self.aggregation_validate,
159
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)]
160
183
 
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)]
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
+ )
167
194
 
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
- )
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
178
220
 
179
221
  async def create(
180
222
  self, payload: BaseModel, check_fields: Optional[List[str]] = None
181
- ) -> Any:
223
+ ) -> ModelT:
182
224
  data = (
183
225
  payload.model_dump() if not isinstance(payload, dict) else payload
184
226
  )
@@ -197,7 +239,7 @@ class BaseService:
197
239
  else created
198
240
  )
199
241
 
200
- async def update(self, id: str, data: BaseModel) -> Any:
242
+ async def update(self, id: str, data: BaseModel) -> ModelT:
201
243
  kwargs = self.get_kwargs_query()
202
244
  obj = await self.repository.get_by_id(id, **kwargs)
203
245
  if not obj: