fastapi-basekit 0.1.18__tar.gz → 0.1.19__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.1.19/PKG-INFO +791 -0
- fastapi_basekit-0.1.19/README.md +759 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/repository/base.py +8 -1
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/service/base.py +29 -4
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/exceptions/handler.py +16 -2
- fastapi_basekit-0.1.19/fastapi_basekit/schema/schema.py +24 -0
- fastapi_basekit-0.1.19/fastapi_basekit.egg-info/PKG-INFO +791 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/pyproject.toml +1 -1
- fastapi_basekit-0.1.18/PKG-INFO +0 -562
- fastapi_basekit-0.1.18/README.md +0 -530
- fastapi_basekit-0.1.18/fastapi_basekit/schema/schema.py +0 -17
- fastapi_basekit-0.1.18/fastapi_basekit.egg-info/PKG-INFO +0 -562
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/LICENSE +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/repository/base.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/service/base.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/controller/base.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/exceptions/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/schema/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/schema/base.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/SOURCES.txt +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/requires.txt +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/setup.cfg +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_base_service.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_jwt_service.py +0 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fastapi-basekit
|
|
3
|
+
Version: 0.1.19
|
|
4
|
+
Summary: Utilities and base classes for FastAPI async projects (Beanie or SQLAlchemy)
|
|
5
|
+
Author-email: Jerson Moreno <jerson.ml820@hotmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mundobien2025/fastapi-basekit
|
|
8
|
+
Project-URL: Repository, https://github.com/mundobien2025/fastapi-basekit
|
|
9
|
+
Project-URL: Issues, https://github.com/mundobien2025/fastapi-basekit/issues
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: fastapi>=0.116.1
|
|
19
|
+
Requires-Dist: pydantic<3,>=2.11.7
|
|
20
|
+
Requires-Dist: fastapi-restful[all]>=0.6.0
|
|
21
|
+
Provides-Extra: beanie
|
|
22
|
+
Requires-Dist: beanie>=1.24.0; extra == "beanie"
|
|
23
|
+
Requires-Dist: motor>=3.3.0; extra == "beanie"
|
|
24
|
+
Provides-Extra: sqlalchemy
|
|
25
|
+
Requires-Dist: SQLAlchemy[asyncio]>=2.0.0; extra == "sqlalchemy"
|
|
26
|
+
Requires-Dist: psycopg2>=2.9.0; extra == "sqlalchemy"
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: beanie>=1.24.0; extra == "all"
|
|
29
|
+
Requires-Dist: motor>=3.3.0; extra == "all"
|
|
30
|
+
Requires-Dist: SQLAlchemy[asyncio]>=2.0.0; extra == "all"
|
|
31
|
+
Requires-Dist: psycopg2>=2.9.0; extra == "all"
|
|
32
|
+
|
|
33
|
+
# 🚀 FastAPI BaseKit
|
|
34
|
+
|
|
35
|
+
<div align="center">
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
**Toolkit base para desarrollo rápido de APIs REST con FastAPI**
|
|
44
|
+
|
|
45
|
+
[Documentación](https://github.com/mundobien2025/fastapi-basekit) •
|
|
46
|
+
[Ejemplos](./examples) •
|
|
47
|
+
[Changelog](./CHANGELOG.md)
|
|
48
|
+
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ✨ Características
|
|
54
|
+
|
|
55
|
+
- 🎯 **CRUD Automático**: Controllers base con operaciones CRUD listas para usar
|
|
56
|
+
- 🔍 **Búsqueda Inteligente**: Búsqueda multi-campo con filtros dinámicos
|
|
57
|
+
- 📊 **Paginación Avanzada**: Paginación automática con metadata completa
|
|
58
|
+
- 🔗 **Relaciones Optimizadas**: Joins dinámicos para evitar queries N+1 (SQLAlchemy)
|
|
59
|
+
- 🎨 **Type-Safe**: Type hints completos para mejor DX
|
|
60
|
+
- 🧪 **Testeable**: Diseño que facilita testing
|
|
61
|
+
- 🗃️ **Multi-DB**: Controllers separados para SQLAlchemy y Beanie (MongoDB)
|
|
62
|
+
- 🔒 **Permisos**: Sistema de permisos basado en clases
|
|
63
|
+
- ⚡ **Performance**: Queries optimizados y lazy loading
|
|
64
|
+
- 📝 **Validación**: Validación automática con Pydantic
|
|
65
|
+
- 🔧 **Queryset Personalizable**: Personaliza queries sin reescribir métodos
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 📦 Instalación
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Instalación básica
|
|
73
|
+
pip install fastapi-basekit
|
|
74
|
+
|
|
75
|
+
# Con soporte SQLAlchemy (PostgreSQL, MySQL, etc.)
|
|
76
|
+
pip install fastapi-basekit[sqlalchemy]
|
|
77
|
+
|
|
78
|
+
# Con soporte Beanie (MongoDB)
|
|
79
|
+
pip install fastapi-basekit[beanie]
|
|
80
|
+
|
|
81
|
+
# Con todo
|
|
82
|
+
pip install fastapi-basekit[all]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 🚀 Inicio Rápido
|
|
88
|
+
|
|
89
|
+
### Ejemplo Simple: CRUD Básico
|
|
90
|
+
|
|
91
|
+
#### 1. Modelo (SQLAlchemy)
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# models/user.py
|
|
95
|
+
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
|
96
|
+
from sqlalchemy.orm import declarative_base
|
|
97
|
+
from datetime import datetime
|
|
98
|
+
|
|
99
|
+
Base = declarative_base()
|
|
100
|
+
|
|
101
|
+
class User(Base):
|
|
102
|
+
__tablename__ = "users"
|
|
103
|
+
|
|
104
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
105
|
+
name = Column(String(100), nullable=False)
|
|
106
|
+
email = Column(String(100), unique=True, nullable=False, index=True)
|
|
107
|
+
age = Column(Integer, nullable=True)
|
|
108
|
+
is_active = Column(Boolean, default=True, nullable=False)
|
|
109
|
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
110
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=True)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### 2. Schema (Pydantic)
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# schemas/user.py
|
|
117
|
+
from pydantic import BaseModel, EmailStr
|
|
118
|
+
from typing import Optional
|
|
119
|
+
from datetime import datetime
|
|
120
|
+
|
|
121
|
+
class UserSchema(BaseModel):
|
|
122
|
+
id: int
|
|
123
|
+
name: str
|
|
124
|
+
email: EmailStr
|
|
125
|
+
age: Optional[int] = None
|
|
126
|
+
is_active: bool
|
|
127
|
+
created_at: datetime
|
|
128
|
+
updated_at: Optional[datetime] = None
|
|
129
|
+
|
|
130
|
+
class Config:
|
|
131
|
+
from_attributes = True
|
|
132
|
+
|
|
133
|
+
class UserCreateSchema(BaseModel):
|
|
134
|
+
name: str
|
|
135
|
+
email: EmailStr
|
|
136
|
+
age: Optional[int] = None
|
|
137
|
+
is_active: bool = True
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### 3. Repository
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# repositories/user.py
|
|
144
|
+
from fastapi_basekit.aio.sqlalchemy.repository.base import BaseRepository
|
|
145
|
+
from models.user import User
|
|
146
|
+
|
|
147
|
+
class UserRepository(BaseRepository):
|
|
148
|
+
model = User
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### 4. Service
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# services/user.py
|
|
155
|
+
from fastapi_basekit.aio.sqlalchemy.service.base import BaseService
|
|
156
|
+
|
|
157
|
+
class UserService(BaseService):
|
|
158
|
+
# Campos por los que se puede buscar
|
|
159
|
+
search_fields = ["name", "email"]
|
|
160
|
+
|
|
161
|
+
# Campos que deben ser únicos al crear
|
|
162
|
+
duplicate_check_fields = ["email"]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### 5. Controller
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# controllers/user.py
|
|
169
|
+
from typing import Optional
|
|
170
|
+
from fastapi import APIRouter, Query, Depends, Request
|
|
171
|
+
from fastapi_basekit.aio.sqlalchemy.controller.base import SQLAlchemyBaseController
|
|
172
|
+
from schemas.user import UserSchema, UserCreateSchema, UserUpdateSchema
|
|
173
|
+
from services.user import UserService
|
|
174
|
+
from repositories.user import UserRepository
|
|
175
|
+
|
|
176
|
+
router = APIRouter(prefix="/users", tags=["users"])
|
|
177
|
+
|
|
178
|
+
def get_user_service(request: Request) -> UserService:
|
|
179
|
+
repository = UserRepository(db=request.state.db)
|
|
180
|
+
return UserService(repository=repository, request=request)
|
|
181
|
+
|
|
182
|
+
@router.get("/")
|
|
183
|
+
class ListUsers(SQLAlchemyBaseController):
|
|
184
|
+
schema_class = UserSchema
|
|
185
|
+
service: UserService = Depends(get_user_service)
|
|
186
|
+
|
|
187
|
+
async def __call__(
|
|
188
|
+
self,
|
|
189
|
+
page: int = Query(1, ge=1),
|
|
190
|
+
count: int = Query(10, ge=1, le=100),
|
|
191
|
+
search: Optional[str] = Query(None),
|
|
192
|
+
is_active: Optional[bool] = Query(None),
|
|
193
|
+
):
|
|
194
|
+
return await self.list()
|
|
195
|
+
|
|
196
|
+
@router.get("/{id}")
|
|
197
|
+
class GetUser(SQLAlchemyBaseController):
|
|
198
|
+
schema_class = UserSchema
|
|
199
|
+
service: UserService = Depends(get_user_service)
|
|
200
|
+
|
|
201
|
+
async def __call__(self, id: int):
|
|
202
|
+
return await self.retrieve(str(id))
|
|
203
|
+
|
|
204
|
+
@router.post("/", status_code=201)
|
|
205
|
+
class CreateUser(SQLAlchemyBaseController):
|
|
206
|
+
schema_class = UserSchema
|
|
207
|
+
service: UserService = Depends(get_user_service)
|
|
208
|
+
|
|
209
|
+
async def __call__(self, data: UserCreateSchema):
|
|
210
|
+
return await self.create(data)
|
|
211
|
+
|
|
212
|
+
@router.put("/{id}")
|
|
213
|
+
class UpdateUser(SQLAlchemyBaseController):
|
|
214
|
+
schema_class = UserSchema
|
|
215
|
+
service: UserService = Depends(get_user_service)
|
|
216
|
+
|
|
217
|
+
async def __call__(self, id: int, data: UserUpdateSchema):
|
|
218
|
+
return await self.update(str(id), data)
|
|
219
|
+
|
|
220
|
+
@router.delete("/{id}")
|
|
221
|
+
class DeleteUser(SQLAlchemyBaseController):
|
|
222
|
+
schema_class = UserSchema
|
|
223
|
+
service: UserService = Depends(get_user_service)
|
|
224
|
+
|
|
225
|
+
async def __call__(self, id: int):
|
|
226
|
+
return await self.delete(str(id))
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### 6. ¡Listo! 🎉
|
|
230
|
+
|
|
231
|
+
Ya tienes un CRUD completo con:
|
|
232
|
+
|
|
233
|
+
- ✅ Paginación automática
|
|
234
|
+
- ✅ Búsqueda por nombre o email
|
|
235
|
+
- ✅ Filtrado por `is_active`
|
|
236
|
+
- ✅ Validación de duplicados
|
|
237
|
+
- ✅ Type hints completos
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 📚 Ejemplos Avanzados
|
|
242
|
+
|
|
243
|
+
### Ejemplo 1: Queryset Personalizado con Agregaciones
|
|
244
|
+
|
|
245
|
+
**Caso de uso**: Listar usuarios con COUNT de referidos y SUM de órdenes sin reescribir `list()`.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# services/user.py
|
|
249
|
+
from sqlalchemy import Select, func, select
|
|
250
|
+
from sqlalchemy.orm import aliased
|
|
251
|
+
from fastapi_basekit.aio.sqlalchemy.service.base import BaseService
|
|
252
|
+
from models.user import User, Referral, Order
|
|
253
|
+
|
|
254
|
+
class UserService(BaseService):
|
|
255
|
+
search_fields = ["name", "email"]
|
|
256
|
+
duplicate_check_fields = ["email"]
|
|
257
|
+
|
|
258
|
+
def build_queryset(self) -> Select:
|
|
259
|
+
"""
|
|
260
|
+
Personaliza el queryset base para incluir agregaciones.
|
|
261
|
+
Este método se ejecuta ANTES de aplicar filtros.
|
|
262
|
+
"""
|
|
263
|
+
referral_alias = aliased(Referral)
|
|
264
|
+
order_alias = aliased(Order)
|
|
265
|
+
|
|
266
|
+
query = (
|
|
267
|
+
select(
|
|
268
|
+
User,
|
|
269
|
+
func.count(func.distinct(referral_alias.id)).label("referidos_count"),
|
|
270
|
+
func.count(func.distinct(order_alias.id)).label("total_orders"),
|
|
271
|
+
func.coalesce(func.sum(order_alias.total), 0).label("total_spent"),
|
|
272
|
+
)
|
|
273
|
+
.outerjoin(referral_alias, User.id == referral_alias.user_id)
|
|
274
|
+
.outerjoin(order_alias, User.id == order_alias.user_id)
|
|
275
|
+
.group_by(User.id)
|
|
276
|
+
)
|
|
277
|
+
return query
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Schema con agregaciones**:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
# schemas/user.py
|
|
284
|
+
class UserWithStatsSchema(BaseModel):
|
|
285
|
+
id: int
|
|
286
|
+
name: str
|
|
287
|
+
email: EmailStr
|
|
288
|
+
created_at: datetime
|
|
289
|
+
referidos_count: int
|
|
290
|
+
total_orders: Optional[int] = None
|
|
291
|
+
total_spent: Optional[int] = None # En centavos
|
|
292
|
+
|
|
293
|
+
class Config:
|
|
294
|
+
from_attributes = True
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Controller** (sin cambios en `list()`):
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
@router.get("/")
|
|
301
|
+
class ListUsersWithStats(SQLAlchemyBaseController):
|
|
302
|
+
schema_class = UserWithStatsSchema
|
|
303
|
+
service: UserService = Depends(get_user_service)
|
|
304
|
+
|
|
305
|
+
async def __call__(
|
|
306
|
+
self,
|
|
307
|
+
page: int = Query(1, ge=1),
|
|
308
|
+
count: int = Query(10, ge=1, le=100),
|
|
309
|
+
search: Optional[str] = Query(None),
|
|
310
|
+
):
|
|
311
|
+
# El queryset personalizado se aplica automáticamente
|
|
312
|
+
return await self.list(search=search)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Resultado**:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"data": [
|
|
320
|
+
{
|
|
321
|
+
"id": 1,
|
|
322
|
+
"name": "Juan Pérez",
|
|
323
|
+
"email": "juan@example.com",
|
|
324
|
+
"created_at": "2024-01-01T00:00:00",
|
|
325
|
+
"referidos_count": 5,
|
|
326
|
+
"total_orders": 12,
|
|
327
|
+
"total_spent": 150000
|
|
328
|
+
}
|
|
329
|
+
],
|
|
330
|
+
"pagination": { ... }
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Ejemplo 2: Joins Dinámicos con Relaciones
|
|
335
|
+
|
|
336
|
+
**Caso de uso**: Cargar relaciones automáticamente para evitar queries N+1.
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
# services/user.py
|
|
340
|
+
class UserService(BaseService):
|
|
341
|
+
search_fields = ["name", "email"]
|
|
342
|
+
duplicate_check_fields = ["email"]
|
|
343
|
+
|
|
344
|
+
def get_kwargs_query(self) -> dict:
|
|
345
|
+
"""
|
|
346
|
+
Define joins según la acción.
|
|
347
|
+
En 'list' y 'retrieve' carga automáticamente las relaciones.
|
|
348
|
+
"""
|
|
349
|
+
if self.action in ["list", "retrieve"]:
|
|
350
|
+
return {"joins": ["role", "roles"]}
|
|
351
|
+
return {}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Modelo con relaciones**:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
# models/user.py
|
|
358
|
+
class User(Base):
|
|
359
|
+
__tablename__ = "users"
|
|
360
|
+
|
|
361
|
+
id = Column(Integer, primary_key=True)
|
|
362
|
+
name = Column(String(100))
|
|
363
|
+
email = Column(String(100), unique=True)
|
|
364
|
+
role_id = Column(Integer, ForeignKey("roles.id"))
|
|
365
|
+
|
|
366
|
+
# Relación uno a muchos
|
|
367
|
+
role = relationship("Role", foreign_keys=[role_id])
|
|
368
|
+
|
|
369
|
+
# Relación muchos a muchos
|
|
370
|
+
roles = relationship("Role", secondary=user_roles, back_populates="users")
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Controller**:
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
@router.get("/")
|
|
377
|
+
class ListUsers(SQLAlchemyBaseController):
|
|
378
|
+
schema_class = UserSchema # Incluye role y roles
|
|
379
|
+
service: UserService = Depends(get_user_service)
|
|
380
|
+
|
|
381
|
+
async def __call__(self, ...):
|
|
382
|
+
# Los joins se aplican automáticamente desde get_kwargs_query()
|
|
383
|
+
return await self.list()
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Ejemplo 3: Sistema de Permisos
|
|
387
|
+
|
|
388
|
+
**Caso de uso**: Control de acceso basado en roles y propiedad.
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
# permissions/user.py
|
|
392
|
+
from fastapi_basekit.aio.permissions.base import BasePermission
|
|
393
|
+
|
|
394
|
+
class IsAdmin(BasePermission):
|
|
395
|
+
message_exception = "Solo administradores pueden realizar esta acción"
|
|
396
|
+
|
|
397
|
+
async def has_permission(self, request: Request) -> bool:
|
|
398
|
+
user = getattr(request.state, "user", None)
|
|
399
|
+
return getattr(user, "is_admin", False) if user else False
|
|
400
|
+
|
|
401
|
+
class IsOwnerOrAdmin(BasePermission):
|
|
402
|
+
message_exception = "Solo el propietario o un administrador puede realizar esta acción"
|
|
403
|
+
|
|
404
|
+
async def has_permission(self, request: Request) -> bool:
|
|
405
|
+
user = getattr(request.state, "user", None)
|
|
406
|
+
if not user:
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
resource_id = request.path_params.get("id")
|
|
410
|
+
if getattr(user, "is_admin", False):
|
|
411
|
+
return True
|
|
412
|
+
|
|
413
|
+
return str(user.id) == str(resource_id)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Controller con permisos**:
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
@router.get("/{id}")
|
|
420
|
+
class GetUser(SQLAlchemyBaseController):
|
|
421
|
+
schema_class = UserSchema
|
|
422
|
+
service: UserService = Depends(get_user_service)
|
|
423
|
+
|
|
424
|
+
def check_permissions(self) -> List[Type[BasePermission]]:
|
|
425
|
+
return [IsOwnerOrAdmin]
|
|
426
|
+
|
|
427
|
+
async def __call__(self, id: int):
|
|
428
|
+
return await self.retrieve(str(id))
|
|
429
|
+
|
|
430
|
+
@router.post("/", status_code=201)
|
|
431
|
+
class CreateUser(SQLAlchemyBaseController):
|
|
432
|
+
schema_class = UserSchema
|
|
433
|
+
service: UserService = Depends(get_user_service)
|
|
434
|
+
|
|
435
|
+
def check_permissions(self) -> List[Type[BasePermission]]:
|
|
436
|
+
return [IsAdmin] # Solo admins pueden crear
|
|
437
|
+
|
|
438
|
+
async def __call__(self, data: UserCreateSchema):
|
|
439
|
+
return await self.create(data)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Ejemplo 4: Filtros Personalizados
|
|
443
|
+
|
|
444
|
+
**Caso de uso**: Transformar filtros antes de aplicarlos.
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
# services/user.py
|
|
448
|
+
class UserService(BaseService):
|
|
449
|
+
search_fields = ["name", "email"]
|
|
450
|
+
duplicate_check_fields = ["email"]
|
|
451
|
+
|
|
452
|
+
def get_filters(self, filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
453
|
+
"""
|
|
454
|
+
Transforma filtros antes de aplicarlos.
|
|
455
|
+
Ejemplo: convertir age_min en filtro de edad.
|
|
456
|
+
"""
|
|
457
|
+
applied = filters or {}
|
|
458
|
+
|
|
459
|
+
# Si viene age_min, lo convertimos en filtro de edad
|
|
460
|
+
if "age_min" in applied:
|
|
461
|
+
age_min = applied.pop("age_min")
|
|
462
|
+
# Aquí podrías agregar lógica adicional
|
|
463
|
+
# Por ejemplo, aplicar filtro de edad mínima
|
|
464
|
+
|
|
465
|
+
return applied
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 📖 Uso de la API
|
|
471
|
+
|
|
472
|
+
### Listar con Filtros y Paginación
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
# Página 1, 10 items
|
|
476
|
+
GET /users?page=1&count=10
|
|
477
|
+
|
|
478
|
+
# Buscar usuarios
|
|
479
|
+
GET /users?search=john
|
|
480
|
+
|
|
481
|
+
# Filtrar activos
|
|
482
|
+
GET /users?is_active=true
|
|
483
|
+
|
|
484
|
+
# Combinar filtros
|
|
485
|
+
GET /users?search=john&is_active=true&page=1&count=10
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Respuesta**:
|
|
489
|
+
|
|
490
|
+
```json
|
|
491
|
+
{
|
|
492
|
+
"data": [
|
|
493
|
+
{
|
|
494
|
+
"id": 1,
|
|
495
|
+
"name": "John Doe",
|
|
496
|
+
"email": "john@example.com",
|
|
497
|
+
"age": 30,
|
|
498
|
+
"is_active": true,
|
|
499
|
+
"created_at": "2024-01-01T00:00:00",
|
|
500
|
+
"updated_at": null
|
|
501
|
+
}
|
|
502
|
+
],
|
|
503
|
+
"pagination": {
|
|
504
|
+
"page": 1,
|
|
505
|
+
"count": 10,
|
|
506
|
+
"total": 100,
|
|
507
|
+
"total_pages": 10
|
|
508
|
+
},
|
|
509
|
+
"message": "Operación exitosa",
|
|
510
|
+
"status": "success"
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Crear Usuario
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
POST /users
|
|
518
|
+
Content-Type: application/json
|
|
519
|
+
|
|
520
|
+
{
|
|
521
|
+
"name": "Jane Doe",
|
|
522
|
+
"email": "jane@example.com",
|
|
523
|
+
"age": 25,
|
|
524
|
+
"is_active": true
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Respuesta**:
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"data": {
|
|
533
|
+
"id": 2,
|
|
534
|
+
"name": "Jane Doe",
|
|
535
|
+
"email": "jane@example.com",
|
|
536
|
+
"age": 25,
|
|
537
|
+
"is_active": true,
|
|
538
|
+
"created_at": "2024-01-02T00:00:00",
|
|
539
|
+
"updated_at": null
|
|
540
|
+
},
|
|
541
|
+
"message": "Creado exitosamente",
|
|
542
|
+
"status": "success"
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## 🎯 Características Avanzadas
|
|
549
|
+
|
|
550
|
+
### build_queryset(): Personalización de Queries
|
|
551
|
+
|
|
552
|
+
El método `build_queryset()` permite personalizar el query base **antes** de aplicar filtros, búsqueda y paginación. Esto es útil para:
|
|
553
|
+
|
|
554
|
+
- Agregar JOINs complejos
|
|
555
|
+
- Incluir agregaciones (COUNT, SUM, AVG)
|
|
556
|
+
- Aplicar GROUP BY
|
|
557
|
+
- Seleccionar campos calculados
|
|
558
|
+
- Optimizar queries específicas
|
|
559
|
+
|
|
560
|
+
**Ventajas**:
|
|
561
|
+
|
|
562
|
+
- ✅ No necesitas reescribir `list()`
|
|
563
|
+
- ✅ Los filtros se aplican automáticamente sobre tu query personalizado
|
|
564
|
+
- ✅ Mantiene toda la funcionalidad de paginación y búsqueda
|
|
565
|
+
|
|
566
|
+
### get_kwargs_query(): Configuración Dinámica
|
|
567
|
+
|
|
568
|
+
Permite definir configuración de queries según la acción:
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
def get_kwargs_query(self) -> dict:
|
|
572
|
+
if self.action == "list":
|
|
573
|
+
return {"joins": ["role", "profile"]}
|
|
574
|
+
elif self.action == "retrieve":
|
|
575
|
+
return {"joins": ["role", "profile", "orders"]}
|
|
576
|
+
return {}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### get_filters(): Transformación de Filtros
|
|
580
|
+
|
|
581
|
+
Transforma o valida filtros antes de aplicarlos:
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
def get_filters(self, filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
585
|
+
applied = filters or {}
|
|
586
|
+
|
|
587
|
+
# Validar o transformar filtros
|
|
588
|
+
if "date_from" in applied:
|
|
589
|
+
# Convertir formato de fecha, etc.
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
return applied
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## 📁 Estructura de Ejemplos
|
|
598
|
+
|
|
599
|
+
El proyecto incluye ejemplos completos en la carpeta `examples/`:
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
examples/
|
|
603
|
+
├── simple_crud/ # CRUD básico
|
|
604
|
+
│ ├── models.py
|
|
605
|
+
│ ├── schemas.py
|
|
606
|
+
│ ├── repository.py
|
|
607
|
+
│ ├── service.py
|
|
608
|
+
│ └── controller.py
|
|
609
|
+
│
|
|
610
|
+
├── advanced_queryset/ # Queryset personalizado con agregaciones
|
|
611
|
+
│ ├── models.py
|
|
612
|
+
│ ├── schemas.py
|
|
613
|
+
│ ├── repository.py
|
|
614
|
+
│ ├── service.py # build_queryset() con COUNT y SUM
|
|
615
|
+
│ └── controller.py
|
|
616
|
+
│
|
|
617
|
+
├── with_relations/ # Relaciones y joins dinámicos
|
|
618
|
+
│ ├── models.py
|
|
619
|
+
│ ├── schemas.py
|
|
620
|
+
│ ├── repository.py
|
|
621
|
+
│ ├── service.py # get_kwargs_query() con joins
|
|
622
|
+
│ └── controller.py
|
|
623
|
+
│
|
|
624
|
+
└── with_permissions/ # Sistema de permisos
|
|
625
|
+
├── models.py
|
|
626
|
+
├── schemas.py
|
|
627
|
+
├── repository.py
|
|
628
|
+
├── service.py
|
|
629
|
+
├── permissions.py # Permisos personalizados
|
|
630
|
+
└── controller.py
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## 🔧 Configuración
|
|
636
|
+
|
|
637
|
+
### Variables de Entorno
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
# .env
|
|
641
|
+
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/dbname
|
|
642
|
+
FASTAPI_BASEKIT_DEFAULT_PAGE_SIZE=25
|
|
643
|
+
FASTAPI_BASEKIT_MAX_PAGE_SIZE=200
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Setup de Base de Datos
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
# database.py
|
|
650
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
651
|
+
|
|
652
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/dbname")
|
|
653
|
+
async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
654
|
+
|
|
655
|
+
async def get_db():
|
|
656
|
+
async with async_session_maker() as session:
|
|
657
|
+
yield session
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Middleware para DB
|
|
661
|
+
|
|
662
|
+
```python
|
|
663
|
+
# main.py
|
|
664
|
+
from fastapi import FastAPI, Request
|
|
665
|
+
from database import get_db
|
|
666
|
+
|
|
667
|
+
app = FastAPI()
|
|
668
|
+
|
|
669
|
+
@app.middleware("http")
|
|
670
|
+
async def db_session_middleware(request: Request, call_next):
|
|
671
|
+
async for session in get_db():
|
|
672
|
+
request.state.db = session
|
|
673
|
+
response = await call_next(request)
|
|
674
|
+
await session.commit()
|
|
675
|
+
return response
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## 🧪 Testing
|
|
681
|
+
|
|
682
|
+
```python
|
|
683
|
+
# tests/test_user_controller.py
|
|
684
|
+
import pytest
|
|
685
|
+
from fastapi.testclient import TestClient
|
|
686
|
+
|
|
687
|
+
def test_list_users(client: TestClient):
|
|
688
|
+
response = client.get("/users?page=1&count=10")
|
|
689
|
+
assert response.status_code == 200
|
|
690
|
+
data = response.json()
|
|
691
|
+
assert "data" in data
|
|
692
|
+
assert "pagination" in data
|
|
693
|
+
assert data["pagination"]["page"] == 1
|
|
694
|
+
|
|
695
|
+
def test_create_user(client: TestClient):
|
|
696
|
+
user_data = {
|
|
697
|
+
"name": "Test User",
|
|
698
|
+
"email": "test@example.com"
|
|
699
|
+
}
|
|
700
|
+
response = client.post("/users", json=user_data)
|
|
701
|
+
assert response.status_code == 201
|
|
702
|
+
data = response.json()
|
|
703
|
+
assert data["data"]["name"] == "Test User"
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## 📊 Arquitectura
|
|
709
|
+
|
|
710
|
+
```
|
|
711
|
+
┌─────────────┐
|
|
712
|
+
│ Client │
|
|
713
|
+
└─────┬───────┘
|
|
714
|
+
│ HTTP Request
|
|
715
|
+
▼
|
|
716
|
+
┌─────────────────┐
|
|
717
|
+
│ Controller │ ← Validación, permisos, formato de respuesta
|
|
718
|
+
└────────┬────────┘
|
|
719
|
+
│
|
|
720
|
+
▼
|
|
721
|
+
┌─────────────────┐
|
|
722
|
+
│ Service │ ← Lógica de negocio, build_queryset(), get_filters()
|
|
723
|
+
└────────┬────────┘
|
|
724
|
+
│
|
|
725
|
+
▼
|
|
726
|
+
┌─────────────────┐
|
|
727
|
+
│ Repository │ ← Acceso a datos, queries optimizados
|
|
728
|
+
└────────┬────────┘
|
|
729
|
+
│
|
|
730
|
+
▼
|
|
731
|
+
┌─────────────────┐
|
|
732
|
+
│ Database │
|
|
733
|
+
└─────────────────┘
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## 🤝 Contribuir
|
|
739
|
+
|
|
740
|
+
¡Las contribuciones son bienvenidas! Por favor lee [CONTRIBUTING.md](./CONTRIBUTING.md) para detalles.
|
|
741
|
+
|
|
742
|
+
### Desarrollo Local
|
|
743
|
+
|
|
744
|
+
```bash
|
|
745
|
+
# Clonar
|
|
746
|
+
git clone https://github.com/mundobien2025/fastapi-basekit.git
|
|
747
|
+
cd fastapi-basekit
|
|
748
|
+
|
|
749
|
+
# Instalar dependencias
|
|
750
|
+
pip install -e ".[dev]"
|
|
751
|
+
|
|
752
|
+
# Ejecutar tests
|
|
753
|
+
pytest
|
|
754
|
+
|
|
755
|
+
# Linting
|
|
756
|
+
black fastapi_basekit
|
|
757
|
+
flake8 fastapi_basekit
|
|
758
|
+
mypy fastapi_basekit
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## 📄 Licencia
|
|
764
|
+
|
|
765
|
+
Este proyecto está licenciado bajo la licencia MIT - ver [LICENSE](./LICENSE) para detalles.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## 🙏 Agradecimientos
|
|
770
|
+
|
|
771
|
+
- [FastAPI](https://fastapi.tiangolo.com/) - El framework web moderno y rápido
|
|
772
|
+
- [SQLAlchemy](https://www.sqlalchemy.org/) - El ORM SQL para Python
|
|
773
|
+
- [Pydantic](https://pydantic-docs.helpmanual.io/) - Validación de datos usando Python type hints
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
777
|
+
## 📞 Soporte
|
|
778
|
+
|
|
779
|
+
- 📖 [Documentación](https://github.com/mundobien2025/fastapi-basekit)
|
|
780
|
+
- 🐛 [Issues](https://github.com/mundobien2025/fastapi-basekit/issues)
|
|
781
|
+
- 💬 [Discussions](https://github.com/mundobien2025/fastapi-basekit/discussions)
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
<div align="center">
|
|
786
|
+
|
|
787
|
+
**Hecho con ❤️ para la comunidad FastAPI**
|
|
788
|
+
|
|
789
|
+
⭐ Si te gusta este proyecto, dale una estrella en GitHub
|
|
790
|
+
|
|
791
|
+
</div>
|