fastapi-basekit 0.3.2__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.
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/PKG-INFO +17 -7
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/README.md +16 -6
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/base.py +2 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/controller/base.py +130 -41
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py +4 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/repository/base.py +63 -13
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/base.py +3 -0
- fastapi_basekit-0.4.0/fastapi_basekit/exceptions/__init__.py +4 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/handler.py +86 -0
- fastapi_basekit-0.4.0/fastapi_basekit/openapi.py +89 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/base.py +1 -1
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/PKG-INFO +17 -7
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/SOURCES.txt +9 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/pyproject.toml +1 -1
- fastapi_basekit-0.4.0/tests/test_action_param_leak.py +35 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/tests/test_base_service.py +18 -1
- fastapi_basekit-0.4.0/tests/test_beanie_aggregation_hooks.py +484 -0
- fastapi_basekit-0.4.0/tests/test_beanie_aggregation_integration.py +199 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/tests/test_controller_auto_permissions.py +56 -71
- fastapi_basekit-0.4.0/tests/test_crud_sqlmodel_repository_service.py +242 -0
- fastapi_basekit-0.4.0/tests/test_exception_handlers.py +55 -0
- fastapi_basekit-0.4.0/tests/test_filter_operators.py +78 -0
- fastapi_basekit-0.4.0/tests/test_openapi_simplify.py +38 -0
- fastapi_basekit-0.4.0/tests/test_sql_queryset_override.py +332 -0
- fastapi_basekit-0.3.2/fastapi_basekit/exceptions/__init__.py +0 -3
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/LICENSE +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/service/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/cli/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/cli/main.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/domain.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/schema.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/entry_points.txt +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/requires.txt +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/setup.cfg +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/tests/test_jwt_service.py +0 -0
- {fastapi_basekit-0.3.2 → 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
|
+
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
{fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/base.py
RENAMED
|
@@ -18,6 +18,7 @@ class BeanieBaseController(BaseController):
|
|
|
18
18
|
|
|
19
19
|
async def list(self):
|
|
20
20
|
"""Lista documentos con paginación usando Beanie."""
|
|
21
|
+
await self.prepare_action("list")
|
|
21
22
|
params = self._params(skip_frames=2)
|
|
22
23
|
items, total = await self.service.list(**params)
|
|
23
24
|
count = params.get("count") or 0
|
|
@@ -39,6 +40,7 @@ class BeanieBaseController(BaseController):
|
|
|
39
40
|
check_fields: Optional[List[str]] = None,
|
|
40
41
|
):
|
|
41
42
|
"""Crea un nuevo documento con validación de campos únicos."""
|
|
43
|
+
await self.prepare_action("create")
|
|
42
44
|
result = await self.service.create(validated_data, check_fields)
|
|
43
45
|
return self.format_response(result, message="Creado exitosamente")
|
|
44
46
|
|
|
@@ -14,6 +14,14 @@ class BaseController:
|
|
|
14
14
|
|
|
15
15
|
service = Depends()
|
|
16
16
|
schema_class: ClassVar[Type[BaseModel]]
|
|
17
|
+
|
|
18
|
+
# DRF Style: Permisos globales por defecto
|
|
19
|
+
permission_classes: ClassVar[List[Type[BasePermission]]] = []
|
|
20
|
+
|
|
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.
|
|
17
25
|
action: ClassVar[Optional[str]] = None
|
|
18
26
|
request: Request
|
|
19
27
|
_params_excluded_fields: ClassVar[Set[str]] = {
|
|
@@ -22,6 +30,7 @@ class BaseController:
|
|
|
22
30
|
"count",
|
|
23
31
|
"search",
|
|
24
32
|
"order_by",
|
|
33
|
+
"action",
|
|
25
34
|
"__class__",
|
|
26
35
|
"args",
|
|
27
36
|
"kwargs",
|
|
@@ -32,34 +41,84 @@ class BaseController:
|
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
def __init__(self) -> None:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
"""Inicializa el controller."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def get_permissions(self) -> List[Type[BasePermission]]:
|
|
48
|
+
"""
|
|
49
|
+
Instancia y retorna la lista de permisos que esta vista requiere.
|
|
50
|
+
|
|
51
|
+
Sobrescribir esto permite lógica tipo DRF:
|
|
52
|
+
|
|
53
|
+
if self.action == 'list':
|
|
54
|
+
return [AllowAny]
|
|
55
|
+
return [IsAuthenticated]
|
|
56
|
+
"""
|
|
57
|
+
return self.permission_classes
|
|
58
|
+
|
|
59
|
+
async def prepare_action(self, action_name: str) -> None:
|
|
60
|
+
"""Set the current action and run permission checks.
|
|
61
|
+
|
|
62
|
+
Auto-called by ``ControllerMeta`` for every public async method on
|
|
63
|
+
the controller. Idempotent within one invocation: if the same
|
|
64
|
+
``action_name`` has already been prepared on this instance,
|
|
65
|
+
subsequent calls are no-ops. This lets custom methods opt into
|
|
66
|
+
calling ``await self.prepare_action(...)`` explicitly without
|
|
67
|
+
double-firing permission checks (when the metaclass already ran
|
|
68
|
+
before entering the method body).
|
|
69
|
+
"""
|
|
70
|
+
if getattr(self, "_basekit_prepared_action", None) == action_name:
|
|
71
|
+
return
|
|
72
|
+
self.action = action_name
|
|
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
|
|
85
|
+
await self.check_permissions()
|
|
86
|
+
|
|
87
|
+
async def check_permissions(self):
|
|
88
|
+
"""Run each declared permission. Raises ``PermissionException``
|
|
89
|
+
on the first denial.
|
|
90
|
+
"""
|
|
91
|
+
for permission_class in self.get_permissions():
|
|
92
|
+
permission = permission_class()
|
|
93
|
+
has_perm = await permission.has_permission(self.request)
|
|
94
|
+
if not has_perm:
|
|
95
|
+
message = getattr(
|
|
96
|
+
permission,
|
|
97
|
+
"message_exception",
|
|
98
|
+
"No tienes permiso para realizar esta acción.",
|
|
99
|
+
)
|
|
100
|
+
raise PermissionException(message)
|
|
101
|
+
|
|
102
|
+
async def check_permissions_class(self):
|
|
103
|
+
"""Backward-compat alias for ``check_permissions``.
|
|
104
|
+
|
|
105
|
+
Pre-0.3.2 controllers called this manually inside endpoint methods.
|
|
106
|
+
Keep working for users who haven't migrated to ``permission_classes``
|
|
107
|
+
+ auto-wrapping yet. New code should declare ``permission_classes``
|
|
108
|
+
on the controller and let the metaclass run permissions.
|
|
109
|
+
"""
|
|
110
|
+
await self.check_permissions()
|
|
41
111
|
|
|
42
112
|
def get_schema_class(self) -> Type[BaseModel]:
|
|
43
113
|
assert self.schema_class is not None, (
|
|
44
114
|
"'%s' should either include a `schema_class` attribute, "
|
|
45
|
-
"or override the `
|
|
115
|
+
"or override the `get_schema_class()` method."
|
|
46
116
|
% self.__class__.__name__
|
|
47
117
|
)
|
|
48
118
|
return self.schema_class
|
|
49
119
|
|
|
50
|
-
async def check_permissions_class(self):
|
|
51
|
-
permissions = self.check_permissions()
|
|
52
|
-
if permissions:
|
|
53
|
-
for permission in permissions:
|
|
54
|
-
obj = permission()
|
|
55
|
-
check = await obj.has_permission(self.request)
|
|
56
|
-
if not check:
|
|
57
|
-
raise PermissionException(obj.message_exception)
|
|
58
|
-
|
|
59
|
-
def check_permissions(self) -> List[Type[BasePermission]]:
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
120
|
async def list(self):
|
|
121
|
+
await self.prepare_action("list")
|
|
63
122
|
params = self._params()
|
|
64
123
|
items, total = await self.service.list(**params)
|
|
65
124
|
count = params.get("count") or 0
|
|
@@ -75,18 +134,22 @@ class BaseController:
|
|
|
75
134
|
return self.format_response(data=items, pagination=pagination)
|
|
76
135
|
|
|
77
136
|
async def retrieve(self, id: str):
|
|
137
|
+
await self.prepare_action("retrieve")
|
|
78
138
|
item = await self.service.retrieve(id)
|
|
79
139
|
return self.format_response(data=item)
|
|
80
140
|
|
|
81
141
|
async def create(self, validated_data: Any):
|
|
142
|
+
await self.prepare_action("create")
|
|
82
143
|
result = await self.service.create(validated_data)
|
|
83
144
|
return self.format_response(result, message="Creado exitosamente")
|
|
84
145
|
|
|
85
146
|
async def update(self, id: str, validated_data: Any):
|
|
147
|
+
await self.prepare_action("update")
|
|
86
148
|
result = await self.service.update(id, validated_data)
|
|
87
149
|
return self.format_response(result, message="Actualizado exitosamente")
|
|
88
150
|
|
|
89
151
|
async def delete(self, id: str):
|
|
152
|
+
await self.prepare_action("delete")
|
|
90
153
|
await self.service.delete(id)
|
|
91
154
|
return self.format_response(None, message="Eliminado exitosamente")
|
|
92
155
|
|
|
@@ -99,33 +162,48 @@ class BaseController:
|
|
|
99
162
|
) -> BaseModel:
|
|
100
163
|
schema = self.get_schema_class()
|
|
101
164
|
|
|
165
|
+
# Robust Pydantic v2 validation. Each branch falls back to the
|
|
166
|
+
# raw value when the schema doesn't fit (custom-action endpoints
|
|
167
|
+
# often return ad-hoc dicts that don't match the controller's
|
|
168
|
+
# default schema_class — those should pass through untouched).
|
|
102
169
|
if isinstance(data, list):
|
|
103
170
|
data_dicts = [self.to_dict(item) for item in data]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
data_parsed = schema.model_validate(data_parsed)
|
|
171
|
+
try:
|
|
172
|
+
adapter = TypeAdapter(List[schema])
|
|
173
|
+
data_parsed = adapter.validate_python(data_dicts)
|
|
174
|
+
except Exception:
|
|
175
|
+
data_parsed = data_dicts
|
|
176
|
+
|
|
111
177
|
elif isinstance(data, dict):
|
|
112
|
-
|
|
178
|
+
try:
|
|
179
|
+
data_parsed = schema.model_validate(data)
|
|
180
|
+
except Exception:
|
|
181
|
+
data_parsed = data
|
|
182
|
+
|
|
183
|
+
elif hasattr(data, "__dict__"):
|
|
184
|
+
data_dict = self.to_dict(data)
|
|
185
|
+
try:
|
|
186
|
+
data_parsed = schema.model_validate(data_dict)
|
|
187
|
+
except Exception:
|
|
188
|
+
data_parsed = data_dict
|
|
189
|
+
|
|
190
|
+
elif data is None:
|
|
191
|
+
data_parsed = None
|
|
113
192
|
else:
|
|
114
193
|
data_parsed = data
|
|
115
194
|
|
|
195
|
+
response_cls = BasePaginationResponse if pagination else BaseResponse
|
|
196
|
+
|
|
197
|
+
# Construcción dinámica de argumentos
|
|
198
|
+
kwargs = {
|
|
199
|
+
"data": data_parsed,
|
|
200
|
+
"message": message or "Operación exitosa",
|
|
201
|
+
"status": response_status,
|
|
202
|
+
}
|
|
116
203
|
if pagination:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
message=message or "Operación exitosa",
|
|
121
|
-
status=response_status,
|
|
122
|
-
)
|
|
123
|
-
else:
|
|
124
|
-
return BaseResponse(
|
|
125
|
-
data=data_parsed,
|
|
126
|
-
message=message or "Operación exitosa",
|
|
127
|
-
status=response_status,
|
|
128
|
-
)
|
|
204
|
+
kwargs["pagination"] = pagination
|
|
205
|
+
|
|
206
|
+
return response_cls(**kwargs)
|
|
129
207
|
|
|
130
208
|
def _params(self, skip_frames: int = 1) -> Dict[str, Any]:
|
|
131
209
|
"""
|
|
@@ -195,7 +273,10 @@ class BaseController:
|
|
|
195
273
|
search = final_value
|
|
196
274
|
elif param_name == "order_by":
|
|
197
275
|
order_by = final_value
|
|
198
|
-
elif
|
|
276
|
+
elif (
|
|
277
|
+
param_name not in standard_params
|
|
278
|
+
and param_name not in self._params_excluded_fields
|
|
279
|
+
):
|
|
199
280
|
# Es un filtro
|
|
200
281
|
filters[param_name] = final_value
|
|
201
282
|
|
|
@@ -208,6 +289,14 @@ class BaseController:
|
|
|
208
289
|
}
|
|
209
290
|
|
|
210
291
|
def to_dict(self, obj: Any):
|
|
211
|
-
|
|
292
|
+
"""Helper para convertir modelos ORM/Pydantic a dict."""
|
|
293
|
+
if hasattr(obj, "model_dump"): # Pydantic v2
|
|
212
294
|
return obj.model_dump()
|
|
295
|
+
if hasattr(obj, "dict"): # Pydantic v1
|
|
296
|
+
return obj.dict()
|
|
297
|
+
if hasattr(obj, "__dict__"): # SQLAlchemy models (basic)
|
|
298
|
+
# Filtramos atributos privados de SQLAlchemy
|
|
299
|
+
return {
|
|
300
|
+
k: v for k, v in obj.__dict__.items() if not k.startswith("_")
|
|
301
|
+
}
|
|
213
302
|
return obj
|
{fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py
RENAMED
|
@@ -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",
|
|
@@ -48,6 +49,7 @@ class SQLAlchemyBaseController(BaseController):
|
|
|
48
49
|
joins: Lista de relaciones a hacer JOIN eager loading
|
|
49
50
|
order_by: Expresión de ordenamiento (ej: User.created_at.desc())
|
|
50
51
|
"""
|
|
52
|
+
await self.prepare_action("list")
|
|
51
53
|
params = self._params(skip_frames=2)
|
|
52
54
|
service_params = {
|
|
53
55
|
**params,
|
|
@@ -73,6 +75,7 @@ class SQLAlchemyBaseController(BaseController):
|
|
|
73
75
|
id: ID del registro
|
|
74
76
|
joins: Lista de relaciones a hacer JOIN eager loading
|
|
75
77
|
"""
|
|
78
|
+
await self.prepare_action("retrieve")
|
|
76
79
|
item = await self.service.retrieve(id, joins=joins)
|
|
77
80
|
return self.format_response(data=item)
|
|
78
81
|
|
|
@@ -89,6 +92,7 @@ class SQLAlchemyBaseController(BaseController):
|
|
|
89
92
|
validated_data: Datos validados para crear
|
|
90
93
|
check_fields: Campos a verificar por duplicados antes de crear
|
|
91
94
|
"""
|
|
95
|
+
await self.prepare_action("create")
|
|
92
96
|
result = await self.service.create(validated_data, check_fields)
|
|
93
97
|
return self.format_response(result, message="Creado exitosamente")
|
|
94
98
|
|
{fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/repository/base.py
RENAMED
|
@@ -131,7 +131,13 @@ class BaseRepository:
|
|
|
131
131
|
joins_to_apply = {}
|
|
132
132
|
|
|
133
133
|
for filter_path, value in filters.items():
|
|
134
|
-
|
|
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 _,
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
_,
|
|
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
|
|
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)
|
{fastapi_basekit-0.3.2 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/base.py
RENAMED
|
@@ -49,6 +49,7 @@ class SQLModelBaseController(BaseController):
|
|
|
49
49
|
joins: Lista de relaciones para eager loading.
|
|
50
50
|
order_by: Expresión de ordenamiento (ej: ``"-created_at"``).
|
|
51
51
|
"""
|
|
52
|
+
await self.prepare_action("list")
|
|
52
53
|
params = self._params(skip_frames=2)
|
|
53
54
|
service_params = {
|
|
54
55
|
**params,
|
|
@@ -73,6 +74,7 @@ class SQLModelBaseController(BaseController):
|
|
|
73
74
|
id: ID del registro.
|
|
74
75
|
joins: Lista de relaciones para eager loading.
|
|
75
76
|
"""
|
|
77
|
+
await self.prepare_action("retrieve")
|
|
76
78
|
item = await self.service.retrieve(id, joins=joins)
|
|
77
79
|
return self.format_response(data=item)
|
|
78
80
|
|
|
@@ -88,6 +90,7 @@ class SQLModelBaseController(BaseController):
|
|
|
88
90
|
validated_data: Datos validados para crear.
|
|
89
91
|
check_fields: Campos a verificar por duplicados antes de crear.
|
|
90
92
|
"""
|
|
93
|
+
await self.prepare_action("create")
|
|
91
94
|
result = await self.service.create(validated_data, check_fields)
|
|
92
95
|
return self.format_response(result, message="Creado exitosamente")
|
|
93
96
|
|
|
@@ -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)
|