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.
- package/CLAUDE.md +425 -0
- package/_userland/agentes/.gitkeep +0 -0
- package/_userland/habilidades/.gitkeep +0 -0
- package/agentes/accesibilidad-wcag-swl.md +683 -0
- package/agentes/arquitecto-swl.md +210 -0
- package/agentes/auto-evolucion-swl.md +408 -0
- package/agentes/backend-api-swl.md +442 -0
- package/agentes/backend-node-swl.md +439 -0
- package/agentes/backend-python-swl.md +469 -0
- package/agentes/backend-workers-swl.md +444 -0
- package/agentes/cloud-infra-swl.md +466 -0
- package/agentes/consolidador-swl.md +487 -0
- package/agentes/datos-swl.md +568 -0
- package/agentes/depurador-swl.md +301 -0
- package/agentes/devops-ci-swl.md +352 -0
- package/agentes/disenador-ui-swl.md +546 -0
- package/agentes/documentador-swl.md +323 -0
- package/agentes/frontend-angular-swl.md +603 -0
- package/agentes/frontend-css-swl.md +700 -0
- package/agentes/frontend-react-swl.md +672 -0
- package/agentes/frontend-swl.md +483 -0
- package/agentes/frontend-tailwind-swl.md +808 -0
- package/agentes/implementador-swl.md +235 -0
- package/agentes/investigador-swl.md +274 -0
- package/agentes/investigador-ux-swl.md +482 -0
- package/agentes/migrador-swl.md +389 -0
- package/agentes/mobile-android-swl.md +473 -0
- package/agentes/mobile-cross-swl.md +501 -0
- package/agentes/mobile-ios-swl.md +464 -0
- package/agentes/notificador-swl.md +886 -0
- package/agentes/observabilidad-swl.md +408 -0
- package/agentes/orquestador-swl.md +490 -0
- package/agentes/planificador-swl.md +222 -0
- package/agentes/producto-prd-swl.md +565 -0
- package/agentes/release-manager-swl.md +545 -0
- package/agentes/rendimiento-swl.md +691 -0
- package/agentes/revisor-codigo-swl.md +254 -0
- package/agentes/revisor-seguridad-swl.md +316 -0
- package/agentes/tdd-qa-swl.md +323 -0
- package/agentes/ux-disenador-swl.md +498 -0
- package/bin/swl-ses.js +119 -0
- package/comandos/swl/actualizar.md +117 -0
- package/comandos/swl/aprender.md +348 -0
- package/comandos/swl/auditar-deps.md +390 -0
- package/comandos/swl/autoresearch.md +346 -0
- package/comandos/swl/checkpoint.md +296 -0
- package/comandos/swl/compactar.md +283 -0
- package/comandos/swl/crear-skill.md +609 -0
- package/comandos/swl/discutir-fase.md +230 -0
- package/comandos/swl/ejecutar-fase.md +302 -0
- package/comandos/swl/evolucionar.md +377 -0
- package/comandos/swl/instalar.md +220 -0
- package/comandos/swl/mapear-codebase.md +205 -0
- package/comandos/swl/nuevo-proyecto.md +154 -0
- package/comandos/swl/planear-fase.md +221 -0
- package/comandos/swl/release.md +405 -0
- package/comandos/swl/salud.md +382 -0
- package/comandos/swl/verificar.md +292 -0
- package/habilidades/accesibilidad-a11y/SKILL.md +584 -0
- package/habilidades/angular-avanzado/SKILL.md +491 -0
- package/habilidades/angular-moderno/SKILL.md +326 -0
- package/habilidades/api-rest-diseno/SKILL.md +302 -0
- package/habilidades/api-rest-diseno/recursos/openapi-template.yaml +506 -0
- package/habilidades/aprendizaje-continuo/SKILL.md +369 -0
- package/habilidades/async-python/SKILL.md +474 -0
- package/habilidades/auth-patrones/SKILL.md +488 -0
- package/habilidades/auto-evolucion-protocolo/SKILL.md +376 -0
- package/habilidades/autoresearch/SKILL.md +248 -0
- package/habilidades/autoresearch/recursos/checklist-template.md +191 -0
- package/habilidades/autoresearch/scripts/calcular-score.js +88 -0
- package/habilidades/checklist-calidad/SKILL.md +247 -0
- package/habilidades/checklist-calidad/recursos/quality-report-template.md +148 -0
- package/habilidades/checklist-seguridad/SKILL.md +224 -0
- package/habilidades/checkpoints-verificacion/SKILL.md +309 -0
- package/habilidades/checkpoints-verificacion/recursos/checkpoint-templates.md +360 -0
- package/habilidades/ci-cd-pipelines/SKILL.md +583 -0
- package/habilidades/ci-cd-pipelines/recursos/github-actions-template.yaml +403 -0
- package/habilidades/cloud-aws/SKILL.md +497 -0
- package/habilidades/compactacion-contexto/SKILL.md +201 -0
- package/habilidades/contenedores-docker/SKILL.md +453 -0
- package/habilidades/contenedores-docker/recursos/dockerfile-template.dockerfile +160 -0
- package/habilidades/css-moderno/SKILL.md +463 -0
- package/habilidades/datos-etl/SKILL.md +486 -0
- package/habilidades/dependencias-auditoria/SKILL.md +293 -0
- package/habilidades/deprecacion-migracion/SKILL.md +485 -0
- package/habilidades/design-tokens/SKILL.md +519 -0
- package/habilidades/discutir-fase/SKILL.md +167 -0
- package/habilidades/diseno-responsivo/SKILL.md +326 -0
- package/habilidades/django-experto/SKILL.md +395 -0
- package/habilidades/doc-sync/SKILL.md +259 -0
- package/habilidades/ejecutar-fase/SKILL.md +199 -0
- package/habilidades/estructura-proyecto-claude/SKILL.md +459 -0
- package/habilidades/estructura-proyecto-claude/recursos/claude-md-template.md +261 -0
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +213 -0
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +77 -0
- package/habilidades/estructura-proyecto-claude/recursos/variantes-por-stack.md +177 -0
- package/habilidades/event-driven/SKILL.md +580 -0
- package/habilidades/extractor-de-aprendizajes/SKILL.md +234 -0
- package/habilidades/fastapi-experto/SKILL.md +368 -0
- package/habilidades/frontend-avanzado/SKILL.md +555 -0
- package/habilidades/git-worktrees-paralelo/SKILL.md +246 -0
- package/habilidades/iam-secretos/SKILL.md +511 -0
- package/habilidades/instalar-sistema/SKILL.md +140 -0
- package/habilidades/kubernetes-orquestacion/SKILL.md +549 -0
- package/habilidades/manejo-errores/SKILL.md +512 -0
- package/habilidades/mapear-codebase/SKILL.md +199 -0
- package/habilidades/microservicios/SKILL.md +473 -0
- package/habilidades/mobile-flutter/SKILL.md +566 -0
- package/habilidades/mobile-react-native/SKILL.md +493 -0
- package/habilidades/monitoring-alertas/SKILL.md +447 -0
- package/habilidades/node-experto/SKILL.md +521 -0
- package/habilidades/notificaciones-multicanal/SKILL.md +448 -0
- package/habilidades/notificaciones-multicanal/recursos/config-template.json +115 -0
- package/habilidades/nuevo-proyecto/SKILL.md +183 -0
- package/habilidades/patrones-python/SKILL.md +381 -0
- package/habilidades/performance-baseline/SKILL.md +243 -0
- package/habilidades/planear-fase/SKILL.md +184 -0
- package/habilidades/postgresql-experto/SKILL.md +379 -0
- package/habilidades/react-experto/SKILL.md +434 -0
- package/habilidades/react-optimizacion/SKILL.md +328 -0
- package/habilidades/release-semver/SKILL.md +226 -0
- package/habilidades/release-semver/scripts/generar-changelog.sh +238 -0
- package/habilidades/sql-optimizacion/SKILL.md +314 -0
- package/habilidades/tailwind-experto/SKILL.md +412 -0
- package/habilidades/tdd-workflow/SKILL.md +267 -0
- package/habilidades/testing-python/SKILL.md +350 -0
- package/habilidades/threat-model-lite/SKILL.md +218 -0
- package/habilidades/typescript-avanzado/SKILL.md +454 -0
- package/habilidades/ux-diseno/SKILL.md +488 -0
- package/habilidades/validacion-ci-sistema/SKILL.md +543 -0
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +286 -0
- package/habilidades/verificar-trabajo/SKILL.md +208 -0
- package/habilidades/wireframes-flujos/SKILL.md +396 -0
- package/habilidades/workflow-claude-code/SKILL.md +359 -0
- package/hooks/calidad-pre-commit.js +578 -0
- package/hooks/escaneo-secretos.js +302 -0
- package/hooks/extraccion-aprendizajes.js +550 -0
- package/hooks/linea-estado.js +249 -0
- package/hooks/monitor-contexto.js +230 -0
- package/hooks/proteccion-rutas.js +249 -0
- package/manifiestos/hooks-config.json +41 -0
- package/manifiestos/modulos.json +318 -0
- package/manifiestos/perfiles.json +189 -0
- package/package.json +45 -0
- package/plantillas/PROJECT.md +122 -0
- package/plantillas/REQUIREMENTS.md +132 -0
- package/plantillas/ROADMAP.md +143 -0
- package/plantillas/STATE.md +109 -0
- package/plantillas/research/ARCHITECTURE.md +220 -0
- package/plantillas/research/FEATURES.md +175 -0
- package/plantillas/research/PITFALLS.md +299 -0
- package/plantillas/research/STACK.md +233 -0
- package/plantillas/research/SUMMARY.md +165 -0
- package/plugin.json +144 -0
- package/reglas/accesibilidad.md +269 -0
- package/reglas/api-diseno.md +400 -0
- package/reglas/arquitectura.md +183 -0
- package/reglas/cloud-infra.md +247 -0
- package/reglas/docs.md +245 -0
- package/reglas/estilo-codigo.md +179 -0
- package/reglas/git-workflow.md +186 -0
- package/reglas/performance.md +195 -0
- package/reglas/pruebas.md +159 -0
- package/reglas/seguridad.md +151 -0
- package/reglas/skills-estandar.md +473 -0
- package/scripts/actualizar.js +51 -0
- package/scripts/desinstalar.js +86 -0
- package/scripts/doctor.js +222 -0
- package/scripts/inicializar.js +89 -0
- package/scripts/instalador.js +333 -0
- package/scripts/lib/detectar-runtime.js +177 -0
- package/scripts/lib/estado.js +112 -0
- package/scripts/lib/hooks-settings.js +283 -0
- package/scripts/lib/manifiestos.js +138 -0
- package/scripts/lib/seguridad.js +160 -0
- package/scripts/publicar.js +209 -0
- 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
|
+
```
|