swl-ses 3.3.2

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 (177) hide show
  1. package/CLAUDE.md +425 -0
  2. package/_userland/agentes/.gitkeep +0 -0
  3. package/_userland/habilidades/.gitkeep +0 -0
  4. package/agentes/accesibilidad-wcag-swl.md +683 -0
  5. package/agentes/arquitecto-swl.md +210 -0
  6. package/agentes/auto-evolucion-swl.md +408 -0
  7. package/agentes/backend-api-swl.md +442 -0
  8. package/agentes/backend-node-swl.md +439 -0
  9. package/agentes/backend-python-swl.md +469 -0
  10. package/agentes/backend-workers-swl.md +444 -0
  11. package/agentes/cloud-infra-swl.md +466 -0
  12. package/agentes/consolidador-swl.md +487 -0
  13. package/agentes/datos-swl.md +568 -0
  14. package/agentes/depurador-swl.md +301 -0
  15. package/agentes/devops-ci-swl.md +352 -0
  16. package/agentes/disenador-ui-swl.md +546 -0
  17. package/agentes/documentador-swl.md +323 -0
  18. package/agentes/frontend-angular-swl.md +603 -0
  19. package/agentes/frontend-css-swl.md +700 -0
  20. package/agentes/frontend-react-swl.md +672 -0
  21. package/agentes/frontend-swl.md +483 -0
  22. package/agentes/frontend-tailwind-swl.md +808 -0
  23. package/agentes/implementador-swl.md +235 -0
  24. package/agentes/investigador-swl.md +274 -0
  25. package/agentes/investigador-ux-swl.md +482 -0
  26. package/agentes/migrador-swl.md +389 -0
  27. package/agentes/mobile-android-swl.md +473 -0
  28. package/agentes/mobile-cross-swl.md +501 -0
  29. package/agentes/mobile-ios-swl.md +464 -0
  30. package/agentes/notificador-swl.md +886 -0
  31. package/agentes/observabilidad-swl.md +408 -0
  32. package/agentes/orquestador-swl.md +490 -0
  33. package/agentes/planificador-swl.md +222 -0
  34. package/agentes/producto-prd-swl.md +565 -0
  35. package/agentes/release-manager-swl.md +545 -0
  36. package/agentes/rendimiento-swl.md +691 -0
  37. package/agentes/revisor-codigo-swl.md +254 -0
  38. package/agentes/revisor-seguridad-swl.md +316 -0
  39. package/agentes/tdd-qa-swl.md +323 -0
  40. package/agentes/ux-disenador-swl.md +498 -0
  41. package/bin/swl-ses.js +119 -0
  42. package/comandos/swl/actualizar.md +117 -0
  43. package/comandos/swl/aprender.md +348 -0
  44. package/comandos/swl/auditar-deps.md +390 -0
  45. package/comandos/swl/autoresearch.md +346 -0
  46. package/comandos/swl/checkpoint.md +296 -0
  47. package/comandos/swl/compactar.md +283 -0
  48. package/comandos/swl/crear-skill.md +609 -0
  49. package/comandos/swl/discutir-fase.md +230 -0
  50. package/comandos/swl/ejecutar-fase.md +302 -0
  51. package/comandos/swl/evolucionar.md +377 -0
  52. package/comandos/swl/instalar.md +220 -0
  53. package/comandos/swl/mapear-codebase.md +205 -0
  54. package/comandos/swl/nuevo-proyecto.md +154 -0
  55. package/comandos/swl/planear-fase.md +221 -0
  56. package/comandos/swl/release.md +405 -0
  57. package/comandos/swl/salud.md +382 -0
  58. package/comandos/swl/verificar.md +292 -0
  59. package/habilidades/accesibilidad-a11y/SKILL.md +584 -0
  60. package/habilidades/angular-avanzado/SKILL.md +491 -0
  61. package/habilidades/angular-moderno/SKILL.md +326 -0
  62. package/habilidades/api-rest-diseno/SKILL.md +302 -0
  63. package/habilidades/api-rest-diseno/recursos/openapi-template.yaml +506 -0
  64. package/habilidades/aprendizaje-continuo/SKILL.md +369 -0
  65. package/habilidades/async-python/SKILL.md +474 -0
  66. package/habilidades/auth-patrones/SKILL.md +488 -0
  67. package/habilidades/auto-evolucion-protocolo/SKILL.md +376 -0
  68. package/habilidades/autoresearch/SKILL.md +248 -0
  69. package/habilidades/autoresearch/recursos/checklist-template.md +191 -0
  70. package/habilidades/autoresearch/scripts/calcular-score.js +88 -0
  71. package/habilidades/checklist-calidad/SKILL.md +247 -0
  72. package/habilidades/checklist-calidad/recursos/quality-report-template.md +148 -0
  73. package/habilidades/checklist-seguridad/SKILL.md +224 -0
  74. package/habilidades/checkpoints-verificacion/SKILL.md +309 -0
  75. package/habilidades/checkpoints-verificacion/recursos/checkpoint-templates.md +360 -0
  76. package/habilidades/ci-cd-pipelines/SKILL.md +583 -0
  77. package/habilidades/ci-cd-pipelines/recursos/github-actions-template.yaml +403 -0
  78. package/habilidades/cloud-aws/SKILL.md +497 -0
  79. package/habilidades/compactacion-contexto/SKILL.md +201 -0
  80. package/habilidades/contenedores-docker/SKILL.md +453 -0
  81. package/habilidades/contenedores-docker/recursos/dockerfile-template.dockerfile +160 -0
  82. package/habilidades/css-moderno/SKILL.md +463 -0
  83. package/habilidades/datos-etl/SKILL.md +486 -0
  84. package/habilidades/dependencias-auditoria/SKILL.md +293 -0
  85. package/habilidades/deprecacion-migracion/SKILL.md +485 -0
  86. package/habilidades/design-tokens/SKILL.md +519 -0
  87. package/habilidades/discutir-fase/SKILL.md +167 -0
  88. package/habilidades/diseno-responsivo/SKILL.md +326 -0
  89. package/habilidades/django-experto/SKILL.md +395 -0
  90. package/habilidades/doc-sync/SKILL.md +259 -0
  91. package/habilidades/ejecutar-fase/SKILL.md +199 -0
  92. package/habilidades/estructura-proyecto-claude/SKILL.md +459 -0
  93. package/habilidades/estructura-proyecto-claude/recursos/claude-md-template.md +261 -0
  94. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +213 -0
  95. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +77 -0
  96. package/habilidades/estructura-proyecto-claude/recursos/variantes-por-stack.md +177 -0
  97. package/habilidades/event-driven/SKILL.md +580 -0
  98. package/habilidades/extractor-de-aprendizajes/SKILL.md +234 -0
  99. package/habilidades/fastapi-experto/SKILL.md +368 -0
  100. package/habilidades/frontend-avanzado/SKILL.md +555 -0
  101. package/habilidades/git-worktrees-paralelo/SKILL.md +246 -0
  102. package/habilidades/iam-secretos/SKILL.md +511 -0
  103. package/habilidades/instalar-sistema/SKILL.md +140 -0
  104. package/habilidades/kubernetes-orquestacion/SKILL.md +549 -0
  105. package/habilidades/manejo-errores/SKILL.md +512 -0
  106. package/habilidades/mapear-codebase/SKILL.md +199 -0
  107. package/habilidades/microservicios/SKILL.md +473 -0
  108. package/habilidades/mobile-flutter/SKILL.md +566 -0
  109. package/habilidades/mobile-react-native/SKILL.md +493 -0
  110. package/habilidades/monitoring-alertas/SKILL.md +447 -0
  111. package/habilidades/node-experto/SKILL.md +521 -0
  112. package/habilidades/notificaciones-multicanal/SKILL.md +448 -0
  113. package/habilidades/notificaciones-multicanal/recursos/config-template.json +115 -0
  114. package/habilidades/nuevo-proyecto/SKILL.md +183 -0
  115. package/habilidades/patrones-python/SKILL.md +381 -0
  116. package/habilidades/performance-baseline/SKILL.md +243 -0
  117. package/habilidades/planear-fase/SKILL.md +184 -0
  118. package/habilidades/postgresql-experto/SKILL.md +379 -0
  119. package/habilidades/react-experto/SKILL.md +434 -0
  120. package/habilidades/react-optimizacion/SKILL.md +328 -0
  121. package/habilidades/release-semver/SKILL.md +226 -0
  122. package/habilidades/release-semver/scripts/generar-changelog.sh +238 -0
  123. package/habilidades/sql-optimizacion/SKILL.md +314 -0
  124. package/habilidades/tailwind-experto/SKILL.md +412 -0
  125. package/habilidades/tdd-workflow/SKILL.md +267 -0
  126. package/habilidades/testing-python/SKILL.md +350 -0
  127. package/habilidades/threat-model-lite/SKILL.md +218 -0
  128. package/habilidades/typescript-avanzado/SKILL.md +454 -0
  129. package/habilidades/ux-diseno/SKILL.md +488 -0
  130. package/habilidades/validacion-ci-sistema/SKILL.md +543 -0
  131. package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +286 -0
  132. package/habilidades/verificar-trabajo/SKILL.md +208 -0
  133. package/habilidades/wireframes-flujos/SKILL.md +396 -0
  134. package/habilidades/workflow-claude-code/SKILL.md +359 -0
  135. package/hooks/calidad-pre-commit.js +578 -0
  136. package/hooks/escaneo-secretos.js +302 -0
  137. package/hooks/extraccion-aprendizajes.js +550 -0
  138. package/hooks/linea-estado.js +249 -0
  139. package/hooks/monitor-contexto.js +230 -0
  140. package/hooks/proteccion-rutas.js +249 -0
  141. package/manifiestos/hooks-config.json +41 -0
  142. package/manifiestos/modulos.json +318 -0
  143. package/manifiestos/perfiles.json +189 -0
  144. package/package.json +45 -0
  145. package/plantillas/PROJECT.md +122 -0
  146. package/plantillas/REQUIREMENTS.md +132 -0
  147. package/plantillas/ROADMAP.md +143 -0
  148. package/plantillas/STATE.md +109 -0
  149. package/plantillas/research/ARCHITECTURE.md +220 -0
  150. package/plantillas/research/FEATURES.md +175 -0
  151. package/plantillas/research/PITFALLS.md +299 -0
  152. package/plantillas/research/STACK.md +233 -0
  153. package/plantillas/research/SUMMARY.md +165 -0
  154. package/plugin.json +144 -0
  155. package/reglas/accesibilidad.md +269 -0
  156. package/reglas/api-diseno.md +400 -0
  157. package/reglas/arquitectura.md +183 -0
  158. package/reglas/cloud-infra.md +247 -0
  159. package/reglas/docs.md +245 -0
  160. package/reglas/estilo-codigo.md +179 -0
  161. package/reglas/git-workflow.md +186 -0
  162. package/reglas/performance.md +195 -0
  163. package/reglas/pruebas.md +159 -0
  164. package/reglas/seguridad.md +151 -0
  165. package/reglas/skills-estandar.md +473 -0
  166. package/scripts/actualizar.js +51 -0
  167. package/scripts/desinstalar.js +86 -0
  168. package/scripts/doctor.js +222 -0
  169. package/scripts/inicializar.js +89 -0
  170. package/scripts/instalador.js +333 -0
  171. package/scripts/lib/detectar-runtime.js +177 -0
  172. package/scripts/lib/estado.js +112 -0
  173. package/scripts/lib/hooks-settings.js +283 -0
  174. package/scripts/lib/manifiestos.js +138 -0
  175. package/scripts/lib/seguridad.js +160 -0
  176. package/scripts/publicar.js +209 -0
  177. package/scripts/validar.js +120 -0
@@ -0,0 +1,234 @@
1
+ ---
2
+ name: extractor-de-aprendizajes
3
+ description: Convertir errores y patrones descubiertos durante la implementación en nuevas habilidades o reglas. Ciclo de mejora continua del sistema SWL.
4
+ ---
5
+
6
+ # Extractor de Aprendizajes
7
+
8
+ ## Propósito
9
+
10
+ Cada error cometido durante la implementación, cada bug inesperado, cada patrón que
11
+ funcionó mejor de lo esperado, es una oportunidad de mejorar el sistema. Este skill
12
+ define el proceso para convertir experiencia concreta en conocimiento estructurado
13
+ que previene errores futuros.
14
+
15
+ ---
16
+
17
+ ## El ciclo de mejora continua
18
+
19
+ ```
20
+ Implementación
21
+
22
+ Error / Insight descubierto
23
+
24
+ Clasificar: ¿es un anti-patrón, un patrón nuevo, o una regla de proyecto?
25
+
26
+ Documentar en la estructura correcta
27
+
28
+ El verificador lo usa en la siguiente iteración
29
+
30
+ Errores prevenidos en el futuro
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Tipos de aprendizajes
36
+
37
+ ### Tipo 1: Anti-patrón (error a prevenir)
38
+
39
+ Se descubrió algo que parece correcto pero falla en producción o en testing.
40
+ Debe convertirse en una **regla negativa** ("NUNCA hagas X").
41
+
42
+ **Indicadores**:
43
+ - Un test falló por una razón no obvia.
44
+ - Un bug tardó más de 30 minutos en diagnosticarse.
45
+ - Se asumió algo sobre una librería que resultó ser incorrecto.
46
+
47
+ ### Tipo 2: Patrón positivo (mejor práctica)
48
+
49
+ Se encontró una forma de hacer algo que resuelve un problema recurrente con
50
+ elegancia. Debe convertirse en una **regla positiva** ("SIEMPRE usa X para Y").
51
+
52
+ **Indicadores**:
53
+ - Se refactorizó código que inicialmente era verboso y quedó mucho más limpio.
54
+ - Una solución resolvió 3 problemas distintos a la vez.
55
+ - El code review elogió específicamente un patrón usado.
56
+
57
+ ### Tipo 3: Gotcha de librería o framework
58
+
59
+ El comportamiento de una dependencia no era el esperado según la documentación o
60
+ la intuición. Debe convertirse en una **nota de advertencia** en el skill relevante.
61
+
62
+ **Indicadores**:
63
+ - "La documentación dice X pero en realidad hace Y".
64
+ - El comportamiento cambia según la versión de la dependencia.
65
+ - Hay una edge case no documentada.
66
+
67
+ ### Tipo 4: Decisión de proyecto
68
+
69
+ Se tomó una decisión de arquitectura o diseño que debe respetarse en el futuro.
70
+ Debe registrarse en **DECISIONS.md** del proyecto.
71
+
72
+ ---
73
+
74
+ ## Protocolo de extracción
75
+
76
+ ### Paso 1: Capturar el contexto del error
77
+
78
+ Cuando se descubre un error o insight, documentarlo inmediatamente con:
79
+
80
+ ```markdown
81
+ ## Aprendizaje: [título corto]
82
+
83
+ **Fecha**: 2026-03-25
84
+ **Contexto**: Implementando endpoint POST /facturas en FastAPI con SQLAlchemy async.
85
+ **Error observado**: MissingGreenlet al acceder a factura.usuario.nombre en el schema Pydantic.
86
+ **Causa raíz**: La relación `usuario` no tenía selectinload() en el query. SQLAlchemy async
87
+ no puede hacer lazy loading fuera de una sesión async activa.
88
+ **Solución aplicada**: Agregar `.options(selectinload(Factura.usuario))` al query.
89
+ **Clasificación**: Anti-patrón → Gotcha de SQLAlchemy async
90
+ ```
91
+
92
+ ### Paso 2: Determinar el destino del aprendizaje
93
+
94
+ | Tipo de aprendizaje | Destino |
95
+ |--------------------|---------|
96
+ | Regla universal de Python | `habilidades/patrones-python/SKILL.md` |
97
+ | Gotcha de FastAPI | `habilidades/fastapi-experto/SKILL.md` |
98
+ | Gotcha de SQLAlchemy | `habilidades/fastapi-experto/SKILL.md` o `CLAUDE.md` del proyecto |
99
+ | Regla de diseño de API | `habilidades/api-rest-diseno/SKILL.md` |
100
+ | Regla de testing | `habilidades/testing-python/SKILL.md` |
101
+ | Decisión de proyecto específico | `DECISIONS.md` del proyecto |
102
+ | Regla que aplica a TODOS los proyectos | `CLAUDE.md` del sistema SWL |
103
+
104
+ ### Paso 3: Escribir la regla en el formato correcto
105
+
106
+ #### Formato para regla negativa (anti-patrón)
107
+
108
+ ```markdown
109
+ ### NUNCA: [título del anti-patrón]
110
+
111
+ **Problema**: Descripción clara de qué falla y por qué.
112
+
113
+ ```python
114
+ # MAL — esto causa [error específico]
115
+ query = select(Factura).where(Factura.id == factura_id)
116
+ result = await db.execute(query)
117
+ factura = result.scalar_one()
118
+ # Acceder a factura.usuario aquí causa MissingGreenlet
119
+ print(factura.usuario.nombre)
120
+ ```
121
+
122
+ ```python
123
+ # BIEN — con selectinload explícito
124
+ query = (
125
+ select(Factura)
126
+ .where(Factura.id == factura_id)
127
+ .options(selectinload(Factura.usuario))
128
+ )
129
+ result = await db.execute(query)
130
+ factura = result.scalar_one()
131
+ print(factura.usuario.nombre) # Funciona correctamente
132
+ ```
133
+ ```
134
+
135
+ #### Formato para regla positiva (mejor práctica)
136
+
137
+ ```markdown
138
+ ### SIEMPRE: [título de la mejor práctica]
139
+
140
+ **Cuándo aplicar**: Descripción de la situación.
141
+ **Beneficio**: Por qué esta forma es mejor.
142
+
143
+ ```python
144
+ # Patrón correcto con ejemplo concreto
145
+ ```
146
+ ```
147
+
148
+ ### Paso 4: Integrar la regla al skill correspondiente
149
+
150
+ 1. Abrir el SKILL.md del skill donde debe vivir la regla.
151
+ 2. Agregar la regla en la sección más relevante.
152
+ 3. Si no hay sección relevante, crear una nueva sección "Gotchas y casos especiales".
153
+ 4. Hacer commit del cambio al skill con mensaje:
154
+ ```
155
+ docs(skills): agrega regla sobre selectinload en SQLAlchemy async
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Plantilla de nuevo skill desde cero
161
+
162
+ Si el aprendizaje no encaja en ningún skill existente, crear uno nuevo:
163
+
164
+ ```
165
+ habilidades/
166
+ └── nuevo-skill/
167
+ └── SKILL.md
168
+ ```
169
+
170
+ ```yaml
171
+ ---
172
+ name: nuevo-skill
173
+ description: Una línea describiendo cuándo activar este skill.
174
+ ---
175
+
176
+ # Título del Skill
177
+
178
+ ## Cuándo activar
179
+ - Caso 1 donde este skill es relevante
180
+ - Caso 2 donde este skill es relevante
181
+
182
+ ## Reglas fundamentales
183
+
184
+ ### Regla 1
185
+ ...
186
+
187
+ ### Regla 2
188
+ ...
189
+
190
+ ## Anti-patrones
191
+
192
+ ### NUNCA: Anti-patrón 1
193
+ ...
194
+
195
+ ## Referencia rápida
196
+ ...
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Indicadores de calidad de un aprendizaje bien documentado
202
+
203
+ Un aprendizaje está bien documentado si:
204
+
205
+ - [ ] Tiene un ejemplo de código concreto (MAL vs. BIEN).
206
+ - [ ] La causa raíz está explicada, no solo el síntoma.
207
+ - [ ] Es accionable: quien lo lee sabe exactamente qué hacer o no hacer.
208
+ - [ ] Está en el skill correcto (no en un lugar genérico).
209
+ - [ ] El título es buscable (contiene la tecnología y el patrón).
210
+
211
+ ---
212
+
213
+ ## Frecuencia de extracción
214
+
215
+ | Momento | Acción |
216
+ |---------|--------|
217
+ | Al terminar una tarea | Revisar si hubo algún insight que documentar |
218
+ | Al resolver un bug difícil | OBLIGATORIO documentar causa raíz y solución |
219
+ | Al hacer code review | Documentar patrones observados |
220
+ | Al finalizar una fase | Revisar STATE.md y DECISIONS.md, promover aprendizajes relevantes |
221
+ | Al finalizar el proyecto | Revisar todos los aprendizajes y consolidarlos en los skills |
222
+
223
+ ---
224
+
225
+ ## Anti-patrones del proceso de extracción
226
+
227
+ - **Documentar en el momento incorrecto**: Hacerlo DURANTE o inmediatamente DESPUÉS del
228
+ error, no días después cuando el contexto se pierde.
229
+ - **Ser demasiado genérico**: "No cometer errores en SQLAlchemy" no es útil.
230
+ "NUNCA acceder a relaciones lazy en async fuera de session" sí lo es.
231
+ - **Duplicar reglas**: Antes de agregar una regla, buscar si ya existe en el skill.
232
+ - **No incluir ejemplo de código**: Las reglas sin código concreto se olvidan.
233
+ - **Documentar en DECISIONS.md lo que debería estar en el skill**: Las decisiones de
234
+ proyecto van en DECISIONS.md; los patrones reutilizables van en skills.
@@ -0,0 +1,368 @@
1
+ ---
2
+ name: fastapi-experto
3
+ description: FastAPI con Pydantic v2, dependency injection, middleware, async patterns, OpenAPI y testing con httpx. Incluye anti-patrones críticos de SQLAlchemy async.
4
+ ---
5
+
6
+ # FastAPI Experto
7
+
8
+ ## Estructura de proyecto recomendada
9
+
10
+ ```
11
+ backend/
12
+ ├── app/
13
+ │ ├── main.py # App factory, lifespan, middleware
14
+ │ ├── routers/ # Endpoints agrupados por dominio
15
+ │ │ ├── __init__.py
16
+ │ │ ├── usuarios.py
17
+ │ │ └── facturas.py
18
+ │ ├── schemas/ # Pydantic v2 — entrada/salida de API
19
+ │ │ ├── usuario.py
20
+ │ │ └── factura.py
21
+ │ ├── models/ # SQLAlchemy ORM
22
+ │ │ ├── base.py
23
+ │ │ ├── usuario.py
24
+ │ │ └── factura.py
25
+ │ ├── services/ # Lógica de negocio (sin commit)
26
+ │ │ └── factura_service.py
27
+ │ ├── dependencies/ # Inyección de dependencias
28
+ │ │ ├── auth.py
29
+ │ │ └── database.py
30
+ │ └── core/
31
+ │ ├── config.py # Settings con pydantic-settings
32
+ │ └── security.py # JWT, hashing
33
+ ```
34
+
35
+ ---
36
+
37
+ ## App factory con lifespan
38
+
39
+ ```python
40
+ # app/main.py
41
+ from contextlib import asynccontextmanager
42
+ from fastapi import FastAPI
43
+ from fastapi.middleware.cors import CORSMiddleware
44
+ from app.core.config import settings
45
+ from app.routers import usuarios, facturas
46
+
47
+ @asynccontextmanager
48
+ async def lifespan(app: FastAPI):
49
+ # Startup: inicializar recursos
50
+ await database.connect()
51
+ yield
52
+ # Shutdown: liberar recursos
53
+ await database.disconnect()
54
+
55
+ def create_app() -> FastAPI:
56
+ app = FastAPI(
57
+ title=settings.APP_NOMBRE,
58
+ version=settings.APP_VERSION,
59
+ lifespan=lifespan,
60
+ docs_url="/docs" if settings.DEBUG else None,
61
+ )
62
+
63
+ app.add_middleware(
64
+ CORSMiddleware,
65
+ allow_origins=settings.CORS_ORIGINS,
66
+ allow_credentials=True,
67
+ allow_methods=["*"],
68
+ allow_headers=["*"],
69
+ )
70
+
71
+ app.include_router(usuarios.router, prefix="/api/v1")
72
+ app.include_router(facturas.router, prefix="/api/v1")
73
+ return app
74
+
75
+ app = create_app()
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Schemas Pydantic v2
81
+
82
+ ```python
83
+ # app/schemas/factura.py
84
+ from pydantic import BaseModel, Field, model_validator, field_validator
85
+ from typing import Literal
86
+ from datetime import date
87
+ from decimal import Decimal
88
+
89
+ class FacturaBase(BaseModel):
90
+ folio: str = Field(..., min_length=1, max_length=20)
91
+ fecha: date
92
+ subtotal: Decimal = Field(..., ge=0, decimal_places=2)
93
+
94
+ class FacturaCreate(FacturaBase):
95
+ cliente_id: str
96
+ # Literal[] obliga a valores del dominio
97
+ estatus: Literal["borrador", "emitida", "cancelada"] = "borrador"
98
+
99
+ class FacturaRead(FacturaBase):
100
+ id: str
101
+ estatus: Literal["borrador", "emitida", "cancelada"]
102
+ nombre_cliente: str # viene de relación ORM, no de columna directa
103
+
104
+ model_config = {"from_attributes": True}
105
+
106
+ @model_validator(mode="wrap")
107
+ @classmethod
108
+ def extraer_nombre_cliente(cls, value, handler):
109
+ # Extraer campo de relación ORM antes de validar
110
+ if hasattr(value, "cliente"):
111
+ value.__dict__["nombre_cliente"] = (
112
+ value.cliente.nombre if value.cliente else ""
113
+ )
114
+ return handler(value)
115
+
116
+ class PaginatedResponse[T](BaseModel):
117
+ items: list[T]
118
+ total: int
119
+ page: int
120
+ page_size: int
121
+ has_next: bool
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Dependency Injection
127
+
128
+ ```python
129
+ # app/dependencies/database.py
130
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
131
+ from typing import AsyncGenerator
132
+
133
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
134
+ async with session_factory() as session:
135
+ try:
136
+ yield session
137
+ except Exception:
138
+ await session.rollback()
139
+ raise
140
+
141
+ # app/dependencies/auth.py
142
+ from fastapi import Depends, HTTPException, status
143
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
144
+ from app.core.security import decode_jwt
145
+
146
+ bearer = HTTPBearer()
147
+
148
+ async def get_current_user(
149
+ credentials: HTTPAuthorizationCredentials = Depends(bearer),
150
+ db: AsyncSession = Depends(get_db),
151
+ ) -> Usuario:
152
+ payload = decode_jwt(credentials.credentials)
153
+ if not payload:
154
+ raise HTTPException(status_code=401, detail="Token inválido")
155
+ usuario = await db.get(Usuario, payload["sub"])
156
+ if not usuario or not usuario.activo:
157
+ raise HTTPException(status_code=401, detail="Usuario inactivo")
158
+ return usuario
159
+
160
+ def require_role(roles: list[str]):
161
+ async def verificar_rol(
162
+ usuario: Usuario = Depends(get_current_user),
163
+ ) -> Usuario:
164
+ if usuario.rol not in roles:
165
+ raise HTTPException(
166
+ status_code=403,
167
+ detail=f"Rol requerido: {roles}. Rol actual: {usuario.rol}"
168
+ )
169
+ return usuario
170
+ return verificar_rol
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Endpoints con patrones correctos
176
+
177
+ ```python
178
+ # app/routers/facturas.py
179
+ from fastapi import APIRouter, Depends, HTTPException, Query
180
+ from sqlalchemy.ext.asyncio import AsyncSession
181
+ from sqlalchemy import select, func
182
+ from sqlalchemy.orm import selectinload
183
+
184
+ router = APIRouter(prefix="/facturas", tags=["Facturas"])
185
+
186
+ # Rutas estáticas ANTES de paramétricas
187
+ @router.get("/exportar", response_model=list[FacturaRead])
188
+ async def exportar_facturas(
189
+ formato: Literal["csv", "xlsx"] = Query("csv"),
190
+ db: AsyncSession = Depends(get_db),
191
+ usuario: Usuario = Depends(get_current_user),
192
+ ):
193
+ ...
194
+
195
+ @router.get("/{factura_id}", response_model=FacturaRead)
196
+ async def obtener_factura(
197
+ factura_id: str,
198
+ db: AsyncSession = Depends(get_db),
199
+ usuario: Usuario = Depends(get_current_user),
200
+ ):
201
+ query = (
202
+ select(Factura)
203
+ .where(Factura.id == factura_id)
204
+ # OBLIGATORIO: selectinload para todas las relaciones usadas en el schema
205
+ .options(selectinload(Factura.cliente))
206
+ )
207
+ result = await db.execute(query)
208
+ factura = result.scalar_one_or_none()
209
+
210
+ if not factura:
211
+ raise HTTPException(status_code=404, detail="Factura no encontrada")
212
+
213
+ # IDOR: validar que el recurso pertenece al usuario
214
+ if factura.empresa_id != usuario.empresa_id:
215
+ raise HTTPException(status_code=403, detail="Acceso denegado")
216
+
217
+ return factura
218
+
219
+ @router.post("/", response_model=FacturaRead, status_code=201)
220
+ async def crear_factura(
221
+ datos: FacturaCreate,
222
+ db: AsyncSession = Depends(get_db),
223
+ usuario: Usuario = Depends(get_current_user),
224
+ _: Usuario = Depends(require_role(["ADMIN", "FACTURISTA"])),
225
+ ):
226
+ # Service no hace commit
227
+ factura = await factura_service.crear(db, datos, owner=usuario)
228
+ await db.commit()
229
+ await db.refresh(factura)
230
+ # Recargar con relaciones para el schema de respuesta
231
+ await db.refresh(factura, ["cliente"])
232
+ return factura
233
+ ```
234
+
235
+ ---
236
+
237
+ ## SQLAlchemy Async — reglas críticas
238
+
239
+ ### NUNCA lazy loading en async
240
+
241
+ ```python
242
+ # MAL — causa MissingGreenlet
243
+ factura = await db.get(Factura, factura_id)
244
+ print(factura.cliente.nombre) # Error: acceso lazy fuera de sesión
245
+
246
+ # BIEN — selectinload explícito
247
+ query = select(Factura).where(Factura.id == factura_id).options(
248
+ selectinload(Factura.cliente)
249
+ )
250
+ result = await db.execute(query)
251
+ factura = result.scalar_one()
252
+ print(factura.cliente.nombre) # OK
253
+ ```
254
+
255
+ ### Services: flush, no commit
256
+
257
+ ```python
258
+ # app/services/factura_service.py
259
+ class FacturaService:
260
+ async def crear(
261
+ self, db: AsyncSession, datos: FacturaCreate, owner: Usuario
262
+ ) -> Factura:
263
+ factura = Factura(
264
+ **datos.model_dump(),
265
+ creado_por=owner.email,
266
+ empresa_id=owner.empresa_id,
267
+ )
268
+ db.add(factura)
269
+ await db.flush() # Obtiene ID sin commitear
270
+ await db.refresh(factura)
271
+ return factura
272
+ # NO hacer commit aquí — el endpoint es responsable
273
+ ```
274
+
275
+ ### lazy="selectin" para relaciones frecuentes
276
+
277
+ ```python
278
+ class Factura(Base):
279
+ __tablename__ = "facturas"
280
+
281
+ id: Mapped[str] = mapped_column(primary_key=True)
282
+ cliente_id: Mapped[str] = mapped_column(ForeignKey("clientes.id"))
283
+
284
+ # Para relaciones accedidas en casi todos los queries
285
+ cliente: Mapped["Cliente"] = relationship(lazy="selectin")
286
+ # Para relaciones accedidas raramente — cargar con selectinload cuando se necesite
287
+ items: Mapped[list["ItemFactura"]] = relationship(lazy="select")
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Middleware personalizado
293
+
294
+ ```python
295
+ import time
296
+ import uuid
297
+ from fastapi import Request, Response
298
+
299
+ async def middleware_request_id(request: Request, call_next):
300
+ request_id = str(uuid.uuid4())
301
+ request.state.request_id = request_id
302
+ inicio = time.time()
303
+ response: Response = await call_next(request)
304
+ duracion = time.time() - inicio
305
+ response.headers["X-Request-ID"] = request_id
306
+ response.headers["X-Response-Time"] = f"{duracion:.4f}s"
307
+ return response
308
+
309
+ app.middleware("http")(middleware_request_id)
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Testing con httpx
315
+
316
+ ```python
317
+ # tests/test_facturas.py
318
+ import pytest
319
+ from httpx import AsyncClient, ASGITransport
320
+ from app.main import app
321
+
322
+ @pytest.fixture
323
+ async def client(db_session):
324
+ async with AsyncClient(
325
+ transport=ASGITransport(app=app),
326
+ base_url="http://test",
327
+ ) as ac:
328
+ yield ac
329
+
330
+ @pytest.mark.asyncio
331
+ async def test_crear_factura(client, token_admin, factura_valida):
332
+ respuesta = await client.post(
333
+ "/api/v1/facturas/",
334
+ json=factura_valida,
335
+ headers={"Authorization": f"Bearer {token_admin}"},
336
+ )
337
+ assert respuesta.status_code == 201
338
+ data = respuesta.json()
339
+ assert data["estatus"] == "borrador"
340
+ assert "id" in data
341
+
342
+ @pytest.mark.asyncio
343
+ async def test_factura_requiere_autenticacion(client):
344
+ respuesta = await client.post("/api/v1/facturas/", json={})
345
+ assert respuesta.status_code == 403 # Sin token
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Configuración con pydantic-settings
351
+
352
+ ```python
353
+ # app/core/config.py
354
+ from pydantic_settings import BaseSettings, SettingsConfigDict
355
+
356
+ class Settings(BaseSettings):
357
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
358
+
359
+ APP_NOMBRE: str = "Mi API"
360
+ APP_VERSION: str = "1.0.0"
361
+ DEBUG: bool = False
362
+ DATABASE_URL: str
363
+ SECRET_KEY: str
364
+ CORS_ORIGINS: list[str] = ["http://localhost:4200"]
365
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
366
+
367
+ settings = Settings()
368
+ ```