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.
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/PKG-INFO +17 -7
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/README.md +16 -6
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/controller/base.py +21 -2
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/controller/base.py +1 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/repository/base.py +63 -13
- fastapi_basekit-0.4.0/fastapi_basekit/exceptions/__init__.py +4 -0
- {fastapi_basekit-0.3.3 → 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.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/base.py +1 -1
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/PKG-INFO +17 -7
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/SOURCES.txt +5 -0
- {fastapi_basekit-0.3.3 → 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.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.3.3/fastapi_basekit/exceptions/__init__.py +0 -3
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/LICENSE +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/service/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/cli/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/cli/main.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/domain.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/schema/schema.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/entry_points.txt +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/requires.txt +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/setup.cfg +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_base_service.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_beanie_aggregation_hooks.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_beanie_aggregation_integration.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_controller_auto_permissions.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_crud_sqlmodel_repository_service.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_jwt_service.py +0 -0
- {fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_sql_queryset_override.py +0 -0
- {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
|
+
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
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
{fastapi_basekit-0.3.3 → 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)
|
|
@@ -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
|
+
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
|
|
@@ -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.
|
|
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
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/controller/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/repository/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/beanie/service/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlalchemy/service/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/controller/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/repository/base.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/__init__.py
RENAMED
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/aio/sqlmodel/service/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/exceptions/api_exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit/templates/project/cookiecutter.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/fastapi_basekit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_beanie_aggregation_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_basekit-0.3.3 → fastapi_basekit-0.4.0}/tests/test_crud_sqlmodel_repository_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|