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.
Files changed (83) hide show
  1. colombia_open-0.3.0/.claude/agents/data-quality-reviewer.md +86 -0
  2. colombia_open-0.3.0/.claude/commands/next-source.md +25 -0
  3. colombia_open-0.3.0/.claude/rules/extractors.md +128 -0
  4. colombia_open-0.3.0/.claude/rules/releases.md +101 -0
  5. colombia_open-0.3.0/.claude/rules/validation.md +93 -0
  6. colombia_open-0.3.0/.claude/skills/new-source/SKILL.md +134 -0
  7. colombia_open-0.3.0/.github/FUNDING.yml +2 -0
  8. colombia_open-0.3.0/.github/workflows/pages-deploy.yml +33 -0
  9. colombia_open-0.3.0/.github/workflows/pipeline-check.yml +97 -0
  10. colombia_open-0.3.0/.github/workflows/publish.yml +58 -0
  11. colombia_open-0.3.0/.github/workflows/release.yml +43 -0
  12. colombia_open-0.3.0/.github/workflows/source-urls.yml +20 -0
  13. colombia_open-0.3.0/.gitignore +36 -0
  14. colombia_open-0.3.0/.pre-commit-config.yaml +26 -0
  15. colombia_open-0.3.0/.python-version +1 -0
  16. colombia_open-0.3.0/CLAUDE.md +154 -0
  17. colombia_open-0.3.0/LICENSE +26 -0
  18. colombia_open-0.3.0/Makefile +50 -0
  19. colombia_open-0.3.0/PKG-INFO +242 -0
  20. colombia_open-0.3.0/README.md +181 -0
  21. colombia_open-0.3.0/data/registry/datasets.json +69 -0
  22. colombia_open-0.3.0/docs/brand/design-system.md +88 -0
  23. colombia_open-0.3.0/docs/datasets/demografia.md +68 -0
  24. colombia_open-0.3.0/docs/datasets/departamentos.md +41 -0
  25. colombia_open-0.3.0/docs/datasets/educacion_establecimientos.md +105 -0
  26. colombia_open-0.3.0/docs/datasets/municipios.md +46 -0
  27. colombia_open-0.3.0/docs/licenses/data_licenses.md +86 -0
  28. colombia_open-0.3.0/docs/roadmap/roadmap.md +103 -0
  29. colombia_open-0.3.0/examples/notebooks/01_departamentos.ipynb +126 -0
  30. colombia_open-0.3.0/examples/notebooks/02_municipios.ipynb +155 -0
  31. colombia_open-0.3.0/examples/notebooks/03_demografia.ipynb +166 -0
  32. colombia_open-0.3.0/examples/notebooks/04_educacion_establecimientos.ipynb +170 -0
  33. colombia_open-0.3.0/examples/notebooks/README.md +15 -0
  34. colombia_open-0.3.0/pyproject.toml +117 -0
  35. colombia_open-0.3.0/scripts/check_source_urls.py +42 -0
  36. colombia_open-0.3.0/scripts/check_validation_registration.py +33 -0
  37. colombia_open-0.3.0/scripts/make_bundle.py +39 -0
  38. colombia_open-0.3.0/scripts/verify_pipeline.py +56 -0
  39. colombia_open-0.3.0/site/assets/brand/mark-gold.svg +14 -0
  40. colombia_open-0.3.0/site/assets/brand/mark-indigo.svg +14 -0
  41. colombia_open-0.3.0/site/favicon.svg +14 -0
  42. colombia_open-0.3.0/site/index.html +243 -0
  43. colombia_open-0.3.0/site/styles.css +228 -0
  44. colombia_open-0.3.0/src/__init__.py +0 -0
  45. colombia_open-0.3.0/src/build_pipeline.py +131 -0
  46. colombia_open-0.3.0/src/builders/__init__.py +0 -0
  47. colombia_open-0.3.0/src/builders/_shared.py +15 -0
  48. colombia_open-0.3.0/src/builders/catalog.py +52 -0
  49. colombia_open-0.3.0/src/builders/datasets.py +57 -0
  50. colombia_open-0.3.0/src/builders/formats.py +39 -0
  51. colombia_open-0.3.0/src/colombia_open/__init__.py +27 -0
  52. colombia_open-0.3.0/src/colombia_open/__main__.py +4 -0
  53. colombia_open-0.3.0/src/colombia_open/cli.py +138 -0
  54. colombia_open-0.3.0/src/colombia_open/core.py +251 -0
  55. colombia_open-0.3.0/src/colombia_open/data_manager.py +129 -0
  56. colombia_open-0.3.0/src/colombia_open/datasets.py +29 -0
  57. colombia_open-0.3.0/src/colombia_open/exceptions.py +23 -0
  58. colombia_open-0.3.0/src/colombia_open/py.typed +0 -0
  59. colombia_open-0.3.0/src/colombia_open/text.py +18 -0
  60. colombia_open-0.3.0/src/extractors/__init__.py +0 -0
  61. colombia_open-0.3.0/src/extractors/base.py +185 -0
  62. colombia_open-0.3.0/src/extractors/demografia_extractor.py +241 -0
  63. colombia_open-0.3.0/src/extractors/divipola_extractor.py +214 -0
  64. colombia_open-0.3.0/src/extractors/educacion_establecimientos_extractor.py +461 -0
  65. colombia_open-0.3.0/src/extractors/http_utils.py +44 -0
  66. colombia_open-0.3.0/src/registry/__init__.py +4 -0
  67. colombia_open-0.3.0/src/registry/dataset_spec.py +60 -0
  68. colombia_open-0.3.0/src/validation/__init__.py +27 -0
  69. colombia_open-0.3.0/src/validation/_constants.py +30 -0
  70. colombia_open-0.3.0/src/validation/_demografia.py +114 -0
  71. colombia_open-0.3.0/src/validation/_educacion.py +149 -0
  72. colombia_open-0.3.0/src/validation/_helpers.py +54 -0
  73. colombia_open-0.3.0/src/validation/_territorio.py +136 -0
  74. colombia_open-0.3.0/src/validation/result.py +27 -0
  75. colombia_open-0.3.0/tests/builders/test_datasets.py +36 -0
  76. colombia_open-0.3.0/tests/extractors/test_demografia_extractor.py +77 -0
  77. colombia_open-0.3.0/tests/extractors/test_divipola_extractor.py +66 -0
  78. colombia_open-0.3.0/tests/extractors/test_educacion_establecimientos_extractor.py +87 -0
  79. colombia_open-0.3.0/tests/test_core_api.py +219 -0
  80. colombia_open-0.3.0/tests/test_data_manager.py +45 -0
  81. colombia_open-0.3.0/tests/validation/test_demografia.py +76 -0
  82. colombia_open-0.3.0/tests/validation/test_educacion.py +118 -0
  83. 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,2 @@
1
+ github: [davidcasr]
2
+ buy_me_a_coffee: davidcasr
@@ -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