fastapi-basekit 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/PKG-INFO +23 -19
  2. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/README.md +16 -10
  3. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/base.py +167 -63
  4. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/service/base.py +81 -30
  5. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/__init__.py +2 -1
  6. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/base.py +8 -1
  7. fastapi_basekit-0.3.2/fastapi_basekit/aio/sqlalchemy/session.py +67 -0
  8. fastapi_basekit-0.3.2/fastapi_basekit/exceptions/__init__.py +3 -0
  9. fastapi_basekit-0.3.2/fastapi_basekit/exceptions/domain.py +57 -0
  10. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +7 -20
  11. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/PKG-INFO +23 -19
  12. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/SOURCES.txt +2 -0
  13. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/requires.txt +6 -8
  14. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/pyproject.toml +7 -9
  15. fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
  16. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/LICENSE +0 -0
  17. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/__init__.py +0 -0
  18. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/__init__.py +0 -0
  19. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/__init__.py +0 -0
  20. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
  21. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
  22. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
  23. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
  24. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/controller/__init__.py +0 -0
  25. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/controller/base.py +0 -0
  26. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/permissions/__init__.py +0 -0
  27. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/permissions/base.py +0 -0
  28. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
  29. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -0
  30. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
  31. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/repository/base.py +0 -0
  32. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
  33. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
  34. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
  35. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
  36. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
  37. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
  38. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
  39. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
  40. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/cli/__init__.py +0 -0
  41. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/cli/main.py +0 -0
  42. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
  43. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/exceptions/handler.py +0 -0
  44. {fastapi_basekit-0.3.0/fastapi_basekit/exceptions → fastapi_basekit-0.3.2/fastapi_basekit/schema}/__init__.py +0 -0
  45. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/base.py +0 -0
  46. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/jwt.py +0 -0
  47. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/schema/schema.py +0 -0
  48. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/__init__.py +0 -0
  49. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
  50. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
  51. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
  52. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
  53. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
  54. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
  55. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
  56. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
  57. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
  58. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
  59. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
  60. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
  61. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
  62. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
  63. {fastapi_basekit-0.3.0/fastapi_basekit/schema → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app}/__init__.py +0 -0
  64. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api}/__init__.py +0 -0
  65. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1}/__init__.py +0 -0
  66. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1 → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints}/__init__.py +0 -0
  67. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
  68. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
  69. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
  70. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
  71. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
  72. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
  73. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
  74. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
  75. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware}/__init__.py +0 -0
  76. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
  77. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
  78. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
  79. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
  80. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
  81. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
  82. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
  83. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions}/__init__.py +0 -0
  84. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
  85. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories}/__init__.py +0 -0
  86. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user}/__init__.py +0 -0
  87. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
  88. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas}/__init__.py +0 -0
  89. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
  90. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
  91. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
  92. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts}/__init__.py +0 -0
  93. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
  94. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
  95. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services}/__init__.py +0 -0
  96. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
  97. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
  98. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
  99. {fastapi_basekit-0.3.0/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services → fastapi_basekit-0.3.2/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils}/__init__.py +0 -0
  100. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
  101. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
  102. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
  103. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
  104. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
  105. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
  106. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/entry_points.txt +0 -0
  107. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/fastapi_basekit.egg-info/top_level.txt +0 -0
  108. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/setup.cfg +0 -0
  109. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/tests/test_api_exceptions.py +0 -0
  110. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/tests/test_base_response.py +0 -0
  111. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/tests/test_base_service.py +0 -0
  112. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/tests/test_controller_auto_permissions.py +0 -0
  113. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/tests/test_crud_beanie_controller.py +0 -0
  114. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/tests/test_crud_controller.py +0 -0
  115. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/tests/test_jwt_service.py +0 -0
  116. {fastapi_basekit-0.3.0 → fastapi_basekit-0.3.2}/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.0
3
+ Version: 0.3.2
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
@@ -16,14 +16,13 @@ Requires-Python: >=3.11
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: fastapi>=0.116.1
19
- Requires-Dist: pydantic<3,>=2.11.7
19
+ Requires-Dist: pydantic<3,>=2.13.0
20
20
  Requires-Dist: fastapi-restful[all]>=0.6.0
21
- Requires-Dist: pyjwt>=2.10.1
21
+ Requires-Dist: pyjwt>=2.12.1
22
22
  Provides-Extra: beanie
23
- Requires-Dist: beanie>=1.24.0; extra == "beanie"
24
- Requires-Dist: motor>=3.3.0; extra == "beanie"
23
+ Requires-Dist: beanie<3,>=2.0; extra == "beanie"
25
24
  Provides-Extra: sqlalchemy
26
- Requires-Dist: SQLAlchemy[asyncio]>=2.0.0; extra == "sqlalchemy"
25
+ Requires-Dist: SQLAlchemy[asyncio]<3,>=2.0.30; extra == "sqlalchemy"
27
26
  Requires-Dist: psycopg2>=2.9.0; extra == "sqlalchemy"
28
27
  Provides-Extra: sqlmodel
29
28
  Requires-Dist: sqlmodel>=0.0.37; extra == "sqlmodel"
@@ -36,9 +35,8 @@ Requires-Dist: mkdocstrings[python]>=0.27.0; extra == "docs"
36
35
  Requires-Dist: pymdown-extensions>=10.10; extra == "docs"
37
36
  Requires-Dist: mike>=2.1.0; extra == "docs"
38
37
  Provides-Extra: all
39
- Requires-Dist: beanie>=1.24.0; extra == "all"
40
- Requires-Dist: motor>=3.3.0; extra == "all"
41
- Requires-Dist: SQLAlchemy[asyncio]>=2.0.0; extra == "all"
38
+ Requires-Dist: beanie<3,>=2.0; extra == "all"
39
+ Requires-Dist: SQLAlchemy[asyncio]<3,>=2.0.30; extra == "all"
42
40
  Requires-Dist: psycopg2>=2.9.0; extra == "all"
43
41
  Requires-Dist: sqlmodel>=0.0.37; extra == "all"
44
42
  Requires-Dist: cookiecutter>=2.5.0; extra == "all"
@@ -48,18 +46,24 @@ Requires-Dist: mkdocstrings[python]>=0.27.0; extra == "all"
48
46
 
49
47
  <div align="center">
50
48
 
51
- # FastAPI BaseKit
49
+ <img src="docs/assets/logo-mark.svg" width="120" alt="fastapi-basekit">
52
50
 
53
- **Toolkit base para construir APIs FastAPI rápido — sin reinventar repos, services y controllers.**
51
+ # fastapi-basekit
54
52
 
55
- [![PyPI](https://img.shields.io/pypi/v/fastapi-basekit?style=flat-square&color=teal)](https://pypi.org/project/fastapi-basekit/)
56
- ![Python](https://img.shields.io/badge/python-3.11+-blue?style=flat-square&logo=python)
57
- ![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=flat-square&logo=fastapi)
58
- ![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-red?style=flat-square)
59
- ![Beanie](https://img.shields.io/badge/MongoDB-Beanie-47A248?style=flat-square&logo=mongodb)
60
- ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
53
+ **FastAPI toolkit con repos, services y controllers listos para producción.**
54
+ **Multi-ORM · CLI scaffolder · plugin Claude Code.**
61
55
 
62
- [**Docs**](https://mundobien2025.github.io/fastapi-basekit) · [**PyPI**](https://pypi.org/project/fastapi-basekit/) · [**Issues**](https://github.com/mundobien2025/fastapi-basekit/issues) · [**Changelog**](./CHANGELOG.md)
56
+ [![PyPI](https://img.shields.io/pypi/v/fastapi-basekit?style=flat-square&color=009485&logo=pypi&logoColor=white)](https://pypi.org/project/fastapi-basekit/)
57
+ [![Python](https://img.shields.io/badge/python-3.11+-2c5282?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/fastapi-basekit/)
58
+ [![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/)
59
+ [![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-c41616?style=flat-square)](https://www.sqlalchemy.org/)
60
+ [![Beanie](https://img.shields.io/badge/MongoDB-Beanie-47A248?style=flat-square&logo=mongodb&logoColor=white)](https://beanie-odm.dev/)
61
+ [![License](https://img.shields.io/badge/license-MIT-009485?style=flat-square)](./LICENSE)
62
+
63
+ [**Docs**](https://mundobien2025.github.io/fastapi-basekit) ·
64
+ [**PyPI**](https://pypi.org/project/fastapi-basekit/) ·
65
+ [**Issues**](https://github.com/mundobien2025/fastapi-basekit/issues) ·
66
+ [**Changelog**](./CHANGELOG.md)
63
67
 
64
68
  </div>
65
69
 
@@ -89,7 +93,7 @@ Abre http://localhost:8000/docs. Login con `admin@example.com` / `ChangeMe2026!`
89
93
 
90
94
  ## Documentación
91
95
 
92
- 📚 **[mundobien2025.github.io/fastapi-basekit](https://mundobien2025.github.io/fastapi-basekit)**
96
+ **[mundobien2025.github.io/fastapi-basekit](https://mundobien2025.github.io/fastapi-basekit)**
93
97
 
94
98
  | Sección | Contenido |
95
99
  |---|---|
@@ -1,17 +1,23 @@
1
1
  <div align="center">
2
2
 
3
- # FastAPI BaseKit
3
+ <img src="docs/assets/logo-mark.svg" width="120" alt="fastapi-basekit">
4
4
 
5
- **Toolkit base para construir APIs FastAPI rápido — sin reinventar repos, services y controllers.**
5
+ # fastapi-basekit
6
6
 
7
- [![PyPI](https://img.shields.io/pypi/v/fastapi-basekit?style=flat-square&color=teal)](https://pypi.org/project/fastapi-basekit/)
8
- ![Python](https://img.shields.io/badge/python-3.11+-blue?style=flat-square&logo=python)
9
- ![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=flat-square&logo=fastapi)
10
- ![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-red?style=flat-square)
11
- ![Beanie](https://img.shields.io/badge/MongoDB-Beanie-47A248?style=flat-square&logo=mongodb)
12
- ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
7
+ **FastAPI toolkit con repos, services y controllers listos para producción.**
8
+ **Multi-ORM · CLI scaffolder · plugin Claude Code.**
13
9
 
14
- [**Docs**](https://mundobien2025.github.io/fastapi-basekit) · [**PyPI**](https://pypi.org/project/fastapi-basekit/) · [**Issues**](https://github.com/mundobien2025/fastapi-basekit/issues) · [**Changelog**](./CHANGELOG.md)
10
+ [![PyPI](https://img.shields.io/pypi/v/fastapi-basekit?style=flat-square&color=009485&logo=pypi&logoColor=white)](https://pypi.org/project/fastapi-basekit/)
11
+ [![Python](https://img.shields.io/badge/python-3.11+-2c5282?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/fastapi-basekit/)
12
+ [![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/)
13
+ [![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-c41616?style=flat-square)](https://www.sqlalchemy.org/)
14
+ [![Beanie](https://img.shields.io/badge/MongoDB-Beanie-47A248?style=flat-square&logo=mongodb&logoColor=white)](https://beanie-odm.dev/)
15
+ [![License](https://img.shields.io/badge/license-MIT-009485?style=flat-square)](./LICENSE)
16
+
17
+ [**Docs**](https://mundobien2025.github.io/fastapi-basekit) ·
18
+ [**PyPI**](https://pypi.org/project/fastapi-basekit/) ·
19
+ [**Issues**](https://github.com/mundobien2025/fastapi-basekit/issues) ·
20
+ [**Changelog**](./CHANGELOG.md)
15
21
 
16
22
  </div>
17
23
 
@@ -41,7 +47,7 @@ Abre http://localhost:8000/docs. Login con `admin@example.com` / `ChangeMe2026!`
41
47
 
42
48
  ## Documentación
43
49
 
44
- 📚 **[mundobien2025.github.io/fastapi-basekit](https://mundobien2025.github.io/fastapi-basekit)**
50
+ **[mundobien2025.github.io/fastapi-basekit](https://mundobien2025.github.io/fastapi-basekit)**
45
51
 
46
52
  | Sección | Contenido |
47
53
  |---|---|
@@ -2,9 +2,9 @@ from typing import Any, Dict, List, Optional, Type, Union
2
2
  from typing import get_args, get_origin
3
3
  import re
4
4
 
5
- from bson import ObjectId, Link
5
+ from bson import ObjectId
6
6
  from pydantic import BaseModel
7
- from beanie import Document
7
+ from beanie import Document, Link
8
8
  from beanie.odm.queries.find import FindMany
9
9
  from beanie.operators import Or, RegEx
10
10
 
@@ -154,18 +154,42 @@ class BaseRepository:
154
154
 
155
155
  raw_filters: Dict[str, Any] = {}
156
156
 
157
+ def _coerce_objectid(value: Any) -> Any:
158
+ """str/UUID → ObjectId para queries Mongo sobre Link.id."""
159
+ if isinstance(value, ObjectId):
160
+ return value
161
+ if isinstance(value, str):
162
+ try:
163
+ return ObjectId(value)
164
+ except Exception:
165
+ return value
166
+ return value
167
+
157
168
  for k, v in (filters or {}).items():
158
169
  # MongoDB-style keys (dot-notation like "user.$id" or operators like "$or")
159
170
  # cannot be resolved via hasattr — pass them as a raw dict to find()
160
171
  if "." in k or k.startswith("$"):
161
172
  raw_filters[k] = v
162
- elif hasattr(self.model, k):
163
- field_attr = getattr(self.model, k)
173
+ continue
164
174
 
175
+ if hasattr(self.model, k):
176
+ field_attr = getattr(self.model, k)
165
177
  if _is_link_field(k):
166
- exprs.append(field_attr.id == v)
178
+ exprs.append(field_attr.id == _coerce_objectid(v))
167
179
  else:
168
180
  exprs.append(field_attr == v)
181
+ continue
182
+
183
+ # `<field>_id` alias para Link fields. Permite filtros con
184
+ # `customer_id`, `user_id`, etc. sin que el caller deba conocer
185
+ # la sintaxis Mongo nested. Sólo se activa si <field> existe en
186
+ # el modelo y es un Link[X].
187
+ if k.endswith("_id"):
188
+ base = k[:-3]
189
+ if hasattr(self.model, base) and _is_link_field(base):
190
+ field_attr = getattr(self.model, base)
191
+ exprs.append(field_attr.id == _coerce_objectid(v))
192
+ continue
169
193
 
170
194
  # Raw MongoDB filters go first so Beanie processes them as a dict condition
171
195
  query_args: list = ([raw_filters] if raw_filters else []) + exprs
@@ -183,27 +207,42 @@ class BaseRepository:
183
207
  # Apply ordering if provided and not already applied
184
208
  if order_by:
185
209
  query = query.sort(order_by)
186
-
210
+
187
211
  total = await query.count()
188
212
  items = await query.skip(count * (page - 1)).limit(count).to_list()
189
213
  return items, total
190
-
191
- async def list_with_aggregation(
214
+
215
+ def build_list_queryset(
216
+ self,
217
+ search: Optional[str] = None,
218
+ search_fields: Optional[List[str]] = None,
219
+ filters: Optional[dict] = None,
220
+ order_by: Optional[List[tuple]] = None,
221
+ **kwargs,
222
+ ) -> FindMany[Document]:
223
+ """Hook: returns the FindMany query used by `list` endpoints.
224
+
225
+ Beanie equivalent of SQLAlchemy `build_list_queryset`. Override at
226
+ repository OR service level (`BaseService.build_list_queryset`) to
227
+ customize filters, projections, or query options before pagination.
228
+ Default implementation delegates to `build_filter_query`.
229
+ """
230
+ return self.build_filter_query(
231
+ search=search,
232
+ search_fields=search_fields or [],
233
+ filters=filters or {},
234
+ order_by=order_by,
235
+ **kwargs,
236
+ )
237
+
238
+ def _build_match_stage(
192
239
  self,
193
240
  search: Optional[str],
194
- search_fields: List[str],
195
- filters: dict,
196
- order_by: str,
197
- page: int,
198
- count: int,
199
- **kwargs
200
- ) -> tuple[List[Document], int]:
201
- """List with support for nested ordering using aggregation ($facet optimized)."""
202
- field_path, direction, is_nested = self._parse_order_field(order_by)
203
- pipeline = []
204
-
205
- # 1. Match stage (same as before)
206
- match_conditions = {}
241
+ search_fields: Optional[List[str]],
242
+ filters: Optional[dict],
243
+ ) -> Dict[str, Any]:
244
+ """Build a `$match` stage dict (without the `$match` wrapper)."""
245
+ match_conditions: Dict[str, Any] = {}
207
246
  if filters:
208
247
  for key, value in filters.items():
209
248
  if isinstance(value, ObjectId):
@@ -212,7 +251,7 @@ class BaseRepository:
212
251
  match_conditions[f"{key}.$id"] = value.id
213
252
  else:
214
253
  match_conditions[key] = value
215
-
254
+
216
255
  if search and search_fields:
217
256
  search_conditions = [
218
257
  {field: {"$regex": f".*{search}.*", "$options": "i"}}
@@ -220,22 +259,47 @@ class BaseRepository:
220
259
  ]
221
260
  if search_conditions:
222
261
  if match_conditions:
223
- match_conditions = {"$and": [match_conditions, {"$or": search_conditions}]}
262
+ match_conditions = {
263
+ "$and": [
264
+ match_conditions,
265
+ {"$or": search_conditions},
266
+ ]
267
+ }
224
268
  else:
225
269
  match_conditions = {"$or": search_conditions}
226
-
270
+ return match_conditions
271
+
272
+ def build_list_pipeline(
273
+ self,
274
+ search: Optional[str] = None,
275
+ search_fields: Optional[List[str]] = None,
276
+ filters: Optional[dict] = None,
277
+ order_by: Optional[str] = None,
278
+ **kwargs,
279
+ ) -> List[Dict[str, Any]]:
280
+ """Hook: returns the aggregation pipeline used by `list` endpoints.
281
+
282
+ Beanie equivalent of SQL subqueries / JOINs. Override at repository
283
+ OR service level (`BaseService.build_list_pipeline`) to add `$lookup`,
284
+ `$project`, `$group`, etc. Default builds `$match` + optional `$sort`
285
+ (with auto `$lookup` for nested-link ordering). The `$facet` pagination
286
+ stage is appended later by `paginate_pipeline`.
287
+ """
288
+ pipeline: List[Dict[str, Any]] = []
289
+
290
+ match_conditions = self._build_match_stage(search, search_fields, filters)
227
291
  if match_conditions:
228
292
  pipeline.append({"$match": match_conditions})
229
-
230
- # 2. Lookup & Sort (same as before)
231
- collection_name = None
232
- first_field = None
233
-
293
+
294
+ if not order_by:
295
+ return pipeline
296
+
297
+ field_path, direction, is_nested = self._parse_order_field(order_by)
298
+
234
299
  if is_nested:
235
300
  parts = field_path.split(".")
236
301
  first_field = parts[0]
237
302
  collection_name = self._get_collection_name_from_field(first_field)
238
-
239
303
  if collection_name:
240
304
  pipeline.extend([
241
305
  {
@@ -243,65 +307,105 @@ class BaseRepository:
243
307
  "from": collection_name,
244
308
  "localField": f"{first_field}.$id",
245
309
  "foreignField": "_id",
246
- "as": f"{first_field}_data"
310
+ "as": f"{first_field}_data",
247
311
  }
248
312
  },
249
313
  {
250
314
  "$unwind": {
251
315
  "path": f"${first_field}_data",
252
- "preserveNullAndEmptyArrays": True
316
+ "preserveNullAndEmptyArrays": True,
253
317
  }
254
- }
318
+ },
255
319
  ])
256
320
  remaining_path = ".".join(parts[1:]) if len(parts) > 1 else ""
257
- sort_field = f"{first_field}_data.{remaining_path}" if remaining_path else f"{first_field}_data"
321
+ sort_field = (
322
+ f"{first_field}_data.{remaining_path}"
323
+ if remaining_path
324
+ else f"{first_field}_data"
325
+ )
258
326
  else:
259
327
  sort_field = field_path
260
328
  else:
261
329
  sort_field = field_path
262
-
330
+
263
331
  pipeline.append({"$sort": {sort_field: direction}})
264
-
265
- # 3. Facet for Single Query Pagination
266
- project_exclusion = {f"{first_field}_data": 0} if (is_nested and collection_name) else None
267
-
268
- facet_stage = {
269
- "$facet": {
270
- "metadata": [{"$count": "total"}],
271
- "data": [
272
- {"$skip": count * (page - 1)},
273
- {"$limit": count}
274
- ]
332
+ return pipeline
333
+
334
+ async def paginate_pipeline(
335
+ self,
336
+ pipeline: List[Dict[str, Any]],
337
+ page: int,
338
+ count: int,
339
+ validate: bool = True,
340
+ ) -> tuple[List[Any], int]:
341
+ """Run an aggregation pipeline with `$facet` pagination.
342
+
343
+ Args:
344
+ pipeline: Pipeline stages (without the final `$facet`).
345
+ page, count: Pagination params.
346
+ validate: If True, validates each row against `self.model`.
347
+ Set False when the pipeline projects a non-model shape (e.g.
348
+ joined columns) — the raw dicts are returned untouched.
349
+ """
350
+ full_pipeline = list(pipeline) + [
351
+ {
352
+ "$facet": {
353
+ "metadata": [{"$count": "total"}],
354
+ "data": [
355
+ {"$skip": count * (page - 1)},
356
+ {"$limit": count},
357
+ ],
358
+ }
275
359
  }
276
- }
277
-
278
- # Add projection to remove join artifacts if needed
279
- if project_exclusion:
280
- facet_stage["$facet"]["data"].append({"$project": project_exclusion})
281
-
282
- pipeline.append(facet_stage)
283
-
284
- # Execute Pipeline
285
- results = await self.model.aggregate(pipeline).to_list()
286
-
287
- # Process Results
360
+ ]
361
+
362
+ results = await self.model.aggregate(full_pipeline).to_list()
288
363
  if not results or not results[0].get("metadata"):
289
364
  return [], 0
290
-
365
+
291
366
  data = results[0]
292
367
  total = data["metadata"][0]["total"] if data["metadata"] else 0
293
368
  items_raw = data["data"]
294
-
295
- # Efficient Validation
296
- items = []
369
+
370
+ if not validate:
371
+ return items_raw, total
372
+
373
+ items: List[Any] = []
297
374
  for raw_item in items_raw:
298
375
  try:
299
376
  items.append(self.model.model_validate(raw_item))
300
377
  except Exception:
301
378
  continue
302
-
303
379
  return items, total
304
380
 
381
+ async def list_with_aggregation(
382
+ self,
383
+ search: Optional[str],
384
+ search_fields: List[str],
385
+ filters: dict,
386
+ order_by: str,
387
+ page: int,
388
+ count: int,
389
+ **kwargs,
390
+ ) -> tuple[List[Document], int]:
391
+ """Backward-compat wrapper: delegates to `build_list_pipeline` +
392
+ `paginate_pipeline`. Kept for callers that hit it directly.
393
+ """
394
+ pipeline = self.build_list_pipeline(
395
+ search=search,
396
+ search_fields=search_fields,
397
+ filters=filters,
398
+ order_by=order_by,
399
+ **kwargs,
400
+ )
401
+ # Strip join artifacts when default sort-lookup added them
402
+ field_path, _, is_nested = self._parse_order_field(order_by) if order_by else ("", 0, False)
403
+ if is_nested:
404
+ first_field = field_path.split(".")[0]
405
+ if self._get_collection_name_from_field(first_field):
406
+ pipeline.append({"$project": {f"{first_field}_data": 0}})
407
+ return await self.paginate_pipeline(pipeline, page, count, validate=True)
408
+
305
409
  async def get_by_id(
306
410
  self,
307
411
  obj_id: Union[str, ObjectId],
@@ -1,5 +1,7 @@
1
1
  from typing import Any, Dict, List, Optional, Union
2
2
 
3
+ from beanie import Document
4
+ from beanie.odm.queries.find import FindMany
3
5
  from fastapi import Request
4
6
  from pydantic import BaseModel
5
7
 
@@ -20,6 +22,8 @@ class BaseService:
20
22
  kwargs_query: Dict[str, Union[str, int]] = {}
21
23
  action: str = ""
22
24
  order_by: Optional[List[tuple]] = None
25
+ use_aggregation: bool = False
26
+ aggregation_validate: bool = True
23
27
 
24
28
  def __init__(
25
29
  self, repository: BaseRepository, request: Optional[Request] = None
@@ -69,61 +73,108 @@ class BaseService:
69
73
  raise NotFoundException(f"id={id} no encontrado")
70
74
  return obj
71
75
 
76
+ def build_list_queryset(
77
+ self,
78
+ search: Optional[str] = None,
79
+ search_fields: Optional[List[str]] = None,
80
+ filters: Optional[Dict[str, Any]] = None,
81
+ order_by: Optional[List[tuple]] = None,
82
+ **kwargs,
83
+ ) -> FindMany[Document]:
84
+ """Service-level hook over `repository.build_list_queryset`.
85
+
86
+ Override here to compose query options across repositories or to
87
+ decorate the FindMany before pagination.
88
+ """
89
+ return self.repository.build_list_queryset(
90
+ search=search,
91
+ search_fields=search_fields or self.search_fields,
92
+ filters=filters,
93
+ order_by=order_by,
94
+ **kwargs,
95
+ )
96
+
97
+ def build_list_pipeline(
98
+ self,
99
+ search: Optional[str] = None,
100
+ search_fields: Optional[List[str]] = None,
101
+ filters: Optional[Dict[str, Any]] = None,
102
+ order_by: Optional[str] = None,
103
+ **kwargs,
104
+ ) -> List[Dict[str, Any]]:
105
+ """Service-level hook over `repository.build_list_pipeline`.
106
+
107
+ Override here to add `$lookup`, `$project`, `$group`, etc. for
108
+ cross-collection joins (subquery-like). Set `use_aggregation = True`
109
+ on the service to force the pipeline path even without nested order.
110
+ Set `aggregation_validate = False` if the projection produces a
111
+ non-model shape (joined columns / flattened rows).
112
+ """
113
+ return self.repository.build_list_pipeline(
114
+ search=search,
115
+ search_fields=search_fields or self.search_fields,
116
+ filters=filters,
117
+ order_by=order_by,
118
+ **kwargs,
119
+ )
120
+
72
121
  async def list(
73
122
  self,
74
123
  search: Optional[str] = None,
75
124
  page: int = 1,
76
125
  count: int = 25,
77
126
  filters: Optional[Dict[str, Any]] = None,
78
- order_by: Optional[str] = None, # NEW: Dynamic ordering (e.g., "-created_at" or "tool__name")
127
+ order_by: Optional[str] = None, # Dynamic ordering (e.g., "-created_at" or "tool__name")
79
128
  ):
80
129
  kwargs = self.get_kwargs_query()
81
130
  applied_filters = self.get_filters(filters)
82
-
83
- # Determine which ordering to use
131
+
132
+ # Resolve ordering
84
133
  if order_by:
85
- # Dynamic ordering from parameter (takes precedence)
86
134
  order_str = order_by
87
135
  else:
88
- # Fall back to service-level ordering
89
136
  default_order = self.get_order()
90
137
  if default_order:
91
- # Convert list of tuples to string format
92
- # e.g., [("created_at", -1)] -> "-created_at"
93
138
  field, direction = default_order[0]
94
139
  order_str = f"{'-' if direction == -1 else ''}{field}"
95
140
  else:
96
141
  order_str = None
97
-
98
- # Check if we need aggregation (nested field with __ or .)
99
- if order_str and ("__" in order_str or "." in order_str):
100
- # Use aggregation pipeline for nested ordering
101
- return await self.repository.list_with_aggregation(
142
+
143
+ nested_order = bool(order_str and ("__" in order_str or "." in order_str))
144
+ use_pipeline = self.use_aggregation or nested_order
145
+
146
+ if use_pipeline:
147
+ pipeline = self.build_list_pipeline(
102
148
  search=search,
103
149
  search_fields=self.search_fields,
104
150
  filters=applied_filters,
105
151
  order_by=order_str,
106
- page=page,
107
- count=count,
108
152
  **kwargs,
109
153
  )
110
- else:
111
- # Use standard query for simple ordering
112
- order_list = None
113
- if order_str:
114
- # Parse the order string
115
- direction = -1 if order_str.startswith("-") else 1
116
- field = order_str.lstrip("-")
117
- order_list = [(field, direction)]
118
-
119
- query = self.repository.build_filter_query(
120
- search=search,
121
- search_fields=self.search_fields,
122
- filters=applied_filters,
123
- order_by=order_list,
124
- **kwargs,
154
+ return await self.repository.paginate_pipeline(
155
+ pipeline,
156
+ page=page,
157
+ count=count,
158
+ validate=self.aggregation_validate,
125
159
  )
126
- return await self.repository.paginate(query, page, count, order_by=order_list)
160
+
161
+ # FindMany path
162
+ order_list = None
163
+ if order_str:
164
+ direction = -1 if order_str.startswith("-") else 1
165
+ field = order_str.lstrip("-")
166
+ order_list = [(field, direction)]
167
+
168
+ query = self.build_list_queryset(
169
+ search=search,
170
+ search_fields=self.search_fields,
171
+ filters=applied_filters,
172
+ order_by=order_list,
173
+ **kwargs,
174
+ )
175
+ return await self.repository.paginate(
176
+ query, page, count, order_by=order_list
177
+ )
127
178
 
128
179
  async def create(
129
180
  self, payload: BaseModel, check_fields: Optional[List[str]] = None
@@ -1,6 +1,7 @@
1
- __all__ = ["repository", "service", "controller"]
2
1
  from .controller.base import SQLAlchemyBaseController
2
+ from .session import make_session_lifecycle
3
3
 
4
4
  __all__ = [
5
5
  "SQLAlchemyBaseController",
6
+ "make_session_lifecycle",
6
7
  ]
@@ -10,7 +10,14 @@ from ....exceptions.api_exceptions import (
10
10
 
11
11
 
12
12
  class BaseService:
13
- """Servicio base para SQLAlchemy AsyncSession."""
13
+ """Servicio base para SQLAlchemy AsyncSession.
14
+
15
+ Regla del proyecto: los servicios NO deben llamar `session.flush()`,
16
+ `session.commit()` ni `session.refresh()`. El flush vive en
17
+ `BaseRepository.create / update`; el commit/rollback único por
18
+ request lo gestiona el lifecycle creado con
19
+ `fastapi_basekit.aio.sqlalchemy.make_session_lifecycle`.
20
+ """
14
21
 
15
22
  repository: BaseRepository
16
23
  search_fields: List[str] = []