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.
Files changed (50) hide show
  1. fastapi_basekit-0.1.19/PKG-INFO +791 -0
  2. fastapi_basekit-0.1.19/README.md +759 -0
  3. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/repository/base.py +8 -1
  4. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/service/base.py +29 -4
  5. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/exceptions/handler.py +16 -2
  6. fastapi_basekit-0.1.19/fastapi_basekit/schema/schema.py +24 -0
  7. fastapi_basekit-0.1.19/fastapi_basekit.egg-info/PKG-INFO +791 -0
  8. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/pyproject.toml +1 -1
  9. fastapi_basekit-0.1.18/PKG-INFO +0 -562
  10. fastapi_basekit-0.1.18/README.md +0 -530
  11. fastapi_basekit-0.1.18/fastapi_basekit/schema/schema.py +0 -17
  12. fastapi_basekit-0.1.18/fastapi_basekit.egg-info/PKG-INFO +0 -562
  13. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/LICENSE +0 -0
  14. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/__init__.py +0 -0
  15. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/__init__.py +0 -0
  16. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/__init__.py +0 -0
  17. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
  18. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
  19. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
  20. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/repository/base.py +0 -0
  21. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
  22. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/beanie/service/base.py +0 -0
  23. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/controller/__init__.py +0 -0
  24. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/controller/base.py +0 -0
  25. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/permissions/__init__.py +0 -0
  26. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/permissions/base.py +0 -0
  27. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
  28. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
  29. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -0
  30. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
  31. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
  32. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/exceptions/__init__.py +0 -0
  33. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
  34. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/schema/__init__.py +0 -0
  35. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/schema/base.py +0 -0
  36. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/schema/jwt.py +0 -0
  37. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/servicios/__init__.py +0 -0
  38. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
  39. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
  40. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/SOURCES.txt +0 -0
  41. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
  42. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/requires.txt +0 -0
  43. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/fastapi_basekit.egg-info/top_level.txt +0 -0
  44. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/setup.cfg +0 -0
  45. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_api_exceptions.py +0 -0
  46. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_base_response.py +0 -0
  47. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_base_service.py +0 -0
  48. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_crud_beanie_controller.py +0 -0
  49. {fastapi_basekit-0.1.18 → fastapi_basekit-0.1.19}/tests/test_crud_controller.py +0 -0
  50. {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
+ ![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi)
38
+ ![Python](https://img.shields.io/badge/python-3.8+-blue?style=for-the-badge&logo=python)
39
+ ![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-red?style=for-the-badge)
40
+ ![MongoDB](https://img.shields.io/badge/MongoDB-47A248?style=for-the-badge&logo=mongodb&logoColor=white)
41
+ ![License](https://img.shields.io/badge/license-MIT-green?style=for-the-badge)
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>