uydatos 0.2.1__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.
- uydatos-0.2.1/.gitattributes +2 -0
- uydatos-0.2.1/.github/workflows/publicar.yml +27 -0
- uydatos-0.2.1/.github/workflows/tests.yml +42 -0
- uydatos-0.2.1/.gitignore +10 -0
- uydatos-0.2.1/CHANGELOG.md +26 -0
- uydatos-0.2.1/LICENSE +21 -0
- uydatos-0.2.1/PKG-INFO +150 -0
- uydatos-0.2.1/README.md +107 -0
- uydatos-0.2.1/pyproject.toml +47 -0
- uydatos-0.2.1/src/uydatos/__init__.py +24 -0
- uydatos-0.2.1/src/uydatos/ancap.py +200 -0
- uydatos-0.2.1/src/uydatos/bcu.py +233 -0
- uydatos-0.2.1/src/uydatos/catalogo.py +81 -0
- uydatos-0.2.1/src/uydatos/ibge.py +97 -0
- uydatos-0.2.1/src/uydatos/ideuy.py +306 -0
- uydatos-0.2.1/src/uydatos/indec.py +155 -0
- uydatos-0.2.1/src/uydatos/inia.py +276 -0
- uydatos-0.2.1/src/uydatos/py.typed +0 -0
- uydatos-0.2.1/src/uydatos/rut.py +114 -0
- uydatos-0.2.1/src/uydatos/salto_grande.py +189 -0
- uydatos-0.2.1/src/uydatos/semanas.py +90 -0
- uydatos-0.2.1/tests/fixtures/ibge_ipca_recorte.json +80 -0
- uydatos-0.2.1/tests/fixtures/indec_ipc_nacional_2016-12_a_2017-12.json +98 -0
- uydatos-0.2.1/tests/fixtures/salto_grande_estaciones_recorte.xml +2 -0
- uydatos-0.2.1/tests/test_ancap.py +155 -0
- uydatos-0.2.1/tests/test_bcu.py +150 -0
- uydatos-0.2.1/tests/test_catalogo.py +44 -0
- uydatos-0.2.1/tests/test_ibge.py +72 -0
- uydatos-0.2.1/tests/test_ideuy.py +242 -0
- uydatos-0.2.1/tests/test_indec.py +127 -0
- uydatos-0.2.1/tests/test_inia.py +229 -0
- uydatos-0.2.1/tests/test_red.py +63 -0
- uydatos-0.2.1/tests/test_rut.py +78 -0
- uydatos-0.2.1/tests/test_salto_grande.py +169 -0
- uydatos-0.2.1/tests/test_semanas.py +79 -0
- uydatos-0.2.1/uv.lock +173 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: publicar
|
|
2
|
+
|
|
3
|
+
# Se dispara al publicar una Release en GitHub. PyPI confía en este workflow
|
|
4
|
+
# (trusted publishing): no hay tokens ni contraseñas guardadas en el repo.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
pypi:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
18
|
+
- name: La etiqueta coincide con la versión del paquete
|
|
19
|
+
run: |
|
|
20
|
+
version=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
|
|
21
|
+
test "v$version" = "${GITHUB_REF_NAME}" || { echo "etiqueta ${GITHUB_REF_NAME} != versión $version"; exit 1; }
|
|
22
|
+
- name: Tests
|
|
23
|
+
run: uv run --python 3.13 --with pytest --with-editable . pytest -q
|
|
24
|
+
- name: Construir
|
|
25
|
+
run: uv build && uvx twine check dist/*
|
|
26
|
+
- name: Publicar en PyPI
|
|
27
|
+
run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
# Una vez por semana contra las fuentes reales: avisa si alguna cambió de forma.
|
|
9
|
+
- cron: "0 9 * * 1"
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
tests:
|
|
17
|
+
if: github.event_name != 'schedule'
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
os: [ubuntu-latest]
|
|
23
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
24
|
+
include:
|
|
25
|
+
- os: windows-latest
|
|
26
|
+
python: "3.13"
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
30
|
+
- name: Lint
|
|
31
|
+
run: uvx ruff check src tests && uvx ruff format --check src tests
|
|
32
|
+
- name: Tests sin red
|
|
33
|
+
run: uv run --python ${{ matrix.python }} --with pytest --with-editable . pytest -q
|
|
34
|
+
|
|
35
|
+
fuentes-reales:
|
|
36
|
+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
41
|
+
- name: Tests contra las fuentes reales
|
|
42
|
+
run: uv run --python 3.13 --with pytest --with-editable . pytest -q -m red
|
uydatos-0.2.1/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Cambios
|
|
2
|
+
|
|
3
|
+
## 0.2.1 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
- Publicado en PyPI: `pip install uydatos`. Se publica solo al crear una Release en GitHub, con
|
|
6
|
+
trusted publishing (sin tokens).
|
|
7
|
+
- Marcado como paquete tipado (`py.typed`), con clasificadores y palabras clave.
|
|
8
|
+
- Insignias y un resumen en inglés en el README.
|
|
9
|
+
|
|
10
|
+
## 0.2.0 — 2026-10-09
|
|
11
|
+
|
|
12
|
+
- **Nuevo: `uydatos.ideuy`**, geocodificación de direcciones uruguayas con la API pública de IDE.uy.
|
|
13
|
+
Cada resultado lleva su calidad (`puerta_exacta`, `puerta_aproximada`, `esquina`,
|
|
14
|
+
`calle_sin_numero`) y descarta lo que la API devuelve sin haberlo encontrado:
|
|
15
|
+
- el centro de la localidad;
|
|
16
|
+
- **otra calle**: una calle inexistente devolvió "CALLE N 2570" con el mismo aviso que una
|
|
17
|
+
puerta ajustada;
|
|
18
|
+
- las respuestas de texto libre que no son una esquina.
|
|
19
|
+
- Las esquinas se piden a v1 como `ESQ` (con `Y` no las reconoce) y en los dos órdenes de
|
|
20
|
+
calles, porque v1 depende del orden. Si v1 no responde, se conserva el resultado de v0.
|
|
21
|
+
- CI: tests en Python 3.10 a 3.13 y Windows, y una corrida semanal contra las fuentes reales.
|
|
22
|
+
|
|
23
|
+
## 0.1.0 — 2026-10-09
|
|
24
|
+
|
|
25
|
+
Primera versión: BCU, ANCAP, INIA, Salto Grande, INDEC, IBGE, RUT y cédula, semanas ISO y el
|
|
26
|
+
catálogo de datos abiertos de Uruguay.
|
uydatos-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manu Martinez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
uydatos-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: uydatos
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Lectores estrictos de datos públicos de Uruguay y la región: BCU, ANCAP, INIA, Salto Grande, INDEC, IBGE, IDE.uy, RUT y cédula.
|
|
5
|
+
Project-URL: Repository, https://github.com/manusabbath-arch/uy-datos-abiertos
|
|
6
|
+
Project-URL: Changelog, https://github.com/manusabbath-arch/uy-datos-abiertos/blob/main/CHANGELOG.md
|
|
7
|
+
Author: Manu Martinez
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Manu Martinez
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: bcu,datos-abiertos,ide-uy,open-data,rut,uruguay
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Natural Language :: Spanish
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
38
|
+
Classifier: Typing :: Typed
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# uydatos
|
|
45
|
+
|
|
46
|
+
[](https://github.com/manusabbath-arch/uy-datos-abiertos/actions/workflows/tests.yml)
|
|
47
|
+
[](https://pypi.org/project/uydatos/)
|
|
48
|
+
[](https://pypi.org/project/uydatos/)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
|
|
51
|
+
> **In English:** strict, dependency-free Python readers for Uruguayan and regional public data
|
|
52
|
+
> (Central Bank of Uruguay exchange rates, ANCAP fuel prices, INIA weather, Salto Grande river
|
|
53
|
+
> stations, Argentina's CPI, Brazil's IPCA, IDE.uy geocoding, RUT/ID check digits). Each reader
|
|
54
|
+
> documents its source's traps and **raises instead of silently fixing** bad data.
|
|
55
|
+
|
|
56
|
+
Lectores **estrictos** de datos públicos de Uruguay y la región, en Python puro (sin
|
|
57
|
+
dependencias).
|
|
58
|
+
|
|
59
|
+
La idea es una sola: **rechazar en vez de arreglar**. Cada fuente de acá tiene al menos una
|
|
60
|
+
trampa que no da error y produce una serie creíble pero falsa: una respuesta de error guardada
|
|
61
|
+
como si fuera un mes sin datos, columnas con los nombres invertidos, un archivo que apila
|
|
62
|
+
publicaciones en vez de ser una serie, una API que sigue sirviendo índices discontinuados. Cada
|
|
63
|
+
lector conoce las trampas de su fuente, las documenta y levanta una excepción con el motivo.
|
|
64
|
+
|
|
65
|
+
| Módulo | Fuente | Qué trae | La trampa principal |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `uydatos.bcu` | Banco Central del Uruguay (web service SOAP) | Dólar, Unidad Indexada, peso argentino, real | Una respuesta de error se guarda igual y parece un mes sin cotización. |
|
|
68
|
+
| `uydatos.ancap` | ANCAP, catálogo de datos abiertos | Precio máximo al público de combustibles | Las columnas `Unidad` y `Valor` vienen invertidas, y el archivo apila publicaciones. |
|
|
69
|
+
| `uydatos.inia` | INIA, estación Salto Grande | Lluvia y temperaturas diarias desde 1970, normales y anomalías | El día va de 9:00 a 9:00; un vacío es nulo, no cero. |
|
|
70
|
+
| `uydatos.salto_grande` | Comisión Técnica Mixta de Salto Grande (SOAP) | Estaciones hidrometeorológicas de la cuenca del río Uruguay | El arreglo SOAP encoded se llama `item`: buscar `Estacion` da lista vacía sin error. |
|
|
71
|
+
| `uydatos.indec` | API de series de datos.gob.ar | IPC nacional de Argentina, base dic-2016 | La API sigue sirviendo las bases 2003 y 2008, discontinuadas. |
|
|
72
|
+
| `uydatos.ibge` | API SIDRA del IBGE | IPCA de Brasil, número-índice | La tabla trae varias variables; una variación leída como índice no da error. |
|
|
73
|
+
| `uydatos.ideuy` | IDE.uy (direcciones.ide.uy) | Coordenadas de una dirección o esquina, con su calidad | Ante lo que no encuentra devuelve el centro de la localidad, una puerta vecina u otra calle, sin error. |
|
|
74
|
+
| `uydatos.rut` | Offline | Dígito verificador de RUT y cédula | Excel convierte el RUT en float y se come el cero inicial. |
|
|
75
|
+
| `uydatos.semanas` | Offline | Semanas ISO 8601 (`W41`) | El 01/01/2027 es la `2026-W53`. |
|
|
76
|
+
| `uydatos.catalogo` | catalogodatos.gub.uy (CKAN) | URLs vigentes de los recursos de un dataset | Las URLs de descarga cambian cuando el organismo resube un archivo. |
|
|
77
|
+
|
|
78
|
+
## Instalación
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
pip install uydatos
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requiere Python 3.10 o posterior.
|
|
85
|
+
|
|
86
|
+
## Uso
|
|
87
|
+
|
|
88
|
+
Cada módulo separa **bajar** (la única función que sale a internet) de **leer** (que trabaja
|
|
89
|
+
sobre el archivo en disco). Así lo bajado queda guardado tal cual vino, y lo que se lee se puede
|
|
90
|
+
volver a verificar.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from pathlib import Path
|
|
94
|
+
from uydatos import bcu
|
|
95
|
+
|
|
96
|
+
bcu.descargar(9800, "2025-01", "2026-09", Path("datos/bcu")) # un XML por mes
|
|
97
|
+
ui = bcu.leer_serie(Path("datos/bcu"), 9800) # {date: valor}
|
|
98
|
+
|
|
99
|
+
ui_mensual = {f["mes"]: f["promedio"] for f in bcu.promedio_mensual(ui)}
|
|
100
|
+
bcu.a_pesos_de({"2025-01": 1000.0}, base="2026-09", ui=ui_mensual) # pesos constantes
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from uydatos.rut import validar_rut, validar_ci
|
|
105
|
+
|
|
106
|
+
validar_rut(12345678919.0) # ('dv_ok_estructura_rara', '012345678919'): Excel se había comido el 0
|
|
107
|
+
validar_ci("1.234.567-2") # ('valido', '12345672')
|
|
108
|
+
validar_ci("1.234.567-3") # ('dv_no_valida', '12345673')
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Desde la línea de comandos:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
python -m uydatos.bcu bajar datos/bcu 2225 2026-01 2026-09
|
|
115
|
+
python -m uydatos.bcu serie datos/bcu 2225
|
|
116
|
+
python -m uydatos.ancap bajar datos/ancap.csv
|
|
117
|
+
python -m uydatos.ancap serie datos/ancap.csv "GASOIL 50-S *"
|
|
118
|
+
python -m uydatos.inia bajar datos/inia && python -m uydatos.inia resumen datos/inia
|
|
119
|
+
python -m uydatos.salto_grande bajar datos/estaciones.xml
|
|
120
|
+
python -m uydatos.salto_grande cerca datos/estaciones.xml -31.383 -57.960
|
|
121
|
+
python -m uydatos.indec bajar datos/ipc_ar.json
|
|
122
|
+
python -m uydatos.ibge bajar datos/ipca.json
|
|
123
|
+
python -m uydatos.catalogo inia-precipitacion-temps-extremas-sg
|
|
124
|
+
python -m uydatos.ideuy "Avenida 18 de Julio 1360" --localidad Montevideo --departamento Montevideo
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Lo que estos lectores se niegan a hacer
|
|
128
|
+
|
|
129
|
+
- **Rellenar huecos.** Un fin de semana sin cotización devuelve `None`, salvo que se pida
|
|
130
|
+
explícitamente el último día hábil (`hacia_atras=True`), y aun así no salta más de una semana.
|
|
131
|
+
- **Convertir un índice en inflación.** Eso es una decisión de análisis, no una lectura.
|
|
132
|
+
- **Inventar unidades** que la fuente no declara.
|
|
133
|
+
- **Elegir por vos** qué cotización corresponde a un fin fiscal.
|
|
134
|
+
|
|
135
|
+
## Tests
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
pip install -e ".[test]"
|
|
139
|
+
pytest # sin red: todo con archivos de prueba
|
|
140
|
+
pytest -m red # contra las fuentes reales
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Datos
|
|
144
|
+
|
|
145
|
+
Este repositorio publica **código**, no datos. Cada fuente tiene sus propias condiciones de uso;
|
|
146
|
+
los datos de los catálogos de Uruguay se publican bajo la Licencia de Datos Abiertos de Uruguay.
|
|
147
|
+
|
|
148
|
+
## Licencia
|
|
149
|
+
|
|
150
|
+
MIT. Usalo, adaptalo y compartilo; solo mantené el aviso de autoría.
|
uydatos-0.2.1/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# uydatos
|
|
2
|
+
|
|
3
|
+
[](https://github.com/manusabbath-arch/uy-datos-abiertos/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/uydatos/)
|
|
5
|
+
[](https://pypi.org/project/uydatos/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
> **In English:** strict, dependency-free Python readers for Uruguayan and regional public data
|
|
9
|
+
> (Central Bank of Uruguay exchange rates, ANCAP fuel prices, INIA weather, Salto Grande river
|
|
10
|
+
> stations, Argentina's CPI, Brazil's IPCA, IDE.uy geocoding, RUT/ID check digits). Each reader
|
|
11
|
+
> documents its source's traps and **raises instead of silently fixing** bad data.
|
|
12
|
+
|
|
13
|
+
Lectores **estrictos** de datos públicos de Uruguay y la región, en Python puro (sin
|
|
14
|
+
dependencias).
|
|
15
|
+
|
|
16
|
+
La idea es una sola: **rechazar en vez de arreglar**. Cada fuente de acá tiene al menos una
|
|
17
|
+
trampa que no da error y produce una serie creíble pero falsa: una respuesta de error guardada
|
|
18
|
+
como si fuera un mes sin datos, columnas con los nombres invertidos, un archivo que apila
|
|
19
|
+
publicaciones en vez de ser una serie, una API que sigue sirviendo índices discontinuados. Cada
|
|
20
|
+
lector conoce las trampas de su fuente, las documenta y levanta una excepción con el motivo.
|
|
21
|
+
|
|
22
|
+
| Módulo | Fuente | Qué trae | La trampa principal |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `uydatos.bcu` | Banco Central del Uruguay (web service SOAP) | Dólar, Unidad Indexada, peso argentino, real | Una respuesta de error se guarda igual y parece un mes sin cotización. |
|
|
25
|
+
| `uydatos.ancap` | ANCAP, catálogo de datos abiertos | Precio máximo al público de combustibles | Las columnas `Unidad` y `Valor` vienen invertidas, y el archivo apila publicaciones. |
|
|
26
|
+
| `uydatos.inia` | INIA, estación Salto Grande | Lluvia y temperaturas diarias desde 1970, normales y anomalías | El día va de 9:00 a 9:00; un vacío es nulo, no cero. |
|
|
27
|
+
| `uydatos.salto_grande` | Comisión Técnica Mixta de Salto Grande (SOAP) | Estaciones hidrometeorológicas de la cuenca del río Uruguay | El arreglo SOAP encoded se llama `item`: buscar `Estacion` da lista vacía sin error. |
|
|
28
|
+
| `uydatos.indec` | API de series de datos.gob.ar | IPC nacional de Argentina, base dic-2016 | La API sigue sirviendo las bases 2003 y 2008, discontinuadas. |
|
|
29
|
+
| `uydatos.ibge` | API SIDRA del IBGE | IPCA de Brasil, número-índice | La tabla trae varias variables; una variación leída como índice no da error. |
|
|
30
|
+
| `uydatos.ideuy` | IDE.uy (direcciones.ide.uy) | Coordenadas de una dirección o esquina, con su calidad | Ante lo que no encuentra devuelve el centro de la localidad, una puerta vecina u otra calle, sin error. |
|
|
31
|
+
| `uydatos.rut` | Offline | Dígito verificador de RUT y cédula | Excel convierte el RUT en float y se come el cero inicial. |
|
|
32
|
+
| `uydatos.semanas` | Offline | Semanas ISO 8601 (`W41`) | El 01/01/2027 es la `2026-W53`. |
|
|
33
|
+
| `uydatos.catalogo` | catalogodatos.gub.uy (CKAN) | URLs vigentes de los recursos de un dataset | Las URLs de descarga cambian cuando el organismo resube un archivo. |
|
|
34
|
+
|
|
35
|
+
## Instalación
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pip install uydatos
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requiere Python 3.10 o posterior.
|
|
42
|
+
|
|
43
|
+
## Uso
|
|
44
|
+
|
|
45
|
+
Cada módulo separa **bajar** (la única función que sale a internet) de **leer** (que trabaja
|
|
46
|
+
sobre el archivo en disco). Así lo bajado queda guardado tal cual vino, y lo que se lee se puede
|
|
47
|
+
volver a verificar.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from uydatos import bcu
|
|
52
|
+
|
|
53
|
+
bcu.descargar(9800, "2025-01", "2026-09", Path("datos/bcu")) # un XML por mes
|
|
54
|
+
ui = bcu.leer_serie(Path("datos/bcu"), 9800) # {date: valor}
|
|
55
|
+
|
|
56
|
+
ui_mensual = {f["mes"]: f["promedio"] for f in bcu.promedio_mensual(ui)}
|
|
57
|
+
bcu.a_pesos_de({"2025-01": 1000.0}, base="2026-09", ui=ui_mensual) # pesos constantes
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from uydatos.rut import validar_rut, validar_ci
|
|
62
|
+
|
|
63
|
+
validar_rut(12345678919.0) # ('dv_ok_estructura_rara', '012345678919'): Excel se había comido el 0
|
|
64
|
+
validar_ci("1.234.567-2") # ('valido', '12345672')
|
|
65
|
+
validar_ci("1.234.567-3") # ('dv_no_valida', '12345673')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Desde la línea de comandos:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
python -m uydatos.bcu bajar datos/bcu 2225 2026-01 2026-09
|
|
72
|
+
python -m uydatos.bcu serie datos/bcu 2225
|
|
73
|
+
python -m uydatos.ancap bajar datos/ancap.csv
|
|
74
|
+
python -m uydatos.ancap serie datos/ancap.csv "GASOIL 50-S *"
|
|
75
|
+
python -m uydatos.inia bajar datos/inia && python -m uydatos.inia resumen datos/inia
|
|
76
|
+
python -m uydatos.salto_grande bajar datos/estaciones.xml
|
|
77
|
+
python -m uydatos.salto_grande cerca datos/estaciones.xml -31.383 -57.960
|
|
78
|
+
python -m uydatos.indec bajar datos/ipc_ar.json
|
|
79
|
+
python -m uydatos.ibge bajar datos/ipca.json
|
|
80
|
+
python -m uydatos.catalogo inia-precipitacion-temps-extremas-sg
|
|
81
|
+
python -m uydatos.ideuy "Avenida 18 de Julio 1360" --localidad Montevideo --departamento Montevideo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Lo que estos lectores se niegan a hacer
|
|
85
|
+
|
|
86
|
+
- **Rellenar huecos.** Un fin de semana sin cotización devuelve `None`, salvo que se pida
|
|
87
|
+
explícitamente el último día hábil (`hacia_atras=True`), y aun así no salta más de una semana.
|
|
88
|
+
- **Convertir un índice en inflación.** Eso es una decisión de análisis, no una lectura.
|
|
89
|
+
- **Inventar unidades** que la fuente no declara.
|
|
90
|
+
- **Elegir por vos** qué cotización corresponde a un fin fiscal.
|
|
91
|
+
|
|
92
|
+
## Tests
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
pip install -e ".[test]"
|
|
96
|
+
pytest # sin red: todo con archivos de prueba
|
|
97
|
+
pytest -m red # contra las fuentes reales
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Datos
|
|
101
|
+
|
|
102
|
+
Este repositorio publica **código**, no datos. Cada fuente tiene sus propias condiciones de uso;
|
|
103
|
+
los datos de los catálogos de Uruguay se publican bajo la Licencia de Datos Abiertos de Uruguay.
|
|
104
|
+
|
|
105
|
+
## Licencia
|
|
106
|
+
|
|
107
|
+
MIT. Usalo, adaptalo y compartilo; solo mantené el aviso de autoría.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uydatos"
|
|
3
|
+
version = "0.2.1"
|
|
4
|
+
description = "Lectores estrictos de datos públicos de Uruguay y la región: BCU, ANCAP, INIA, Salto Grande, INDEC, IBGE, IDE.uy, RUT y cédula."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
authors = [{ name = "Manu Martinez" }]
|
|
8
|
+
keywords = ["uruguay", "datos-abiertos", "open-data", "bcu", "ide-uy", "rut"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Intended Audience :: Science/Research",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Natural Language :: Spanish",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.10"
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
test = ["pytest>=8"]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/uydatos"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
addopts = "-m 'not red'"
|
|
35
|
+
markers = [
|
|
36
|
+
"red: sale a internet contra la fuente real; se corre a mano con `pytest -m red`",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 120
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "F", "I", "W"]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Repository = "https://github.com/manusabbath-arch/uy-datos-abiertos"
|
|
47
|
+
Changelog = "https://github.com/manusabbath-arch/uy-datos-abiertos/blob/main/CHANGELOG.md"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""uydatos — lectores estrictos de datos públicos de Uruguay y la región.
|
|
2
|
+
|
|
3
|
+
Cada módulo lee una fuente y **rechaza en vez de arreglar**: una respuesta mala, una columna
|
|
4
|
+
cambiada o un hueco en la serie levantan una excepción con el motivo, en lugar de devolver una
|
|
5
|
+
serie creíble pero falsa. La única función de cada módulo que sale a internet se llama
|
|
6
|
+
`descargar*()`; todo lo demás trabaja sobre archivos en disco.
|
|
7
|
+
|
|
8
|
+
Módulos:
|
|
9
|
+
|
|
10
|
+
- `bcu` — cotizaciones del Banco Central del Uruguay (dólar, UI, peso argentino, real).
|
|
11
|
+
- `ancap` — precios máximos de combustibles fijados por el Poder Ejecutivo.
|
|
12
|
+
- `inia` — clima diario de INIA Salto Grande, con normales y anomalías.
|
|
13
|
+
- `salto_grande` — estaciones hidrometeorológicas de la CTM de Salto Grande.
|
|
14
|
+
- `indec` — IPC nacional de Argentina (API de series de datos.gob.ar).
|
|
15
|
+
- `ibge` — IPCA de Brasil (API SIDRA del IBGE).
|
|
16
|
+
- `ideuy` — geocodificación de direcciones uruguayas con IDE.uy.
|
|
17
|
+
- `rut` — dígito verificador de RUT y cédula uruguaya.
|
|
18
|
+
- `semanas` — semanas ISO 8601 (W1, W2...) con los bordes de año resueltos.
|
|
19
|
+
- `catalogo` — recursos del catálogo de datos abiertos de Uruguay (CKAN).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "0.2.1"
|
|
23
|
+
|
|
24
|
+
USER_AGENT = f"uydatos/{__version__} (lectura de datos abiertos)"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Precios máximos de combustibles de ANCAP, aprobados por decreto del Poder Ejecutivo.
|
|
2
|
+
|
|
3
|
+
Dataset `ancap-precio-combustible-pe` del catálogo de datos abiertos: **precio máximo al
|
|
4
|
+
público, IVA incluido**. **No es** `ancap-precio-combustible-ex-planta`, que es el precio
|
|
5
|
+
deducidos flete, tasas y margen. Los dos se citan como "los precios de ANCAP" y no son el mismo
|
|
6
|
+
número.
|
|
7
|
+
|
|
8
|
+
Las trampas (medidas en octubre de 2026)
|
|
9
|
+
---------------------------------------
|
|
10
|
+
1. **Las columnas vienen invertidas.** El encabezado dice `Unidad` y `Valor`, pero `Unidad`
|
|
11
|
+
trae el **número** y `Valor` la **unidad de medida**. Quien sume "Valor" suma `'$/lt'`.
|
|
12
|
+
Si la fuente las vuelve a dar vuelta, el lector lo rechaza.
|
|
13
|
+
2. **El archivo no es una serie: es un log de publicaciones acumulativas.** Cada mes ANCAP
|
|
14
|
+
republica el año entero hasta ese mes y el archivo **apila** las publicaciones: un mismo mes
|
|
15
|
+
aparece decenas de veces. Contar o promediar filas mide publicaciones, no precios: usar
|
|
16
|
+
`serie_mensual()`.
|
|
17
|
+
3. **El BOM viene doble**: abrir con `utf-8-sig` no alcanza, queda un `\\ufeff` pegado al
|
|
18
|
+
nombre de la primera columna.
|
|
19
|
+
4. **`S/C`** es *sin cotización*: un nulo, no un cero.
|
|
20
|
+
5. **Coma decimal y punto de miles** (`1.234,50`): un `float()` pelado no sirve.
|
|
21
|
+
|
|
22
|
+
Uso:
|
|
23
|
+
python -m uydatos.ancap bajar precios.csv
|
|
24
|
+
python -m uydatos.ancap serie precios.csv "GASOIL 50-S *"
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import csv
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Optional, Sequence
|
|
34
|
+
|
|
35
|
+
DATASET = "ancap-precio-combustible-pe"
|
|
36
|
+
URL_CSV = (
|
|
37
|
+
"https://catalogodatos.gub.uy/dataset/556beb30-02fc-4d06-909a-f42083c89ef7/resource/"
|
|
38
|
+
"1a37fc4a-44fd-4842-bcb4-b5845dc8eced/download/datos-de-precios-de-combustibles.csv"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
COLUMNAS_ESPERADAS = ["Año", "Mes", "Producto", "Unidad", "Valor"]
|
|
42
|
+
SIN_COTIZACION = "S/C"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AncapInvalido(ValueError):
|
|
46
|
+
"""La serie no cumple el contrato de la fuente."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def numero_europeo(texto: str) -> Optional[float]:
|
|
50
|
+
"""`'1.234,50'` -> 1234.5. `None` si no se puede leer: nunca un cero inventado."""
|
|
51
|
+
t = texto.replace("\xa0", " ").strip().replace(".", "").replace(",", ".")
|
|
52
|
+
if not t:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
return float(t)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _limpiar_encabezado(campos: list[str]) -> list[str]:
|
|
61
|
+
return [c.replace("", "").strip() for c in campos]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def leer_publicaciones(ruta: Path) -> list[dict]:
|
|
65
|
+
"""Lee el archivo **crudo**: una fila por publicación, sin deduplicar.
|
|
66
|
+
|
|
67
|
+
Devuelve, por fila, `anio`, `mes`, `producto`, `precio` (float | None) y `unidad`.
|
|
68
|
+
Para el precio de un mes usar `serie_mensual()`, no esto.
|
|
69
|
+
"""
|
|
70
|
+
ruta = Path(ruta)
|
|
71
|
+
if not ruta.is_file():
|
|
72
|
+
raise AncapInvalido(f"{ruta}: el archivo no está en el disco")
|
|
73
|
+
with ruta.open(encoding="utf-8-sig", newline="") as f:
|
|
74
|
+
lector = csv.reader(f, delimiter=";")
|
|
75
|
+
try:
|
|
76
|
+
encabezado = _limpiar_encabezado(next(lector))
|
|
77
|
+
except StopIteration:
|
|
78
|
+
raise AncapInvalido(f"{ruta}: archivo vacío") from None
|
|
79
|
+
if encabezado != COLUMNAS_ESPERADAS:
|
|
80
|
+
raise AncapInvalido(f"{ruta}: columnas inesperadas {encabezado}, se esperaban {COLUMNAS_ESPERADAS}")
|
|
81
|
+
publicaciones = []
|
|
82
|
+
for n, campos in enumerate(lector, start=2):
|
|
83
|
+
if not any(c.strip() for c in campos):
|
|
84
|
+
continue
|
|
85
|
+
if len(campos) != len(COLUMNAS_ESPERADAS):
|
|
86
|
+
raise AncapInvalido(f"{ruta} fila {n}: {len(campos)} columnas")
|
|
87
|
+
anio_crudo, mes_crudo, producto, precio_crudo, unidad = (c.strip() for c in campos)
|
|
88
|
+
# `Valor` tiene que traer la UNIDAD. Si trae un número, la fuente invirtió las columnas.
|
|
89
|
+
if unidad and unidad != SIN_COTIZACION and not unidad.startswith("$"):
|
|
90
|
+
raise AncapInvalido(
|
|
91
|
+
f"{ruta} fila {n}: la columna `Valor` debería traer la unidad y trae {unidad!r}. "
|
|
92
|
+
f"¿La fuente invirtió las columnas?"
|
|
93
|
+
)
|
|
94
|
+
try:
|
|
95
|
+
anio, mes = int(anio_crudo), int(mes_crudo)
|
|
96
|
+
except ValueError:
|
|
97
|
+
raise AncapInvalido(f"{ruta} fila {n}: año/mes ilegible {campos[:2]!r}") from None
|
|
98
|
+
if precio_crudo == SIN_COTIZACION:
|
|
99
|
+
precio = None
|
|
100
|
+
else:
|
|
101
|
+
precio = numero_europeo(precio_crudo)
|
|
102
|
+
if precio is None:
|
|
103
|
+
raise AncapInvalido(f"{ruta} fila {n}: precio ilegible {precio_crudo!r}")
|
|
104
|
+
publicaciones.append(
|
|
105
|
+
{
|
|
106
|
+
"anio": anio,
|
|
107
|
+
"mes": mes,
|
|
108
|
+
"producto": producto,
|
|
109
|
+
"precio": precio,
|
|
110
|
+
"unidad": "" if unidad == SIN_COTIZACION else unidad,
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
if not publicaciones:
|
|
114
|
+
raise AncapInvalido(f"{ruta}: archivo sin filas de datos")
|
|
115
|
+
return publicaciones
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def serie_mensual(publicaciones: list[dict]) -> list[dict]:
|
|
119
|
+
"""Deduplica el log apilado: **una fila por (producto, año, mes)**.
|
|
120
|
+
|
|
121
|
+
Desempate declarado: **el valor manda sobre el `S/C`**. Un mes que alguna vez tuvo precio no
|
|
122
|
+
se pierde porque una publicación posterior diga `S/C`; entre dos precios distintos gana el
|
|
123
|
+
de la publicación más reciente (la que aparece después en el archivo).
|
|
124
|
+
"""
|
|
125
|
+
orden: list[tuple] = []
|
|
126
|
+
estado: dict[tuple, dict] = {}
|
|
127
|
+
for p in publicaciones:
|
|
128
|
+
clave = (p["producto"], p["anio"], p["mes"])
|
|
129
|
+
if clave not in estado:
|
|
130
|
+
orden.append(clave)
|
|
131
|
+
estado[clave] = {
|
|
132
|
+
"anio": p["anio"],
|
|
133
|
+
"mes": p["mes"],
|
|
134
|
+
"producto": p["producto"],
|
|
135
|
+
"precio": None,
|
|
136
|
+
"unidad": "",
|
|
137
|
+
}
|
|
138
|
+
if p["precio"] is not None:
|
|
139
|
+
estado[clave]["precio"] = p["precio"]
|
|
140
|
+
estado[clave]["unidad"] = p["unidad"]
|
|
141
|
+
return [estado[k] for k in orden]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def precio_del_mes(serie: list[dict], producto: str, anio: int, mes: int) -> Optional[float]:
|
|
145
|
+
"""El precio de ese producto en ese mes, o `None` si nunca tuvo cotización.
|
|
146
|
+
|
|
147
|
+
Espera la salida de `serie_mensual()`, no la de `leer_publicaciones()`.
|
|
148
|
+
"""
|
|
149
|
+
for s in serie:
|
|
150
|
+
if s["producto"] == producto and s["anio"] == anio and s["mes"] == mes:
|
|
151
|
+
return s["precio"]
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def descargar(destino: Path, url: str = URL_CSV) -> Path:
|
|
156
|
+
"""Baja el CSV del catálogo a `destino`. **Única función que sale a la red.**
|
|
157
|
+
|
|
158
|
+
Si la URL del recurso cambia, `uydatos.catalogo.recursos(DATASET)` da la vigente.
|
|
159
|
+
"""
|
|
160
|
+
import urllib.request
|
|
161
|
+
|
|
162
|
+
from uydatos import USER_AGENT
|
|
163
|
+
|
|
164
|
+
destino = Path(destino)
|
|
165
|
+
destino.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
167
|
+
with urllib.request.urlopen(req, timeout=90) as r: # noqa: S310 — host fijo
|
|
168
|
+
destino.write_bytes(r.read())
|
|
169
|
+
return destino
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
173
|
+
ap = argparse.ArgumentParser(prog="python -m uydatos.ancap", description=__doc__.splitlines()[0])
|
|
174
|
+
sub = ap.add_subparsers(dest="orden", required=True)
|
|
175
|
+
b = sub.add_parser("bajar")
|
|
176
|
+
b.add_argument("archivo", type=Path)
|
|
177
|
+
s = sub.add_parser("serie", help="precio por mes de un producto (sin argumento: lista los productos)")
|
|
178
|
+
s.add_argument("archivo", type=Path)
|
|
179
|
+
s.add_argument("producto", nargs="?")
|
|
180
|
+
args = ap.parse_args(argv)
|
|
181
|
+
|
|
182
|
+
if args.orden == "bajar":
|
|
183
|
+
print(f"bajado {descargar(args.archivo)}")
|
|
184
|
+
return 0
|
|
185
|
+
publicaciones = leer_publicaciones(args.archivo)
|
|
186
|
+
serie = serie_mensual(publicaciones)
|
|
187
|
+
print(f"{len(publicaciones)} publicaciones, {len(serie)} meses distintos (producto x año x mes)")
|
|
188
|
+
if not args.producto:
|
|
189
|
+
for p in sorted({s["producto"] for s in serie}):
|
|
190
|
+
print(f" {p}")
|
|
191
|
+
return 0
|
|
192
|
+
print("anio;mes;precio;unidad")
|
|
193
|
+
for s in serie:
|
|
194
|
+
if s["producto"] == args.producto:
|
|
195
|
+
print(f"{s['anio']};{s['mes']:02d};{'' if s['precio'] is None else s['precio']};{s['unidad']}")
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
sys.exit(main())
|