fastapi-basekit 0.3.3__tar.gz → 0.4.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 (125) hide show
  1. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/PKG-INFO +17 -7
  2. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/README.md +16 -6
  3. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/controller/base.py +21 -2
  4. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py +1 -0
  5. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/repository/base.py +63 -13
  6. fastapi_basekit-0.4.0/fastapi_basekit/exceptions/__init__.py +4 -0
  7. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/handler.py +86 -0
  8. fastapi_basekit-0.4.0/fastapi_basekit/openapi.py +89 -0
  9. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/base.py +1 -1
  10. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/PKG-INFO +17 -7
  11. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/SOURCES.txt +5 -0
  12. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/pyproject.toml +1 -1
  13. fastapi_basekit-0.4.0/tests/test_action_param_leak.py +35 -0
  14. fastapi_basekit-0.4.0/tests/test_exception_handlers.py +55 -0
  15. fastapi_basekit-0.4.0/tests/test_filter_operators.py +78 -0
  16. fastapi_basekit-0.4.0/tests/test_openapi_simplify.py +38 -0
  17. fastapi_basekit-0.3.3/fastapi_basekit/exceptions/__init__.py +0 -3
  18. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/LICENSE +0 -0
  19. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/__init__.py +0 -0
  20. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/__init__.py +0 -0
  21. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/__init__.py +0 -0
  22. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
  23. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
  24. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
  25. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/base.py +0 -0
  26. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
  27. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/service/base.py +0 -0
  28. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/controller/__init__.py +0 -0
  29. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/permissions/__init__.py +0 -0
  30. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/permissions/base.py +0 -0
  31. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
  32. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
  33. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
  34. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
  35. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/base.py +0 -0
  36. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
  37. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
  38. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
  39. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
  40. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
  41. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
  42. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
  43. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
  44. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/cli/__init__.py +0 -0
  45. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/cli/main.py +0 -0
  46. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
  47. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/domain.py +0 -0
  48. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/__init__.py +0 -0
  49. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/jwt.py +0 -0
  50. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/schema.py +0 -0
  51. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/__init__.py +0 -0
  52. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
  53. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
  54. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
  55. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
  56. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
  57. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
  58. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
  59. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
  60. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
  61. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
  62. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
  63. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
  64. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
  65. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
  66. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
  67. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
  68. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
  69. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/__init__.py +0 -0
  70. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
  71. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
  72. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
  73. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
  74. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
  75. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
  76. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
  77. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
  78. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
  79. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
  80. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
  81. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
  82. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
  83. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
  84. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
  85. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
  86. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
  87. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
  88. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
  89. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
  90. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
  91. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
  92. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
  93. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
  94. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
  95. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
  96. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
  97. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
  98. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
  99. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
  100. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
  101. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
  102. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
  103. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
  104. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
  105. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
  106. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
  107. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
  108. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
  109. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
  110. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/entry_points.txt +0 -0
  111. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/requires.txt +0 -0
  112. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/top_level.txt +0 -0
  113. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/setup.cfg +0 -0
  114. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_api_exceptions.py +0 -0
  115. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_base_response.py +0 -0
  116. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_base_service.py +0 -0
  117. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_beanie_aggregation_hooks.py +0 -0
  118. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_beanie_aggregation_integration.py +0 -0
  119. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_controller_auto_permissions.py +0 -0
  120. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_crud_beanie_controller.py +0 -0
  121. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_crud_controller.py +0 -0
  122. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_crud_sqlmodel_repository_service.py +0 -0
  123. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_jwt_service.py +0 -0
  124. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_sql_queryset_override.py +0 -0
  125. {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_sqlalchemy_base_service_order.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-basekit
3
- Version: 0.3.3
3
+ Version: 0.4.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
@@ -105,13 +105,25 @@ Abre http://localhost:8000/docs. Login con `admin@example.com` / `ChangeMe2026!`
105
105
 
106
106
  ## Como plugin de Claude Code
107
107
 
108
+ Dentro de Claude Code (terminal) corre:
109
+
108
110
  ```bash
109
- /plugin marketplace add https://github.com/mundobien2025/fastapi-basekit
110
- /plugin install fastapi-basekit
111
- /plugin list
111
+ /plugin marketplace add mundobien2025/fastapi-basekit
112
+ /plugin install fastapi-basekit@fastapi-basekit
113
+ /plugin list # verifica instalación
112
114
  ```
113
115
 
114
- Luego pide: *"Crea el recurso `Invoice` con CRUD completo"* Claude usa la skill automáticamente.
116
+ Eso registra el marketplace `fastapi-basekit` desde el repo de GitHub e
117
+ instala el plugin (que incluye la skill `fastapi-basekit-crud`). Luego
118
+ pide: *"Crea el recurso `Invoice` con CRUD completo siguiendo
119
+ fastapi-basekit"* — Claude detecta la skill y aplica el patrón canónico
120
+ (model → repo → service → schema → controller con `@cbv`).
121
+
122
+ Para actualizar a una versión nueva:
123
+
124
+ ```bash
125
+ /plugin update fastapi-basekit@fastapi-basekit
126
+ ```
115
127
 
116
128
  ## Instalación por ORM
117
129
 
@@ -127,8 +139,6 @@ pip install fastapi-basekit[all] # todo
127
139
 
128
140
  PRs bienvenidos. Setup local en [docs/contributing](https://mundobien2025.github.io/fastapi-basekit/contributing).
129
141
 
130
- Mantenedores: [`RELEASING.md`](./RELEASING.md) tiene release flow, CI/CD, mike y troubleshooting.
131
-
132
142
  ## Licencia
133
143
 
134
144
  [MIT](./LICENSE) — © Jerson Moreno
@@ -59,13 +59,25 @@ Abre http://localhost:8000/docs. Login con `admin@example.com` / `ChangeMe2026!`
59
59
 
60
60
  ## Como plugin de Claude Code
61
61
 
62
+ Dentro de Claude Code (terminal) corre:
63
+
62
64
  ```bash
63
- /plugin marketplace add https://github.com/mundobien2025/fastapi-basekit
64
- /plugin install fastapi-basekit
65
- /plugin list
65
+ /plugin marketplace add mundobien2025/fastapi-basekit
66
+ /plugin install fastapi-basekit@fastapi-basekit
67
+ /plugin list # verifica instalación
66
68
  ```
67
69
 
68
- Luego pide: *"Crea el recurso `Invoice` con CRUD completo"* Claude usa la skill automáticamente.
70
+ Eso registra el marketplace `fastapi-basekit` desde el repo de GitHub e
71
+ instala el plugin (que incluye la skill `fastapi-basekit-crud`). Luego
72
+ pide: *"Crea el recurso `Invoice` con CRUD completo siguiendo
73
+ fastapi-basekit"* — Claude detecta la skill y aplica el patrón canónico
74
+ (model → repo → service → schema → controller con `@cbv`).
75
+
76
+ Para actualizar a una versión nueva:
77
+
78
+ ```bash
79
+ /plugin update fastapi-basekit@fastapi-basekit
80
+ ```
69
81
 
70
82
  ## Instalación por ORM
71
83
 
@@ -81,8 +93,6 @@ pip install fastapi-basekit[all] # todo
81
93
 
82
94
  PRs bienvenidos. Setup local en [docs/contributing](https://mundobien2025.github.io/fastapi-basekit/contributing).
83
95
 
84
- Mantenedores: [`RELEASING.md`](./RELEASING.md) tiene release flow, CI/CD, mike y troubleshooting.
85
-
86
96
  ## Licencia
87
97
 
88
98
  [MIT](./LICENSE) — © Jerson Moreno
@@ -18,7 +18,11 @@ class BaseController:
18
18
  # DRF Style: Permisos globales por defecto
19
19
  permission_classes: ClassVar[List[Type[BasePermission]]] = []
20
20
 
21
- action: Optional[str] = None
21
+ # ClassVar so cbv (fastapi-restful) does NOT promote it to a query param.
22
+ # A plain `Optional[str]` annotation here leaks a spurious `action` query
23
+ # parameter onto every mounted endpoint. `prepare_action` still assigns
24
+ # `self.action` (instance attr) at runtime, which works fine.
25
+ action: ClassVar[Optional[str]] = None
22
26
  request: Request
23
27
  _params_excluded_fields: ClassVar[Set[str]] = {
24
28
  "self",
@@ -26,6 +30,7 @@ class BaseController:
26
30
  "count",
27
31
  "search",
28
32
  "order_by",
33
+ "action",
29
34
  "__class__",
30
35
  "args",
31
36
  "kwargs",
@@ -66,6 +71,17 @@ class BaseController:
66
71
  return
67
72
  self.action = action_name
68
73
  self._basekit_prepared_action = action_name
74
+ # Propagate the canonical CRUD action ("list"/"retrieve"/...) to the
75
+ # service so `get_kwargs_query` / `get_filters` can branch on it. The
76
+ # service's own constructor derives `action` from the endpoint
77
+ # function name (e.g. "list_users"), which is unreliable for that
78
+ # purpose; the controller knows the canonical action.
79
+ service = getattr(self, "service", None)
80
+ if service is not None:
81
+ try:
82
+ service.action = action_name
83
+ except Exception:
84
+ pass
69
85
  await self.check_permissions()
70
86
 
71
87
  async def check_permissions(self):
@@ -257,7 +273,10 @@ class BaseController:
257
273
  search = final_value
258
274
  elif param_name == "order_by":
259
275
  order_by = final_value
260
- elif param_name not in standard_params:
276
+ elif (
277
+ param_name not in standard_params
278
+ and param_name not in self._params_excluded_fields
279
+ ):
261
280
  # Es un filtro
262
281
  filters[param_name] = final_value
263
282
 
@@ -24,6 +24,7 @@ class SQLAlchemyBaseController(BaseController):
24
24
  "use_or",
25
25
  "joins",
26
26
  "order_by",
27
+ "action",
27
28
  "__class__",
28
29
  "args",
29
30
  "kwargs",
@@ -131,7 +131,13 @@ class BaseRepository:
131
131
  joins_to_apply = {}
132
132
 
133
133
  for filter_path, value in filters.items():
134
- attr, field_joins = self._resolve_field_path(filter_path)
134
+ # Detect an optional trailing operator suffix (e.g.
135
+ # "age__gte", "created_at__lte", "name__ilike", "id__in").
136
+ # The remaining path is resolved normally, so relationship
137
+ # traversal ("user__role__code") keeps working — only a final
138
+ # token that matches a known operator is treated as one.
139
+ resolve_path, op = self._split_operator(filter_path)
140
+ attr, field_joins = self._resolve_field_path(resolve_path)
135
141
 
136
142
  if attr is None:
137
143
  continue
@@ -156,10 +162,53 @@ class BaseRepository:
156
162
  )
157
163
 
158
164
  if not is_relationship and is_column:
159
- resolved_filters[filter_path] = (attr, processed_value)
165
+ resolved_filters[filter_path] = (attr, processed_value, op)
160
166
 
161
167
  return resolved_filters, joins_to_apply
162
168
 
169
+ # Operadores de filtro soportados como sufijo "campo__op".
170
+ FILTER_OPERATORS = frozenset(
171
+ {"eq", "ne", "gt", "gte", "lt", "lte", "in", "like", "ilike"}
172
+ )
173
+
174
+ def _split_operator(self, filter_path: str) -> Tuple[str, str]:
175
+ """Separa un sufijo de operador del path de filtro.
176
+
177
+ "age__gte" → ("age", "gte"); "user__role__code" → (..., "eq").
178
+ Solo se interpreta como operador si el último token coincide con
179
+ ``FILTER_OPERATORS`` y queda al menos un token de campo delante.
180
+ """
181
+ if "__" in filter_path:
182
+ head, _, last = filter_path.rpartition("__")
183
+ if head and last in self.FILTER_OPERATORS:
184
+ return head, last
185
+ return filter_path, "eq"
186
+
187
+ @staticmethod
188
+ def _condition_for(field: Any, value: Any, op: str) -> Any:
189
+ """Construye la condición SQLAlchemy para un operador dado."""
190
+ if op == "ne":
191
+ return field != value
192
+ if op == "gt":
193
+ return field > value
194
+ if op == "gte":
195
+ return field >= value
196
+ if op == "lt":
197
+ return field < value
198
+ if op == "lte":
199
+ return field <= value
200
+ if op == "in":
201
+ seq = value if isinstance(value, (list, tuple, set)) else [value]
202
+ return field.in_(list(seq))
203
+ if op == "like":
204
+ return field.like(f"%{value}%")
205
+ if op == "ilike":
206
+ return field.ilike(f"%{value}%")
207
+ # op == "eq" (default): conserva la semántica IN para listas.
208
+ if isinstance(value, (list, tuple)) and not isinstance(value, str):
209
+ return field.in_(value)
210
+ return field == value
211
+
163
212
  def _resolve_order_by(
164
213
  self,
165
214
  order_by: Optional[Any] = None,
@@ -292,14 +341,13 @@ class BaseRepository:
292
341
 
293
342
  # Modo avanzado: usar resolved_filters si está disponible
294
343
  if resolved_filters is not None:
295
- for _, (field, processed_value) in resolved_filters.items():
296
- # Aplicar la lógica de IN o ==
297
- if isinstance(
298
- processed_value, (list, tuple)
299
- ) and not isinstance(processed_value, str):
300
- conditions.append(field.in_(processed_value))
301
- else:
302
- conditions.append(field == processed_value)
344
+ for _, resolved in resolved_filters.items():
345
+ # Tupla (field, value) legacy o (field, value, op) con operador.
346
+ field, processed_value = resolved[0], resolved[1]
347
+ op = resolved[2] if len(resolved) > 2 else "eq"
348
+ conditions.append(
349
+ self._condition_for(field, processed_value, op)
350
+ )
303
351
  # Modo legacy: compatibilidad con código existente
304
352
  elif filters is not None:
305
353
  for fn, value in filters.items():
@@ -442,8 +490,10 @@ class BaseRepository:
442
490
  if not resolved_filters:
443
491
  return None
444
492
 
445
- # Obtener el atributo resuelto
446
- _, (field, processed_value) = next(iter(resolved_filters.items()))
493
+ # Obtener el atributo resuelto (puede traer operador como 3er elem)
494
+ _, resolved = next(iter(resolved_filters.items()))
495
+ field, processed_value = resolved[0], resolved[1]
496
+ op = resolved[2] if len(resolved) > 2 else "eq"
447
497
 
448
498
  # Construir query con JOINs de filtrado
449
499
  query = select(self.model)
@@ -453,7 +503,7 @@ class BaseRepository:
453
503
  query = query.join(relation_attr)
454
504
 
455
505
  # Aplicar condición
456
- query = query.where(field == processed_value)
506
+ query = query.where(self._condition_for(field, processed_value, op))
457
507
 
458
508
  # Aplicar joins de carga
459
509
  query = self._apply_joins(query, joins)
@@ -0,0 +1,4 @@
1
+ from fastapi_basekit.exceptions.domain import DomainError
2
+ from fastapi_basekit.exceptions.handler import register_exception_handlers
3
+
4
+ __all__ = ["DomainError", "register_exception_handlers"]
@@ -109,3 +109,89 @@ async def value_exception_handler(
109
109
  status_code=status.HTTP_400_BAD_REQUEST,
110
110
  content=response.model_dump(mode="json"),
111
111
  )
112
+
113
+
114
+ async def request_validation_handler(
115
+ request: Request, exc: RequestValidationError
116
+ ):
117
+ """FastAPI request validation → envelope unificado (no relanza).
118
+
119
+ Equivalente a `validation_exception_handler` pero devuelve la respuesta
120
+ directamente, sin depender de que otro handler capture la excepción
121
+ relanzada. Cuerpo idéntico al que produciría `ValidationException`.
122
+ """
123
+ response = BaseResponse(
124
+ status="VALIDATION_ERROR",
125
+ message="Error de validación",
126
+ data=exc.errors(),
127
+ )
128
+ return JSONResponse(
129
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
130
+ content=response.model_dump(mode="json"),
131
+ )
132
+
133
+
134
+ async def integrity_error_handler(request: Request, exc: Exception):
135
+ """SQLAlchemy IntegrityError → envelope de integridad (HTTP 400)."""
136
+ detail = getattr(exc, "orig", None)
137
+ response = BaseResponse(
138
+ status="DATABASE_INTEGRITY_ERROR",
139
+ message="Registro ya existe o viola una restricción de integridad",
140
+ data={"detail": str(detail) if detail is not None else str(exc)},
141
+ )
142
+ return JSONResponse(
143
+ status_code=status.HTTP_400_BAD_REQUEST,
144
+ content=response.model_dump(mode="json"),
145
+ )
146
+
147
+
148
+ def register_exception_handlers(
149
+ app,
150
+ *,
151
+ sqlalchemy: bool = True,
152
+ mongo: bool = True,
153
+ ) -> None:
154
+ """Registra el conjunto completo de handlers sobre una app FastAPI.
155
+
156
+ Un solo punto de cableado para que cada proyecto NO copie-pegue el
157
+ mapeo de excepciones → ``BaseResponse``. Todas las subclases de
158
+ ``APIException`` (NotFound, Permission, JWT, Validation, Integrity,
159
+ Global...) las atiende ``api_exception_handler`` vía resolución por MRO.
160
+
161
+ Args:
162
+ app: instancia FastAPI.
163
+ sqlalchemy: registra el handler de ``IntegrityError`` (requiere
164
+ SQLAlchemy instalado).
165
+ mongo: registra handlers de Mongo/Beanie si están disponibles.
166
+ """
167
+ app.add_exception_handler(APIException, api_exception_handler)
168
+ app.add_exception_handler(
169
+ RequestValidationError, request_validation_handler
170
+ )
171
+ app.add_exception_handler(ValidationError, value_exception_handler)
172
+ app.add_exception_handler(ValueError, value_exception_handler)
173
+
174
+ if sqlalchemy:
175
+ try: # pragma: no cover - dependencia opcional
176
+ from sqlalchemy.exc import IntegrityError
177
+
178
+ app.add_exception_handler(
179
+ IntegrityError, integrity_error_handler
180
+ )
181
+ except ImportError:
182
+ pass
183
+
184
+ if mongo:
185
+ # Solo registra si las clases reales (no los fallbacks) existen.
186
+ if DuplicateKeyError.__module__ != __name__:
187
+ app.add_exception_handler(
188
+ DuplicateKeyError, duplicate_key_exception_handler
189
+ )
190
+ if DocumentNotFound.__module__ != __name__:
191
+ app.add_exception_handler(
192
+ DocumentNotFound, document_not_found_handler
193
+ )
194
+
195
+ # Catch-all al final; las subclases de APIException ya tienen prioridad
196
+ # por especificidad de tipo.
197
+ app.add_exception_handler(Exception, global_exception_handler)
@@ -0,0 +1,89 @@
1
+ """Helpers para limpiar el esquema OpenAPI generado.
2
+
3
+ Con controllers `@cbv`, FastAPI genera ``operationId`` largos y ruidosos
4
+ (p. ej. ``UserController_list_users_users__get``) y, según la versión de
5
+ fastapi-restful, summaries con prefijo de clase (``UserController.list_users``).
6
+ ``simplify_openapi`` normaliza ambos sin acoplarse a nombres de recurso de un
7
+ proyecto concreto: el ``operationId`` pasa a ser el nombre del método de ruta
8
+ (``list_users``) y el ``summary`` se vuelve legible.
9
+ """
10
+
11
+ import re
12
+ from typing import Any, Callable, Dict, Optional
13
+
14
+ from fastapi.openapi.utils import get_openapi
15
+ from fastapi.routing import APIRoute
16
+
17
+ # Prefijo de clase cbv en summaries con punto: "UserController.list_users".
18
+ _CBV_DOT_PREFIX = re.compile(r"^[A-Za-z0-9_]+\.(.+)$")
19
+
20
+ _HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head"}
21
+
22
+
23
+ def _clean_label(value: str) -> str:
24
+ """Quita el prefijo de clase cbv (forma con punto) y formatea legible."""
25
+ match = _CBV_DOT_PREFIX.match(value)
26
+ tail = match.group(1) if match else value
27
+ return tail.replace("_", " ").strip().capitalize()
28
+
29
+
30
+ def simplify_openapi(
31
+ app,
32
+ *,
33
+ title: Optional[str] = None,
34
+ version: Optional[str] = None,
35
+ description: Optional[str] = None,
36
+ summary_overrides: Optional[Dict[str, str]] = None,
37
+ ) -> Callable[[], Dict[str, Any]]:
38
+ """Instala un ``app.openapi`` con operationIds/summaries limpios.
39
+
40
+ El ``operationId`` de cada ruta pasa a ser su ``name`` (el nombre del
41
+ método, p. ej. ``list_users``), de modo que ``summary_overrides`` se
42
+ indexa por ese nombre simple.
43
+
44
+ Args:
45
+ app: instancia FastAPI.
46
+ title/version/description: overrides opcionales de metadatos.
47
+ summary_overrides: mapa ``operationId -> summary`` (clave = nombre de
48
+ método de ruta). Lo específico del proyecto vive aquí, no en la
49
+ librería.
50
+
51
+ Returns:
52
+ La función ``custom_openapi`` instalada.
53
+ """
54
+ overrides = summary_overrides or {}
55
+
56
+ # operationId = nombre del método de ruta SIN el prefijo de clase cbv
57
+ # ("UserController.list_users" → "list_users"). Idempotente.
58
+ for route in app.routes:
59
+ if isinstance(route, APIRoute):
60
+ match = _CBV_DOT_PREFIX.match(route.name)
61
+ route.operation_id = match.group(1) if match else route.name
62
+
63
+ def custom_openapi() -> Dict[str, Any]:
64
+ if app.openapi_schema:
65
+ return app.openapi_schema
66
+
67
+ schema = get_openapi(
68
+ title=title or app.title,
69
+ version=version or app.version,
70
+ description=description or app.description,
71
+ routes=app.routes,
72
+ tags=app.openapi_tags,
73
+ )
74
+
75
+ for path_item in schema.get("paths", {}).values():
76
+ for method, operation in path_item.items():
77
+ if method not in _HTTP_METHODS:
78
+ continue
79
+ op_id = operation.get("operationId", "")
80
+ if op_id in overrides:
81
+ operation["summary"] = overrides[op_id]
82
+ elif operation.get("summary"):
83
+ operation["summary"] = _clean_label(operation["summary"])
84
+
85
+ app.openapi_schema = schema
86
+ return schema
87
+
88
+ app.openapi = custom_openapi
89
+ return custom_openapi
@@ -20,6 +20,6 @@ class BasePaginationResponse(BaseModel, Generic[T]):
20
20
  data: List[T]
21
21
  message: str = "Operación exitosa"
22
22
  status: str = "success"
23
- pagination: Optional[Dict[str, Any]]
23
+ pagination: Optional[Dict[str, Any]] = None
24
24
 
25
25
  model_config = ConfigDict(from_attributes=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-basekit
3
- Version: 0.3.3
3
+ Version: 0.4.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
@@ -105,13 +105,25 @@ Abre http://localhost:8000/docs. Login con `admin@example.com` / `ChangeMe2026!`
105
105
 
106
106
  ## Como plugin de Claude Code
107
107
 
108
+ Dentro de Claude Code (terminal) corre:
109
+
108
110
  ```bash
109
- /plugin marketplace add https://github.com/mundobien2025/fastapi-basekit
110
- /plugin install fastapi-basekit
111
- /plugin list
111
+ /plugin marketplace add mundobien2025/fastapi-basekit
112
+ /plugin install fastapi-basekit@fastapi-basekit
113
+ /plugin list # verifica instalación
112
114
  ```
113
115
 
114
- Luego pide: *"Crea el recurso `Invoice` con CRUD completo"* Claude usa la skill automáticamente.
116
+ Eso registra el marketplace `fastapi-basekit` desde el repo de GitHub e
117
+ instala el plugin (que incluye la skill `fastapi-basekit-crud`). Luego
118
+ pide: *"Crea el recurso `Invoice` con CRUD completo siguiendo
119
+ fastapi-basekit"* — Claude detecta la skill y aplica el patrón canónico
120
+ (model → repo → service → schema → controller con `@cbv`).
121
+
122
+ Para actualizar a una versión nueva:
123
+
124
+ ```bash
125
+ /plugin update fastapi-basekit@fastapi-basekit
126
+ ```
115
127
 
116
128
  ## Instalación por ORM
117
129
 
@@ -127,8 +139,6 @@ pip install fastapi-basekit[all] # todo
127
139
 
128
140
  PRs bienvenidos. Setup local en [docs/contributing](https://mundobien2025.github.io/fastapi-basekit/contributing).
129
141
 
130
- Mantenedores: [`RELEASING.md`](./RELEASING.md) tiene release flow, CI/CD, mike y troubleshooting.
131
-
132
142
  ## Licencia
133
143
 
134
144
  [MIT](./LICENSE) — © Jerson Moreno
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  fastapi_basekit/__init__.py
5
+ fastapi_basekit/openapi.py
5
6
  fastapi_basekit.egg-info/PKG-INFO
6
7
  fastapi_basekit.egg-info/SOURCES.txt
7
8
  fastapi_basekit.egg-info/dependency_links.txt
@@ -103,6 +104,7 @@ fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/use
103
104
  fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py
104
105
  fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py
105
106
  fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py
107
+ tests/test_action_param_leak.py
106
108
  tests/test_api_exceptions.py
107
109
  tests/test_base_response.py
108
110
  tests/test_base_service.py
@@ -112,6 +114,9 @@ tests/test_controller_auto_permissions.py
112
114
  tests/test_crud_beanie_controller.py
113
115
  tests/test_crud_controller.py
114
116
  tests/test_crud_sqlmodel_repository_service.py
117
+ tests/test_exception_handlers.py
118
+ tests/test_filter_operators.py
115
119
  tests/test_jwt_service.py
120
+ tests/test_openapi_simplify.py
116
121
  tests/test_sql_queryset_override.py
117
122
  tests/test_sqlalchemy_base_service_order.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-basekit"
7
- version = "0.3.3"
7
+ version = "0.4.0"
8
8
  description = "Utilities and base classes for FastAPI async projects (Beanie, SQLAlchemy or SQLModel)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,35 @@
1
+ """Regresión: cbv NO debe exponer `action` como query param.
2
+
3
+ `BaseController.action` se declara `ClassVar` precisamente para que
4
+ fastapi-restful (cbv) no lo promueva a parámetro de query en cada ruta
5
+ montada. Antes de 0.4.0 aparecía un `?action=` espurio en toda mutación.
6
+ """
7
+
8
+ from fastapi import FastAPI
9
+
10
+ from tests.example_crud.controller import router
11
+
12
+
13
+ def _spec():
14
+ app = FastAPI()
15
+ app.include_router(router)
16
+ return app.openapi()
17
+
18
+
19
+ def test_no_spurious_action_query_param_on_any_route():
20
+ spec = _spec()
21
+ offending = []
22
+ for path, item in spec["paths"].items():
23
+ for method, op in item.items():
24
+ for param in op.get("parameters", []):
25
+ if param.get("name") == "action" and param.get("in") == "query":
26
+ offending.append(f"{method.upper()} {path}")
27
+ assert offending == [], f"`action` leaked as query param on: {offending}"
28
+
29
+
30
+ def test_declared_query_filters_still_present():
31
+ # Sanidad: los filtros declarados explícitamente sí deben seguir ahí.
32
+ spec = _spec()
33
+ get_list = spec["paths"]["/users/"]["get"]
34
+ names = {p["name"] for p in get_list.get("parameters", [])}
35
+ assert {"page", "count", "search", "is_active", "age_min"} <= names
@@ -0,0 +1,55 @@
1
+ """register_exception_handlers: envelope unificado para toda la app."""
2
+
3
+ import pytest
4
+ from fastapi import FastAPI
5
+ from fastapi.testclient import TestClient
6
+ from pydantic import BaseModel
7
+
8
+ from fastapi_basekit.exceptions import register_exception_handlers
9
+ from fastapi_basekit.exceptions.api_exceptions import NotFoundException
10
+
11
+
12
+ @pytest.fixture
13
+ def client():
14
+ app = FastAPI()
15
+ register_exception_handlers(app)
16
+
17
+ class Body(BaseModel):
18
+ n: int
19
+
20
+ @app.get("/boom-api")
21
+ async def boom_api():
22
+ raise NotFoundException(message="no existe")
23
+
24
+ @app.get("/boom-value")
25
+ async def boom_value():
26
+ raise ValueError("campo malo")
27
+
28
+ @app.post("/echo")
29
+ async def echo(body: Body):
30
+ return {"n": body.n}
31
+
32
+ return TestClient(app, raise_server_exceptions=False)
33
+
34
+
35
+ def test_api_exception_maps_to_envelope(client):
36
+ r = client.get("/boom-api")
37
+ assert r.status_code == 404
38
+ body = r.json()
39
+ assert body["status"] == "NOT_FOUND"
40
+ assert body["message"] == "no existe"
41
+ assert set(body) == {"data", "message", "status"}
42
+
43
+
44
+ def test_request_validation_maps_to_envelope(client):
45
+ r = client.post("/echo", json={"n": "not-an-int"})
46
+ assert r.status_code == 422
47
+ body = r.json()
48
+ assert body["status"] == "VALIDATION_ERROR"
49
+ assert isinstance(body["data"], list)
50
+
51
+
52
+ def test_value_error_maps_to_envelope(client):
53
+ r = client.get("/boom-value")
54
+ assert r.status_code == 400
55
+ assert r.json()["status"] == "VALUE_ERROR"
@@ -0,0 +1,78 @@
1
+ """Operadores de filtro en BaseRepository (sufijo campo__op)."""
2
+
3
+ import uuid
4
+
5
+ from sqlalchemy import String, select
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
7
+
8
+ from fastapi_basekit.aio.sqlalchemy.repository.base import BaseRepository
9
+
10
+
11
+ class Base(DeclarativeBase):
12
+ pass
13
+
14
+
15
+ class Widget(Base):
16
+ __tablename__ = "widgets"
17
+
18
+ id: Mapped[str] = mapped_column(
19
+ String, primary_key=True, default=lambda: str(uuid.uuid4())
20
+ )
21
+ name: Mapped[str] = mapped_column(String)
22
+ qty: Mapped[int] = mapped_column(default=0)
23
+
24
+
25
+ class WidgetRepository(BaseRepository):
26
+ model = Widget
27
+
28
+
29
+ def _repo() -> WidgetRepository:
30
+ return WidgetRepository.__new__(WidgetRepository)
31
+
32
+
33
+ def test_split_operator_detects_known_suffix():
34
+ repo = _repo()
35
+ assert repo._split_operator("qty__gte") == ("qty", "gte")
36
+ assert repo._split_operator("name__ilike") == ("name", "ilike")
37
+ assert repo._split_operator("id__in") == ("id", "in")
38
+
39
+
40
+ def test_split_operator_keeps_relationship_paths():
41
+ repo = _repo()
42
+ # 'code' no es operador → path se conserva, op por defecto eq.
43
+ assert repo._split_operator("user__role__code") == (
44
+ "user__role__code",
45
+ "eq",
46
+ )
47
+ assert repo._split_operator("status") == ("status", "eq")
48
+
49
+
50
+ def _sql(condition) -> str:
51
+ return str(
52
+ select(Widget).where(condition).compile(
53
+ compile_kwargs={"literal_binds": True}
54
+ )
55
+ )
56
+
57
+
58
+ def test_condition_for_each_operator():
59
+ f = Widget.qty
60
+ assert ">=" in _sql(BaseRepository._condition_for(f, 5, "gte"))
61
+ assert ">" in _sql(BaseRepository._condition_for(f, 5, "gt"))
62
+ assert "<=" in _sql(BaseRepository._condition_for(f, 5, "lte"))
63
+ assert "<" in _sql(BaseRepository._condition_for(f, 5, "lt"))
64
+ assert "!=" in _sql(BaseRepository._condition_for(f, 5, "ne"))
65
+ assert "IN" in _sql(BaseRepository._condition_for(f, [1, 2], "in"))
66
+ assert "LIKE" in _sql(
67
+ BaseRepository._condition_for(Widget.name, "ab", "like")
68
+ )
69
+ assert "lower(" in _sql(
70
+ BaseRepository._condition_for(Widget.name, "ab", "ilike")
71
+ ).lower()
72
+
73
+
74
+ def test_condition_for_eq_keeps_list_in_semantics():
75
+ sql = _sql(BaseRepository._condition_for(Widget.qty, [1, 2], "eq"))
76
+ assert "IN" in sql
77
+ sql_scalar = _sql(BaseRepository._condition_for(Widget.qty, 1, "eq"))
78
+ assert "=" in sql_scalar
@@ -0,0 +1,38 @@
1
+ """simplify_openapi: quita el prefijo de clase cbv de summaries/operationIds."""
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from fastapi_basekit.openapi import simplify_openapi
6
+ from tests.example_crud.controller import router
7
+
8
+
9
+ def test_strips_cbv_class_prefix():
10
+ app = FastAPI()
11
+ app.include_router(router)
12
+ simplify_openapi(app)
13
+ spec = app.openapi()
14
+
15
+ for path, item in spec["paths"].items():
16
+ for method, op in item.items():
17
+ op_id = op.get("operationId", "")
18
+ assert "." not in op_id, f"operationId sin limpiar: {op_id}"
19
+ summary = op.get("summary", "")
20
+ # El prefijo "UserController." no debe sobrevivir en el summary.
21
+ assert "Controller." not in summary
22
+
23
+
24
+ def test_summary_override_applies():
25
+ app = FastAPI()
26
+ app.include_router(router)
27
+ # operationId base (ya sin prefijo tras limpiar) para list_users.
28
+ simplify_openapi(app, summary_overrides={"list_users": "Listar usuarios"})
29
+ spec = app.openapi()
30
+ summaries = {
31
+ op.get("operationId"): op.get("summary")
32
+ for item in spec["paths"].values()
33
+ for op in item.values()
34
+ }
35
+ # El override se aplica por operationId original (con prefijo cbv).
36
+ assert "Listar usuarios" in summaries.values() or any(
37
+ s == "Listar usuarios" for s in summaries.values()
38
+ )
@@ -1,3 +0,0 @@
1
- from fastapi_basekit.exceptions.domain import DomainError
2
-
3
- __all__ = ["DomainError"]
File without changes