colombia-open 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- colombia_open-0.3.0/.claude/agents/data-quality-reviewer.md +86 -0
- colombia_open-0.3.0/.claude/commands/next-source.md +25 -0
- colombia_open-0.3.0/.claude/rules/extractors.md +128 -0
- colombia_open-0.3.0/.claude/rules/releases.md +101 -0
- colombia_open-0.3.0/.claude/rules/validation.md +93 -0
- colombia_open-0.3.0/.claude/skills/new-source/SKILL.md +134 -0
- colombia_open-0.3.0/.github/FUNDING.yml +2 -0
- colombia_open-0.3.0/.github/workflows/pages-deploy.yml +33 -0
- colombia_open-0.3.0/.github/workflows/pipeline-check.yml +97 -0
- colombia_open-0.3.0/.github/workflows/publish.yml +58 -0
- colombia_open-0.3.0/.github/workflows/release.yml +43 -0
- colombia_open-0.3.0/.github/workflows/source-urls.yml +20 -0
- colombia_open-0.3.0/.gitignore +36 -0
- colombia_open-0.3.0/.pre-commit-config.yaml +26 -0
- colombia_open-0.3.0/.python-version +1 -0
- colombia_open-0.3.0/CLAUDE.md +154 -0
- colombia_open-0.3.0/LICENSE +26 -0
- colombia_open-0.3.0/Makefile +50 -0
- colombia_open-0.3.0/PKG-INFO +242 -0
- colombia_open-0.3.0/README.md +181 -0
- colombia_open-0.3.0/data/registry/datasets.json +69 -0
- colombia_open-0.3.0/docs/brand/design-system.md +88 -0
- colombia_open-0.3.0/docs/datasets/demografia.md +68 -0
- colombia_open-0.3.0/docs/datasets/departamentos.md +41 -0
- colombia_open-0.3.0/docs/datasets/educacion_establecimientos.md +105 -0
- colombia_open-0.3.0/docs/datasets/municipios.md +46 -0
- colombia_open-0.3.0/docs/licenses/data_licenses.md +86 -0
- colombia_open-0.3.0/docs/roadmap/roadmap.md +103 -0
- colombia_open-0.3.0/examples/notebooks/01_departamentos.ipynb +126 -0
- colombia_open-0.3.0/examples/notebooks/02_municipios.ipynb +155 -0
- colombia_open-0.3.0/examples/notebooks/03_demografia.ipynb +166 -0
- colombia_open-0.3.0/examples/notebooks/04_educacion_establecimientos.ipynb +170 -0
- colombia_open-0.3.0/examples/notebooks/README.md +15 -0
- colombia_open-0.3.0/pyproject.toml +117 -0
- colombia_open-0.3.0/scripts/check_source_urls.py +42 -0
- colombia_open-0.3.0/scripts/check_validation_registration.py +33 -0
- colombia_open-0.3.0/scripts/make_bundle.py +39 -0
- colombia_open-0.3.0/scripts/verify_pipeline.py +56 -0
- colombia_open-0.3.0/site/assets/brand/mark-gold.svg +14 -0
- colombia_open-0.3.0/site/assets/brand/mark-indigo.svg +14 -0
- colombia_open-0.3.0/site/favicon.svg +14 -0
- colombia_open-0.3.0/site/index.html +243 -0
- colombia_open-0.3.0/site/styles.css +228 -0
- colombia_open-0.3.0/src/__init__.py +0 -0
- colombia_open-0.3.0/src/build_pipeline.py +131 -0
- colombia_open-0.3.0/src/builders/__init__.py +0 -0
- colombia_open-0.3.0/src/builders/_shared.py +15 -0
- colombia_open-0.3.0/src/builders/catalog.py +52 -0
- colombia_open-0.3.0/src/builders/datasets.py +57 -0
- colombia_open-0.3.0/src/builders/formats.py +39 -0
- colombia_open-0.3.0/src/colombia_open/__init__.py +27 -0
- colombia_open-0.3.0/src/colombia_open/__main__.py +4 -0
- colombia_open-0.3.0/src/colombia_open/cli.py +138 -0
- colombia_open-0.3.0/src/colombia_open/core.py +251 -0
- colombia_open-0.3.0/src/colombia_open/data_manager.py +129 -0
- colombia_open-0.3.0/src/colombia_open/datasets.py +29 -0
- colombia_open-0.3.0/src/colombia_open/exceptions.py +23 -0
- colombia_open-0.3.0/src/colombia_open/py.typed +0 -0
- colombia_open-0.3.0/src/colombia_open/text.py +18 -0
- colombia_open-0.3.0/src/extractors/__init__.py +0 -0
- colombia_open-0.3.0/src/extractors/base.py +185 -0
- colombia_open-0.3.0/src/extractors/demografia_extractor.py +241 -0
- colombia_open-0.3.0/src/extractors/divipola_extractor.py +214 -0
- colombia_open-0.3.0/src/extractors/educacion_establecimientos_extractor.py +461 -0
- colombia_open-0.3.0/src/extractors/http_utils.py +44 -0
- colombia_open-0.3.0/src/registry/__init__.py +4 -0
- colombia_open-0.3.0/src/registry/dataset_spec.py +60 -0
- colombia_open-0.3.0/src/validation/__init__.py +27 -0
- colombia_open-0.3.0/src/validation/_constants.py +30 -0
- colombia_open-0.3.0/src/validation/_demografia.py +114 -0
- colombia_open-0.3.0/src/validation/_educacion.py +149 -0
- colombia_open-0.3.0/src/validation/_helpers.py +54 -0
- colombia_open-0.3.0/src/validation/_territorio.py +136 -0
- colombia_open-0.3.0/src/validation/result.py +27 -0
- colombia_open-0.3.0/tests/builders/test_datasets.py +36 -0
- colombia_open-0.3.0/tests/extractors/test_demografia_extractor.py +77 -0
- colombia_open-0.3.0/tests/extractors/test_divipola_extractor.py +66 -0
- colombia_open-0.3.0/tests/extractors/test_educacion_establecimientos_extractor.py +87 -0
- colombia_open-0.3.0/tests/test_core_api.py +219 -0
- colombia_open-0.3.0/tests/test_data_manager.py +45 -0
- colombia_open-0.3.0/tests/validation/test_demografia.py +76 -0
- colombia_open-0.3.0/tests/validation/test_educacion.py +118 -0
- colombia_open-0.3.0/tests/validation/test_territorio.py +75 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-quality-reviewer
|
|
3
|
+
description: Usar PROACTIVAMENTE antes de mergear cualquier fuente nueva o cambio a un extractor/validador/builder existente — revisa que cumpla el contrato de esquema/validación de colombia-open antes de que el código se integre. Invocar también si David pide "revisa la calidad de esta fuente" o similar.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Eres el revisor de calidad de datos de colombia-open. Tu trabajo es
|
|
8
|
+
verificar que una fuente nueva (o un cambio a una existente) cumple el
|
|
9
|
+
contrato del proyecto ANTES de que se mergee — no escribes código, solo
|
|
10
|
+
reportas si pasa o qué falta.
|
|
11
|
+
|
|
12
|
+
## Qué revisar, en orden
|
|
13
|
+
|
|
14
|
+
1. **Extractor** (`.claude/rules/extractors.md`):
|
|
15
|
+
- ¿`fetch()` tiene try-en-vivo + except-a-fallback, nunca lanza si hay
|
|
16
|
+
fallback posible?
|
|
17
|
+
- ¿El `except` captura solo fallas esperadas de la fuente
|
|
18
|
+
(`RequestException`, `KeyError`, `ValueError`, `TypeError`), nunca un
|
|
19
|
+
`except Exception` desnudo que esconda bugs de programación como si
|
|
20
|
+
fueran "fuente caída"?
|
|
21
|
+
- ¿El fallback está claramente marcado `source_mode="fallback"` y es
|
|
22
|
+
obviamente una muestra mínima, no datos reales disfrazados?
|
|
23
|
+
- ¿Los códigos administrativos se normalizan a string de ancho fijo
|
|
24
|
+
(`.zfill(n)`) en el extractor, nunca dejados como int?
|
|
25
|
+
- ¿Usa `write_raw_snapshot_atomic`/`write_staging_csv_atomic` (nunca
|
|
26
|
+
escritura manual no atómica)?
|
|
27
|
+
- ¿Usa importaciones absolutas normales (sin manipular `sys.path` ni
|
|
28
|
+
try/except de importación) y se invoca como módulo
|
|
29
|
+
(`python -m src.extractors.{name}_extractor`)?
|
|
30
|
+
- ¿`REUSE_POLICY` refleja la licencia real verificada de la fuente
|
|
31
|
+
(no un default asumido)?
|
|
32
|
+
|
|
33
|
+
2. **Validación** (`.claude/rules/validation.md`):
|
|
34
|
+
- ¿`validate_{dataset}()` devuelve siempre un `ValidationResult`
|
|
35
|
+
tipado (`src/validation/result.py`), nunca un dict plano, y nunca
|
|
36
|
+
lanza?
|
|
37
|
+
- ¿Valida: no-vacío, unicidad de clave primaria, ancho fijo de
|
|
38
|
+
cualquier código administrativo, y conteo mínimo si
|
|
39
|
+
`source_mode="live"`?
|
|
40
|
+
- ¿Si el dataset referencia códigos de otro dataset, valida integridad
|
|
41
|
+
referencial?
|
|
42
|
+
- ¿Está registrada en `src/validation/__init__.py::VALIDATORS`? Corre
|
|
43
|
+
`python scripts/check_validation_registration.py` para confirmar —
|
|
44
|
+
no lo asumas leyendo el código, ejecútalo.
|
|
45
|
+
|
|
46
|
+
3. **Registro** (`data/registry/datasets.json`):
|
|
47
|
+
- ¿La entrada tiene `description`, `extractor`, `source_name`,
|
|
48
|
+
`join_keys`, `outputs`, `documentation`, `usage_examples`?
|
|
49
|
+
- ¿`expected_record_count` es razonable para la fuente real (no un
|
|
50
|
+
placeholder tipo `0` o `999999`)?
|
|
51
|
+
|
|
52
|
+
4. **Tests**:
|
|
53
|
+
- Corre `python -m pytest -v` sobre los tests nuevos. ¿Cubren: fallback
|
|
54
|
+
con códigos de ancho correcto, dry_run sin escritura, caso feliz de
|
|
55
|
+
validación, al menos un caso de error por regla, dataframe vacío,
|
|
56
|
+
warning en modo fallback?
|
|
57
|
+
|
|
58
|
+
5. **Documentación**:
|
|
59
|
+
- ¿Existe `docs/datasets/{dataset}.md` con esquema completo
|
|
60
|
+
(columna/tipo/descripción), fuente, licencia, ejemplos, limitaciones?
|
|
61
|
+
- ¿`docs/roadmap/roadmap.md` tiene la fila marcada como implementada con link al
|
|
62
|
+
extractor?
|
|
63
|
+
- ¿`src/colombia_open/datasets.py::Dataset` incluye el dataset nuevo?
|
|
64
|
+
|
|
65
|
+
6. **Gates automatizados** — corre y reporta el resultado real, no lo
|
|
66
|
+
infieras:
|
|
67
|
+
```bash
|
|
68
|
+
python -m src.build_pipeline
|
|
69
|
+
python scripts/verify_pipeline.py --require-live
|
|
70
|
+
python -m pytest
|
|
71
|
+
python -m ruff check src/ tests/
|
|
72
|
+
python -m mypy
|
|
73
|
+
python -m bandit -c pyproject.toml -r src/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Formato del reporte
|
|
77
|
+
|
|
78
|
+
Para cada punto del checklist: ✅ cumple / ⚠️ cumple parcialmente (explica
|
|
79
|
+
qué falta) / ❌ no cumple (explica el problema concreto, con archivo y
|
|
80
|
+
línea si aplica). Termina con un veredicto único: **LISTO PARA MERGEAR** o
|
|
81
|
+
**FALTA: <lista concreta y accionable>**. No hables de "calidad en
|
|
82
|
+
general" — cada hallazgo debe ser una acción concreta que alguien pueda
|
|
83
|
+
ejecutar.
|
|
84
|
+
|
|
85
|
+
No corrijas el código tú mismo salvo que David te lo pida explícitamente
|
|
86
|
+
después de ver tu reporte — tu rol aquí es revisar, no implementar.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Lee docs/roadmap/roadmap.md, propone la siguiente fuente candidata y arranca la skill new-source sobre ella.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Lee `docs/roadmap/roadmap.md` completo. De la tabla de fuentes, identifica la fila con
|
|
6
|
+
menor dificultad (🟢 > 🟡 > 🟠 > 🔴) entre las que NO están implementadas
|
|
7
|
+
todavía (columna "Estado" = ⬜).
|
|
8
|
+
|
|
9
|
+
Antes de proponerla:
|
|
10
|
+
1. Verifica el endpoint/URL actual de esa fuente con una petición real
|
|
11
|
+
(curl/fetch), no solo lo que dice la tabla — puede estar desactualizada.
|
|
12
|
+
2. Si el acceso real difiere de lo descrito (URL muerta, requiere login,
|
|
13
|
+
formato distinto), dilo explícitamente y, si cambia la dificultad
|
|
14
|
+
estimada, actualiza la fila en `docs/roadmap/roadmap.md` antes de seguir.
|
|
15
|
+
3. Presenta la fuente candidata a David con: nombre, URL verificada,
|
|
16
|
+
formato de datos, licencia (si es visible), y por qué es la de menor
|
|
17
|
+
dificultad disponible. Espera confirmación antes de empezar a
|
|
18
|
+
implementar — no asumas que la primera candidata es la correcta sin
|
|
19
|
+
que él lo confirme, puede haber una razón para priorizar otra.
|
|
20
|
+
|
|
21
|
+
Una vez confirmada, invoca la skill `new-source` sobre esa fuente (nombre +
|
|
22
|
+
URL verificada) y sigue su checklist completo.
|
|
23
|
+
|
|
24
|
+
Recuerda la regla de `CLAUDE.md`: una fuente por fin de semana. Esta
|
|
25
|
+
sesión implementa UNA fuente, de punta a punta, no varias a medias.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Contrato para cualquier archivo en src/extractors/
|
|
3
|
+
appliesTo: "src/extractors/**"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Contrato de extractores
|
|
7
|
+
|
|
8
|
+
Todo extractor implementa `BaseExtractor` (`src/extractors/base.py`) y
|
|
9
|
+
sigue este contrato. La skill `.claude/skills/new-source/SKILL.md` genera
|
|
10
|
+
un esqueleto que ya lo cumple — si estás escribiendo uno a mano, verifica
|
|
11
|
+
cada punto.
|
|
12
|
+
|
|
13
|
+
## 1. Try en vivo, fallback solo si todo falla — y solo para fallas esperadas
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
EXPECTED_FETCH_FAILURES: tuple[type[Exception], ...] = (RequestException, KeyError, ValueError, TypeError)
|
|
17
|
+
|
|
18
|
+
def fetch(self, **kwargs) -> ExtractionResult:
|
|
19
|
+
try:
|
|
20
|
+
... # fuente en vivo
|
|
21
|
+
return ExtractionResult(..., source_mode="live", ...)
|
|
22
|
+
except EXPECTED_FETCH_FAILURES as exc:
|
|
23
|
+
... # fallback embebido mínimo
|
|
24
|
+
return ExtractionResult(..., source_mode="fallback", ...)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **Nunca uses `except Exception` desnudo aquí.** Captura solo las fallas
|
|
28
|
+
que de verdad pueden venir de la fuente: errores de red/HTTP
|
|
29
|
+
(`requests.RequestException`), forma de JSON inesperada (`KeyError`,
|
|
30
|
+
`TypeError`), o un valor que no parsea (`ValueError` — esto incluye
|
|
31
|
+
`JSONDecodeError`, que hereda de `ValueError`). Un bug de programación
|
|
32
|
+
real (`AttributeError` por un typo, `NameError`, etc.) **debe reventar**,
|
|
33
|
+
no disfrazarse de "fuente caída" — un `except Exception` amplio esconde
|
|
34
|
+
justo los errores que más urgen arreglar.
|
|
35
|
+
- `fetch()` **nunca lanza** ante una falla esperada de la lista de arriba
|
|
36
|
+
si hay un fallback posible — siempre devuelve un `ExtractionResult`, con
|
|
37
|
+
`source_mode` reflejando la verdad.
|
|
38
|
+
- El fallback es una muestra mínima embebida en el propio archivo
|
|
39
|
+
(5-15 filas representativas), solo para desarrollo offline. **Nunca se
|
|
40
|
+
presenta como dato real** y **nunca se publica**:
|
|
41
|
+
`scripts/verify_pipeline.py --require-live` lo rechaza.
|
|
42
|
+
- Si de verdad no hay fallback razonable (la fuente es indispensable y no
|
|
43
|
+
hay snapshot previo), es correcto dejar que la excepción se propague —
|
|
44
|
+
mejor un build que falla fuerte que un silencio.
|
|
45
|
+
|
|
46
|
+
## 2. Snapshot crudo, write-once
|
|
47
|
+
|
|
48
|
+
Cada fetch en vivo exitoso escribe el payload crudo (antes de normalizar)
|
|
49
|
+
a `data/raw/{dataset}_{timestamp}.json` vía
|
|
50
|
+
`write_raw_snapshot_atomic()`. Nunca se edita ese archivo después de
|
|
51
|
+
escrito — es el rastro de auditoría de qué devolvió la fuente exactamente.
|
|
52
|
+
Guarda también su SHA256 (`compute_sha256()`) en el `ExtractionResult`.
|
|
53
|
+
|
|
54
|
+
## 3. Normalización: códigos administrativos como string de ancho fijo
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
"cod_dpto": str(row["cod_dpto"]).zfill(2), # nunca int
|
|
58
|
+
"cod_mpio": str(row["cod_mpio"]).zfill(5), # nunca int
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Cualquier columna que sea un código administrativo (departamento,
|
|
62
|
+
municipio, y los que vengan en fuentes futuras) se normaliza a string de
|
|
63
|
+
ancho fijo en el extractor, no después. Pandas/Polars/Excel truncan ceros
|
|
64
|
+
iniciales de un int silenciosamente — ese bug no se detecta en los tests a
|
|
65
|
+
menos que verifiques explícitamente el ancho (ver
|
|
66
|
+
`.claude/rules/validation.md`).
|
|
67
|
+
|
|
68
|
+
## 4. Staging: CSV + metadata.json, siempre juntos
|
|
69
|
+
|
|
70
|
+
`write_staging()` escribe ambos atómicamente
|
|
71
|
+
(`write_staging_csv_atomic()`). El metadata.json sigue el esquema de
|
|
72
|
+
`ExtractionResult.to_staging_metadata()`:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"dataset": "...",
|
|
77
|
+
"source_name": "...",
|
|
78
|
+
"source_url": "...",
|
|
79
|
+
"source_mode": "live" | "fallback",
|
|
80
|
+
"source_detail": "...",
|
|
81
|
+
"refreshed_at_utc": "...",
|
|
82
|
+
"record_count": 0,
|
|
83
|
+
"fields": ["..."],
|
|
84
|
+
"notes": ["..."],
|
|
85
|
+
"raw_snapshot_path": "...",
|
|
86
|
+
"snapshot_hash": "...",
|
|
87
|
+
"reuse_policy": { "status": "...", "license": "...", ... }
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Nunca se escribe el CSV sin su metadata.json hermano — un builder que lo
|
|
92
|
+
encuentre sin metadata debe fallar fuerte, no asumir valores.
|
|
93
|
+
|
|
94
|
+
## 5. Red: reintentos solo en errores de red, nunca en 4xx/5xx
|
|
95
|
+
|
|
96
|
+
Usa `src/extractors/http_utils.py::get_json`/`get_bytes` — ya tienen
|
|
97
|
+
backoff exponencial para `ConnectionError`/`Timeout` (vía `tenacity`).
|
|
98
|
+
**No reintentes en respuestas 4xx/5xx** — eso es una respuesta real del
|
|
99
|
+
servidor (dato cambió de formato, endpoint movido, bloqueo anti-bot), no
|
|
100
|
+
un problema de red transitorio; cae a fallback o deja que falle.
|
|
101
|
+
|
|
102
|
+
## 6. Siempre se invoca como módulo, nunca como script suelto
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from src.extractors.base import BaseExtractor, ExtractionResult, ...
|
|
106
|
+
from src.extractors.http_utils import get_json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Importaciones absolutas normales, sin manipular `sys.path` ni try/except
|
|
110
|
+
de importación. Esto funciona porque la convención del proyecto es
|
|
111
|
+
**invocar siempre con `-m` desde la raíz del repo**:
|
|
112
|
+
`python -m src.extractors.{name}_extractor` (así lo hace el Makefile y la
|
|
113
|
+
CI) — nunca `python src/extractors/{name}_extractor.py` directo, que
|
|
114
|
+
fallaría al importar porque `src` no quedaría en el path.
|
|
115
|
+
|
|
116
|
+
No reintroduzcas el truco de `sys.path.insert()` / `try: from src.x import
|
|
117
|
+
... except ModuleNotFoundError: from x import ...` — es deuda técnica que
|
|
118
|
+
otros proyectos cargan por compatibilidad histórica con código viejo, que
|
|
119
|
+
este proyecto no tiene. Si un extractor nuevo "necesita" ese truco para
|
|
120
|
+
funcionar, el problema real es que algo lo está invocando sin `-m`.
|
|
121
|
+
|
|
122
|
+
## 7. Si la fuente requiere scraping real (no API/descarga directa)
|
|
123
|
+
|
|
124
|
+
Eso va bajo el extra `scraping` (`curl_cffi`, `beautifulsoup4`,
|
|
125
|
+
`defusedxml`), no `pipeline`. Antes de llegar ahí: ¿de verdad no hay una
|
|
126
|
+
API JSON o un endpoint de descarga directa? Prefiere siempre eso —
|
|
127
|
+
scraping HTML es la fuente más frágil y cara de mantener (ver `docs/roadmap/roadmap.md`,
|
|
128
|
+
fuentes 🟠/🔴).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Convención de versionado y releases
|
|
2
|
+
|
|
3
|
+
`colombia-open` publica dos cosas por separado, cada una con su propio
|
|
4
|
+
workflow manual:
|
|
5
|
+
|
|
6
|
+
- **Datos**: GitHub Releases con el bundle de `data/normalized/` + SHA256
|
|
7
|
+
(`.github/workflows/release.yml`). Es lo que `ColombiaOpen()` descarga y
|
|
8
|
+
cachea automáticamente en el primer uso (ver `data_manager.py`).
|
|
9
|
+
- **Paquete**: el código de `colombia_open/` a PyPI, vía trusted
|
|
10
|
+
publishing — sin token almacenado (`.github/workflows/publish.yml`).
|
|
11
|
+
Verificado end-to-end el 2026-10-18 (build local + venv aislado +
|
|
12
|
+
descarga real del bundle), pero **todavía no se ha corrido el primer
|
|
13
|
+
publish real** — requiere que el mantenedor configure un trusted
|
|
14
|
+
publisher en pypi.org primero (ver el comentario del workflow).
|
|
15
|
+
|
|
16
|
+
Usa SemVer (`MAJOR.MINOR.PATCH`) sobre `version` en `pyproject.toml` para
|
|
17
|
+
ambos — el mismo número de versión identifica el release de datos
|
|
18
|
+
(`vX.Y.Z` en GitHub Releases) y el release del paquete (`X.Y.Z` en PyPI).
|
|
19
|
+
|
|
20
|
+
## Qué sube cada número
|
|
21
|
+
|
|
22
|
+
**PATCH** (`0.X.Y` → `0.X.(Y+1)`)
|
|
23
|
+
- Fix en un extractor/validador existente que no cambia el esquema
|
|
24
|
+
publicado ni agrega un dataset.
|
|
25
|
+
- Corrección de docs, typos, CI/pipeline sin impacto en los datos
|
|
26
|
+
publicados.
|
|
27
|
+
- Actualización de dependencias sin cambio de comportamiento.
|
|
28
|
+
|
|
29
|
+
**MINOR** (`0.X.Y` → `0.(X+1).0`)
|
|
30
|
+
- Agregar una fuente/dataset nuevo al catálogo — el caso más común en
|
|
31
|
+
este proyecto, dado el ritmo "una fuente por fin de semana" (ver
|
|
32
|
+
`CLAUDE.md`).
|
|
33
|
+
- Agregar un método nuevo a la API pública (`ColombiaOpen`) de forma
|
|
34
|
+
retrocompatible.
|
|
35
|
+
|
|
36
|
+
**MAJOR** (`X.Y.Z` → `(X+1).0.0`)
|
|
37
|
+
- Cambio de esquema que rompe compatibilidad en un dataset ya publicado
|
|
38
|
+
(renombrar/quitar una columna, cambiar su tipo).
|
|
39
|
+
- Cambio breaking en la API pública (renombrar/quitar un método, cambiar
|
|
40
|
+
una firma de forma incompatible).
|
|
41
|
+
- Mientras el proyecto esté en `0.x`, el salto real a `1.0.0` queda
|
|
42
|
+
reservado para cuando el catálogo y la API se consideren estables — no
|
|
43
|
+
es solo "un cambio grande", es ese hito específico.
|
|
44
|
+
|
|
45
|
+
## Reglas al cortar un release
|
|
46
|
+
|
|
47
|
+
1. **Confirma cuál fue el último release REAL antes de asumir el
|
|
48
|
+
siguiente número** — `pyproject.toml` puede estar desactualizado si
|
|
49
|
+
alguien olvidó bumpearlo. Nunca asumas el estado de releases solo por
|
|
50
|
+
lo que dice el código:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
gh release list
|
|
54
|
+
git fetch --tags && git tag --sort=-v:refname
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. El número de versión en `pyproject.toml` se bumpea **como parte del
|
|
58
|
+
commit que cierra el trabajo que se va a liberar** — nunca se
|
|
59
|
+
"adelanta" especulativamente antes de que el trabajo esté listo, ni se
|
|
60
|
+
reusa un número que ya corresponde a un tag publicado.
|
|
61
|
+
|
|
62
|
+
3. El tag del release **siempre coincide exactamente** con `version` en
|
|
63
|
+
`pyproject.toml` en el commit que se está liberando (ej. si
|
|
64
|
+
`pyproject.toml` dice `0.2.0`, el tag es `v0.2.0`).
|
|
65
|
+
|
|
66
|
+
4. Cortar el release (manual a propósito — ver el comentario en
|
|
67
|
+
`.github/workflows/release.yml`, es decisión del mantenedor, no algo
|
|
68
|
+
que un agente dispare sin que se le pida explícitamente):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
gh workflow run release.yml -f tag=vX.Y.Z
|
|
72
|
+
gh run watch
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
El workflow reconstruye desde las fuentes en vivo y **falla el release
|
|
76
|
+
si cualquier dataset queda en `source_mode="fallback"`**
|
|
77
|
+
(`scripts/verify_pipeline.py --require-live`) — eso es intencional, no
|
|
78
|
+
un bug a silenciar.
|
|
79
|
+
|
|
80
|
+
5. Después de un release exitoso, revisa que no haya quedado
|
|
81
|
+
documentación desactualizada referenciando el tag anterior (README
|
|
82
|
+
"Estado del proyecto", landing page, etc.) — ver el incidente real de
|
|
83
|
+
esto en el historial de commits de 2026-10-18, donde el README quedó
|
|
84
|
+
mencionando `v0.1.0` después de publicarse `v0.2.0`.
|
|
85
|
+
|
|
86
|
+
## Publicar el paquete a PyPI
|
|
87
|
+
|
|
88
|
+
Decisión del mantenedor, no algo que un agente dispare sin que se le pida
|
|
89
|
+
explícitamente (mismo criterio que el release de datos). Requiere que el
|
|
90
|
+
trusted publisher ya esté configurado en pypi.org (ver el comentario de
|
|
91
|
+
`publish.yml`) — sin eso, el paso de publicación falla con un error de
|
|
92
|
+
autenticación, no hay ningún secret que configurar en este repo.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
gh workflow run publish.yml -f expected_version=X.Y.Z
|
|
96
|
+
gh run watch
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
El workflow valida que `pyproject.toml` tenga exactamente esa versión
|
|
100
|
+
antes de construir y publicar — falla fuerte si no coincide, en vez de
|
|
101
|
+
publicar silenciosamente la versión equivocada.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Qué significa "validado" para un dataset en colombia-open
|
|
3
|
+
appliesTo: "src/validation/**"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Contrato de validación
|
|
7
|
+
|
|
8
|
+
## Qué "validado" significa aquí
|
|
9
|
+
|
|
10
|
+
Un dataset está validado cuando su función `validate_{dataset}()` devuelve
|
|
11
|
+
`status: "ok"` — eso significa, como mínimo:
|
|
12
|
+
|
|
13
|
+
1. **No está vacío.**
|
|
14
|
+
2. **La clave primaria es única** (`cod_mpio` en municipios, `cod_dpto` en
|
|
15
|
+
departamentos) — usa `_helpers.duplicate_count()`.
|
|
16
|
+
3. **Los códigos administrativos tienen el ancho fijo correcto**
|
|
17
|
+
(`_helpers.invalid_fixed_length_count()`) — 2 dígitos para
|
|
18
|
+
departamento, 5 para municipio. Esto detecta el bug clásico de "Excel/
|
|
19
|
+
pandas se comió el cero inicial" antes de que llegue a producción.
|
|
20
|
+
4. **Integridad referencial** donde aplique (`_helpers.unknown_codes_count()`)
|
|
21
|
+
— ej. todo `cod_dpto` en `municipios` debe existir en `departamentos`.
|
|
22
|
+
5. **Si `source_mode == "live"`**: el conteo de filas no puede ser
|
|
23
|
+
sospechosamente bajo (`EXPECTED_*_MIN_COUNT` en `_constants.py`) — una
|
|
24
|
+
fuente que de repente devuelve 10 filas en vez de 1100 es una fuente
|
|
25
|
+
rota, no un dataset pequeño.
|
|
26
|
+
|
|
27
|
+
## Forma del resultado — nunca lanza, y está tipado
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from .result import ValidationResult
|
|
31
|
+
|
|
32
|
+
def validate_X(df: pl.DataFrame, metadata: dict | None = None) -> ValidationResult:
|
|
33
|
+
return ValidationResult(
|
|
34
|
+
dataset="X",
|
|
35
|
+
status="error" if errors else "ok",
|
|
36
|
+
record_count=df.height,
|
|
37
|
+
errors=[...], # lista de strings — problemas que bloquean publicación
|
|
38
|
+
warnings=[...], # lista de strings — no bloquean, pero se reportan
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`ValidationResult` (`src/validation/result.py`) es un `dataclass` frozen,
|
|
43
|
+
no un `dict[str, Any]` — así mypy chequea de verdad los campos (`.status`,
|
|
44
|
+
`.errors`, `.warnings`) en vez de aceptar cualquier llave. Tiene un método
|
|
45
|
+
`.to_dict()` solo para la frontera pública (`colombia_open.core.
|
|
46
|
+
ColombiaOpen.validate_dataset()` lo usa para mantener su firma pública
|
|
47
|
+
`dict[str, Any]`) — el código interno del pipeline siempre usa el objeto
|
|
48
|
+
tipado, nunca el dict.
|
|
49
|
+
|
|
50
|
+
Un validador **nunca lanza una excepción por un problema de calidad de
|
|
51
|
+
datos** — siempre devuelve un `ValidationResult`. Quien llama
|
|
52
|
+
(`build_pipeline.py`, `scripts/verify_pipeline.py`, el CLI `colombia-open
|
|
53
|
+
validate`) decide si `status == "error"` debe detener el build. Esto
|
|
54
|
+
mantiene a los validadores como funciones puras, fáciles de testear con un
|
|
55
|
+
DataFrame construido a mano (accede a los campos por atributo:
|
|
56
|
+
`result.status`, `result.errors`) — ver
|
|
57
|
+
`tests/validation/test_territorio.py` como plantilla.
|
|
58
|
+
|
|
59
|
+
## Dónde vive cada pieza
|
|
60
|
+
|
|
61
|
+
- Un módulo por dominio bajo `src/validation/_{dominio}.py` (ej.
|
|
62
|
+
`_territorio.py` para departamentos/municipios). Nunca valides inline en
|
|
63
|
+
`build_pipeline.py` — siempre en su propio módulo.
|
|
64
|
+
- Primitivas compartidas en `_helpers.py` (no dupliques
|
|
65
|
+
`duplicate_count`/`missing_columns`/etc. dentro de un módulo de dominio).
|
|
66
|
+
- Constantes específicas del dominio (bounding box, conteos esperados) en
|
|
67
|
+
`_constants.py`.
|
|
68
|
+
- **Registro obligatorio**: toda función `validate_*` se exporta en
|
|
69
|
+
`src/validation/__init__.py` y se agrega al dict `VALIDATORS`.
|
|
70
|
+
`scripts/check_validation_registration.py` (parte de `make doctor`)
|
|
71
|
+
falla si un dataset del registro no tiene validador registrado — un
|
|
72
|
+
validador que exists pero no está en `VALIDATORS` se salta en silencio
|
|
73
|
+
en el build, así que esto NO es opcional.
|
|
74
|
+
|
|
75
|
+
## Warnings esperados vs. errores
|
|
76
|
+
|
|
77
|
+
- `source_mode == "fallback"` siempre agrega un **warning**, nunca un
|
|
78
|
+
error — el fallback es válido para desarrollo, solo no se publica (eso
|
|
79
|
+
lo decide `scripts/verify_pipeline.py --require-live`, no el validador).
|
|
80
|
+
- Coordenadas fuera del bounding box de Colombia (`_constants.py`) son
|
|
81
|
+
**warning**, no error — podría ser un dato legítimo mal registrado en la
|
|
82
|
+
fuente que vale la pena investigar, pero no amerita bloquear el build
|
|
83
|
+
por sí solo.
|
|
84
|
+
- Duplicados de clave primaria, ancho de código incorrecto, y filas
|
|
85
|
+
huérfanas por integridad referencial son siempre **error**.
|
|
86
|
+
|
|
87
|
+
## Tests de un validador nuevo
|
|
88
|
+
|
|
89
|
+
Como mínimo (ver `tests/validation/test_territorio.py`):
|
|
90
|
+
- Caso feliz: DataFrame válido → `status == "ok"`.
|
|
91
|
+
- Un caso por cada regla de error (duplicado, ancho de código, huérfano).
|
|
92
|
+
- DataFrame vacío / columnas faltantes → error claro, no un traceback.
|
|
93
|
+
- `source_mode == "fallback"` → warning presente.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Usar al agregar una fuente de datos nueva a colombia-open (extractor + builder + validación + registro + tests + docs). Recibe el nombre de la fuente y su URL/endpoint.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Agregar una fuente nueva a colombia-open
|
|
6
|
+
|
|
7
|
+
Antes de escribir código: **verifica el endpoint/URL actual de la fuente**
|
|
8
|
+
(curl/fetch real, no solo lo que dice `docs/roadmap/roadmap.md` — esa tabla
|
|
9
|
+
puede estar desactualizada). Si el acceso real no coincide con lo que
|
|
10
|
+
`docs/roadmap/roadmap.md` describe, corrígelo ahí antes de seguir.
|
|
11
|
+
|
|
12
|
+
Usa `src/extractors/divipola_extractor.py` + `src/validation/_territorio.py`
|
|
13
|
+
+ `data/registry/datasets.json` (entradas `departamentos`/`municipios`)
|
|
14
|
+
como plantilla de referencia en cada paso — son el ejemplo end-to-end ya
|
|
15
|
+
funcionando de este mismo contrato.
|
|
16
|
+
|
|
17
|
+
**Regla de alcance**: esta skill implementa UNA fuente, de punta a punta.
|
|
18
|
+
No empieces una segunda mientras esta no pase `make check` completo.
|
|
19
|
+
|
|
20
|
+
## Checklist
|
|
21
|
+
|
|
22
|
+
### 1. Extractor — `src/extractors/{name}_extractor.py`
|
|
23
|
+
|
|
24
|
+
- Importaciones absolutas normales desde `src.extractors.base`/
|
|
25
|
+
`src.extractors.http_utils` — sin manipular `sys.path` (ver
|
|
26
|
+
`.claude/rules/extractors.md` §6). El extractor se invoca siempre como
|
|
27
|
+
módulo: `python -m src.extractors.{name}_extractor`.
|
|
28
|
+
- Clase `{Name}Extractor(BaseExtractor)` con `dataset_name`, `fetch()`,
|
|
29
|
+
`write_staging()`.
|
|
30
|
+
- `fetch()`: try en vivo → except → fallback embebido mínimo (5-15 filas).
|
|
31
|
+
Siempre devuelve `ExtractionResult` con `source_mode` correcto. El
|
|
32
|
+
`except` captura una tupla explícita de fallas esperadas de la fuente
|
|
33
|
+
(`RequestException`, `KeyError`, `ValueError`, `TypeError` — ajusta
|
|
34
|
+
según lo que de verdad pueda fallar en esta fuente), **nunca**
|
|
35
|
+
`except Exception` desnudo (ver `.claude/rules/extractors.md` §1).
|
|
36
|
+
- Define `REUSE_POLICY: ReusePolicy` con la licencia real de la fuente
|
|
37
|
+
(revisa el portal — no asumas CC BY por defecto). Si es ambigua,
|
|
38
|
+
**pregunta antes de asumir** y marca `status="restricted"` mientras
|
|
39
|
+
tanto (ver `docs/licenses/data_licenses.md`).
|
|
40
|
+
- Normaliza cualquier código administrativo a string de ancho fijo
|
|
41
|
+
(`.zfill(n)`) — nunca lo dejes como int.
|
|
42
|
+
- Si la fuente requiere scraping real (no hay API/descarga directa):
|
|
43
|
+
el extractor va bajo el extra `scraping`, no `pipeline` — importa
|
|
44
|
+
`curl_cffi`/`beautifulsoup4` solo ahí, no en `pyproject.toml[pipeline]`.
|
|
45
|
+
- `if __name__ == "__main__":` al final, igual que el ejemplo — así
|
|
46
|
+
`make extract` (que invoca `python -m src.extractors.{name}_extractor`)
|
|
47
|
+
puede correrlo.
|
|
48
|
+
|
|
49
|
+
### 2. Builder — entrada en `src/builders/datasets.py` (o módulo nuevo si el dominio no existe)
|
|
50
|
+
|
|
51
|
+
- Si la fuente produce un dataset normalizado 1:1 desde staging, no
|
|
52
|
+
necesitas una función `derive_*` — el builder solo lee staging y
|
|
53
|
+
escribe parquet/json directo.
|
|
54
|
+
- Si deriva más de un dataset lógico de un mismo staging (como
|
|
55
|
+
`divipola` → `departamentos` + `municipios`), escribe una función
|
|
56
|
+
`derive_{dataset}(staging_df) -> pl.DataFrame` por cada uno.
|
|
57
|
+
- Agrega una columna `{campo}_clean` (vía `builders/datasets.py::clean_name`)
|
|
58
|
+
a cualquier columna de nombre que vaya a usarse para búsqueda/join por
|
|
59
|
+
texto.
|
|
60
|
+
|
|
61
|
+
### 3. Validador — `src/validation/_{dominio}.py`
|
|
62
|
+
|
|
63
|
+
- Función `validate_{dataset}(df, metadata=None, **kwargs) -> ValidationResult`
|
|
64
|
+
que sigue el contrato de `.claude/rules/validation.md`: nunca lanza,
|
|
65
|
+
siempre devuelve un `ValidationResult` (`src/validation/result.py`), no
|
|
66
|
+
un dict plano.
|
|
67
|
+
- Mínimo: columnas requeridas presentes, clave primaria única, ancho fijo
|
|
68
|
+
de cualquier código administrativo, conteo mínimo si `source_mode="live"`.
|
|
69
|
+
- Si el dataset referencia códigos de otro dataset ya existente (ej. todo
|
|
70
|
+
dataset territorial futuro probablemente referencia `cod_dpto` o
|
|
71
|
+
`cod_mpio`), valida integridad referencial con
|
|
72
|
+
`_helpers.unknown_codes_count()`.
|
|
73
|
+
- **Registra la función**: impórtala y agrégala al dict `VALIDATORS` en
|
|
74
|
+
`src/validation/__init__.py`. Sin esto, `make doctor` falla.
|
|
75
|
+
|
|
76
|
+
### 4. Registro — entrada en `data/registry/datasets.json`
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
"{dataset}": {
|
|
80
|
+
"description": "...",
|
|
81
|
+
"extractor": "src/extractors/{name}_extractor.py",
|
|
82
|
+
"source_name": "...",
|
|
83
|
+
"join_keys": ["..."],
|
|
84
|
+
"expected_record_count": 0,
|
|
85
|
+
"outputs": {
|
|
86
|
+
"parquet": "data/normalized/{dataset}.parquet",
|
|
87
|
+
"json": "data/normalized/{dataset}.json"
|
|
88
|
+
},
|
|
89
|
+
"usage_examples": {
|
|
90
|
+
"python": "from colombia_open import ColombiaOpen\n\nhub = ColombiaOpen()\ndf = hub.load_polars('{dataset}')",
|
|
91
|
+
"cli": "colombia-open path {dataset} --output parquet"
|
|
92
|
+
},
|
|
93
|
+
"documentation": "docs/datasets/{dataset}.md"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 5. Orquestador — `src/build_pipeline.py`
|
|
98
|
+
|
|
99
|
+
Agrega el bloque de extracción + derive + validate + write + catalog para
|
|
100
|
+
la fuente nueva, siguiendo el patrón ya escrito para `divipola`. Si el
|
|
101
|
+
dominio es nuevo, importa el validador nuevo arriba junto a los demás.
|
|
102
|
+
|
|
103
|
+
### 6. Tests — `tests/{extractors,builders,validation}/test_{name}.py`
|
|
104
|
+
|
|
105
|
+
Como mínimo (ver los tests de `divipola` como plantilla):
|
|
106
|
+
- Extractor: normalización del fallback produce códigos de ancho correcto;
|
|
107
|
+
`run(dry_run=True)` no escribe nada a disco.
|
|
108
|
+
- Builder (si aplica `derive_*`): dedup correcto, columna `_clean` presente.
|
|
109
|
+
- Validador: caso feliz + un caso por regla de error + dataframe vacío +
|
|
110
|
+
warning en `source_mode="fallback"`.
|
|
111
|
+
- Si agregaste algo a `colombia_open/` (ej. un método nuevo en `core.py`
|
|
112
|
+
específico del dominio): test en `tests/test_core_api.py`.
|
|
113
|
+
|
|
114
|
+
### 7. Documentación y registro final
|
|
115
|
+
|
|
116
|
+
1. `docs/datasets/{dataset}.md` — descripción, fuente, licencia, esquema
|
|
117
|
+
completo (tabla columna/tipo/descripción), ejemplos de uso, limitaciones
|
|
118
|
+
conocidas. Usa `docs/datasets/municipios.md` como plantilla.
|
|
119
|
+
2. Agrega el/los dataset(s) nuevo(s) al enum
|
|
120
|
+
`src/colombia_open/datasets.py::Dataset`.
|
|
121
|
+
3. Marca la fila correspondiente en `docs/roadmap/roadmap.md` como implementada, con
|
|
122
|
+
link al extractor (mismo formato que la fila de geografía).
|
|
123
|
+
4. Corre `make check` completo — build, verify, test, lint, format-check,
|
|
124
|
+
typecheck, sec. Todo verde antes de considerar la fuente terminada.
|
|
125
|
+
|
|
126
|
+
## No hagas esto
|
|
127
|
+
|
|
128
|
+
- No implementes una segunda fuente en la misma sesión "ya que estás
|
|
129
|
+
aquí" — una fuente por fin de semana (ver `CLAUDE.md`).
|
|
130
|
+
- No le pongas scraping HTML a una fuente que tiene API JSON o descarga
|
|
131
|
+
directa — verifica eso primero.
|
|
132
|
+
- No marques una fuente como lista sin correr `make check` completo.
|
|
133
|
+
- No asumas la licencia de una fuente — verifícala en el portal real y,
|
|
134
|
+
si es ambigua, pregunta antes de publicar (ver `docs/licenses/data_licenses.md`).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: pages-deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "site/**"
|
|
8
|
+
- ".github/workflows/pages-deploy.yml"
|
|
9
|
+
workflow_dispatch: {}
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pages: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: pages
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
deploy:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment:
|
|
24
|
+
name: github-pages
|
|
25
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: actions/configure-pages@v5
|
|
29
|
+
- uses: actions/upload-pages-artifact@v3
|
|
30
|
+
with:
|
|
31
|
+
path: "site"
|
|
32
|
+
- id: deployment
|
|
33
|
+
uses: actions/deploy-pages@v4
|