Cliente Python para la API Tempus del INE (Instituto Nacional de Estadística de España)
Project description
ine-api
Cliente Python tipado (sync + async) para la API Tempus del INE (Instituto Nacional de Estadística de España).
Estado
v1.0 — API pública estable. Cliente Python tipado (sync + async) para
la API Tempus del INE, con cobertura completa del catálogo (32 endpoints; ver
Cobertura). Publicado en
PyPI; versión instalada: ine.__version__.
Instalación
Requiere Python ≥ 3.12.
pip install ine-api
o con uv:
uv add ine-api
Desarrollo / contribución:
git clone https://github.com/juanmicl/ine-api.git
cd ine-api
uv sync # instala runtime + dev en un .venv aislado
uv run pytest # corre los tests
Quickstart (sync)
from ine import Client, Lang
with Client(lang=Lang.ES) as client:
operaciones = client.operaciones.list()
for op in operaciones[:5]:
print(op.id, op.codigo, op.nombre)
# Últimas 12 observaciones de la serie IPC53262.
# Troceamos en local: en vivo, `nult` puede devolver cuerpos vacíos
# para algunas series del INE.
datos = client.datos.serie("53262")
for serie in datos:
for obs in serie.data[-12:]:
print(obs.fecha.isoformat(), obs.valor)
Client es un gestor de contexto: cierra la conexión HTTP al salir del bloque.
Las respuestas se validan con pydantic (Operacion, DatosSerie, ...).
Quickstart (async)
Para aplicaciones que ya usan asyncio (FastAPI, crawlers, etc.):
import asyncio
from ine import AsyncClient, Lang
async def main() -> None:
async with AsyncClient(lang=Lang.ES) as client:
operaciones = await client.operaciones.list()
for op in operaciones[:5]:
print(op.id, op.codigo, op.nombre)
# Últimas 12 observaciones (troceamos en local; ver nota en el quickstart sync).
datos = await client.datos.serie("53262")
for serie in datos:
for obs in serie.data[-12:]:
print(obs.fecha.isoformat(), obs.valor)
asyncio.run(main())
AsyncClient es el espejo asíncrono de Client: misma API, pero cada método
es una coroutine que se espera con await.
¿Por qué existe?
La API Tempus del INE tiene varias rarezas que un wrapper ingenuo no maneja correctamente. Este cliente las traduce a excepciones tipadas y a modelos pydantic:
- HTTP 200 con cuerpo de error. El INE responde
200 OKcon el body"La operación indicada no existe (X)"(un string JSON) cuando un recurso lógico no existe.raise_for_status()no lo detecta → el cliente lo traduce aINELogicalError. - Redirecciones al resolver códigos. Al pedir por código alfanumérico (p.
ej.
IPC), la API hace un301alIdnumérico. El cliente sigue redirecciones por defecto. - Los 404 devuelven HTML, no JSON. Un recurso inexistente responde
404con una página HTML. El cliente lo detecta por estado y por content-type y lo traduce aINENotFoundError/INEParseError. - Tablas demasiado grandes. El INE rechaza ciertas tablas (p. ej. el
Padrón) con un
200de "restricciones de volumen". El cliente lo traduce aINEVolumeErrory ofrecedownload_table()como vía alternativa (fichero oficial CSV/PC-Axis/XLSX).
Además, el esquema del INE es irregular (claves PascalCase, FK_/T3_,
Fecha como epoch en ms, campos opcionales inconsistentes). Los modelos
pydantic normalizan las claves a snake_case y ofrecen raw=True como
válvula de escape cuando el esquema cambia.
Manejo de errores
Todas las excepciones heredan de INEError, así que basta un
except INEError para capturar cualquier fallo de la librería.
| Excepción | Cuándo |
|---|---|
INEError |
Raíz de la jerarquía. Base para cualquier fallo. |
INEConnectionError |
Red / timeout / DNS / reset de conexión. |
INEHTTPError |
Respuesta HTTP 4xx/5xx. Expone .status, .url y .body. |
INENotFoundError |
Recurso no encontrado (HTTP 404). Subclase de INEHTTPError. |
INELogicalError |
El INE respondió 200 con un mensaje de error lógico (rareza nº 1). |
INEVolumeError |
Subclase de INELogicalError: la tabla es demasiado grande ("restricciones de volumen"). Usa download_table. |
INEParseError |
La respuesta no es JSON o no tiene la forma esperada. |
from ine import Client
from ine.errors import INEConnectionError, INENotFoundError, INEError
with Client() as client:
try:
datos = client.datos.serie("0") # id inválido
except INENotFoundError:
print("La serie no existe.")
except INEConnectionError:
print("No se pudo contactar con el INE (red).")
except INEError as err:
print(f"Otro fallo: {err}")
Importa las excepciones desde ine.errors:
from ine.errors import (
INEError,
INEConnectionError,
INEHTTPError,
INENotFoundError,
INELogicalError,
INEVolumeError,
INEParseError,
)
Parámetros del INE
Varios métodos aceptan estos parámetros de query del INE (todos opcionales):
| Parámetro | Valores válidos | Significado |
|---|---|---|
det |
"0" / "1" / "2" |
Nivel de detalle (básico / detallado / muy detallado). |
tip |
"A" / "M" / "AM" |
Tipo de respuesta: amigable / metadatos / ambos. |
nult |
int |
Devuelve los nult últimos datos o periodos. |
p |
"1" / "3" / "6" / "12" |
Periodicidad: mensual / trimestral / bianual / anual. |
date |
["aaaammdd:aaaammdd"] |
Rango de fechas (el final es opcional: aaaammdd:). |
tv |
["id_variable:id_valor", ...] |
Filtros variable:valor (repetibles). |
filtros |
list[(var, [valores])] → param g |
Grupos OR (mismo grupo) / AND (grupos distintos). |
geo |
"0" / "1" |
Ámbito geográfico: 0 nacional / 1 desagregado (CCAA, provincias, municipios). |
clasif |
int |
Restringe a una clasificación concreta (en valores.by_variable). |
page |
int |
Página de un listado paginado (hasta 500 elem./página). |
raw |
bool |
Si es True, devuelve el dict crudo del INE (sin modelo). |
Filtros g (parámetro filtros): una lista de grupos (variable, valores).
- Varios valores en un mismo grupo → OR (
g1=["115:29","115:30"]). - Varios grupos → AND (
g1=...+g2="3:84"). valores=None→ todos los valores de esa variable (g3="762:").
client.datos.metadata_operacion(
"IPC", p="1", nult=12,
filtros=[("115", ["29", "30"]), ("3", ["84"])],
)
Cobertura de endpoints
Soportados (32 — catálogo completo)
| Dominio | Método (namespace) | Recurso |
|---|---|---|
| OPERACIONES | client.operaciones.list() |
OPERACIONES_DISPONIBLES |
client.operaciones.get(id) |
OPERACION/{id} |
|
| SERIES | client.series.get(id) |
SERIE/{id} |
client.series.by_operacion(op) |
SERIES_OPERACION/{op} |
|
client.series.by_tabla(id) |
SERIES_TABLA/{id} |
|
client.series.valores(id) |
VALORES_SERIE/{id} |
|
client.series.metadata_operacion(op, filtros=...) |
SERIE_METADATAOPERACION/{op} |
|
| DATOS | client.datos.tabla(id) |
DATOS_TABLA/{id} |
client.datos.serie(id, ...) |
DATOS_SERIE/{id} |
|
client.datos.metadata_operacion(op, filtros=...) |
DATOS_METADATAOPERACION/{op} |
|
| MAESTROS | client.maestros.escalas() |
ESCALAS |
client.maestros.escala(id) |
ESCALA/{id} |
|
client.maestros.unidades() |
UNIDADES |
|
client.maestros.unidad(id) |
UNIDAD/{id} |
|
client.maestros.unidades_operacion(op) |
UNIDADES_OPERACION/{op} |
|
client.maestros.periodo(id) |
PERIODO/{id} |
|
client.maestros.periodicidades() |
PERIODICIDADES |
|
client.maestros.periodicidad(id) |
PERIODICIDAD/{id} |
|
client.maestros.clasificaciones() |
CLASIFICACIONES |
|
client.maestros.clasificaciones_operacion(op) |
CLASIFICACIONES_OPERACION/{op} |
|
| PUBLICACIONES | client.publicaciones.publicaciones() |
PUBLICACIONES |
client.publicaciones.publicaciones_operacion(op) |
PUBLICACIONES_OPERACION/{op} |
|
client.publicaciones.publicacion_fecha(id) |
PUBLICACIONFECHA_PUBLICACION/{id} |
|
| VARIABLES | client.variables.variables() |
VARIABLES |
client.variables.variables_operacion(op) |
VARIABLES_OPERACION/{op} |
|
client.variables.variable(id) |
VARIABLE/{id} (no documentado) |
|
| VALORES | client.valores.by_variable(id_var) |
VALORES_VARIABLE/{id_var} |
client.valores.by_variable_operacion(id_var, op) |
VALORES_VARIABLEOPERACION/{id_var}/{op} |
|
client.valores.hijos(id_var, id_valor) |
VALORES_HIJOS/{id_var}/{id_valor} |
|
| TABLAS | client.tablas.by_operacion(op) |
TABLAS_OPERACION/{op} |
client.tablas.grupos(id) |
GRUPOS_TABLA/{id} |
|
client.tablas.valores_grupo(id, id_grupo) |
VALORES_GRUPOSTABLA/{id}/{id_grupo} |
Incluye 8 endpoints no documentados en el OpenAPI oficial (7 en
client.maestros
client.variables.variable), descubiertos empíricamente y verificados en vivo.
Cobertura completa de la API Tempus del INE: los 32 endpoints del catálogo (24 del spec oficial + 8 no documentados).
Configuración
Todos los parámetros del constructor son keyword-only (la firma es estable entre versiones):
from ine import Cache, Client, Lang
client = Client(
lang=Lang.ES, # idioma de los textos de la respuesta
base_url="https://servicios.ine.es", # host del servicio Tempus
timeout=10.0, # timeout por petición, en segundos
follow_redirects=True, # seguir redirecciones (el INE hace 301 al resolver códigos)
retries=3, # reintentos sobre GET idempotente (red + 429 + 5xx)
headers={"X-Custom": "..."}, # cabeceras extra
cache=Cache(ttl=300), # cache en memoria opt-in (None = sin cache, por defecto)
httpx_client=None, # cliente httpx inyectado (DI para tests/config avanzada)
)
lang—Lang.ES|EN|CA|GL|EU. Determina el segmento/js/{lang}/de las URLs y el idioma de los textos.retries— reintentos automáticos sobre GET idempotente ante errores de red y429/5xx, con backoff y respeto aRetry-After.0los desactiva. Solo aplica cuando el cliente construye su propiohttpx.Client.httpx_client— inyección de dependencias: si pasas tu propiohttpx.Client, se respeta tal cual (sin reintentos ni cabeceras propias). Útil para tests (conrespx) o para configuración avanzada del transporte.
El gestor de contexto cierra la conexión HTTP al salir:
with Client() as client: # abre
...
# cierra automáticamente
Cache (opt-in)
Para no repetir peticiones idénticas al INE dentro de un mismo proceso (menos
latencia y carga), activa un cache en memoria con TTL pasando un objeto
Cache. Por defecto está desactivado (cache=None) — nunca te servirá
datos stale sin que tú lo pidas:
from ine import Cache, Client
with Client(cache=Cache(ttl=300)) as client: # cachea 5 min
a = client.series.by_operacion("IPC") # → petición HTTP
b = client.series.by_operacion("IPC") # → cache (0 peticiones)
Cache(*, ttl=300, maxsize=None)—ttlen segundos;maxsizeopcional (evicción FIFO cuando se alcanza).- Solo se cachean las respuestas válidas; los errores (
INEError) se relanzan siempre (nunca se cachean). - Es memoria por proceso (se pierde al cerrar). La misma instancia
Cachepuede compartirirse entre variosClient(sync y async). - El modelado pydantic se re-ejecuta sobre el dato cacheado (barato); si
necesitas el JSON crudo sin re-validar, usa
raw=True.
Descarga de ficheros (CSV / PC-Axis / XLSX)
Para tablas muy grandes que la API JSON rechaza ("restricciones de volumen",
p. ej. el Padrón, id 68535) o cuando necesitas el formato oficial, descarga el
fichero directamente. Es un servicio distinto (ine.es/jaxiT3/files, no la
API Tempus JSON) y la descarga es por streaming:
from ine import Client, Format
with Client() as client:
# Streama por chunks a fichero (seguro para decenas de MB) → devuelve Path
path = client.download_table("68535", fmt=Format.CSV_BDSC, path="padron.csv")
# O a bytes en memoria (cuidado con tablas muy grandes)
data = client.download_table("68535", fmt=Format.PX) # → bytes
Format:CSV_BDSC(CSV con cabecera, separador;),CSV_BD,PX(PC-Axis),XLSX.download_tablevive en la raíz delClient(no bajo un namespace) porque es un servicio distinto al de la API JSON.pathdado → streama al fichero y devuelvepathlib.Path;path=None→bytes(se carga entero en memoria).langpor defecto es el del cliente; los bytes son crudos (el charset del INE es inconsistente: declara ISO-8859-15 pero lleva BOM UTF-8).- Errores:
INENotFoundError(404),INEHTTPError,INEConnectionError. - Cuándo usar esto vs
client.datos.tabla: para tablas normales,datos.tablaes mejor (filtrable connult/date/tv, tipado).download_tablees la salida para tablas bloqueadas por volumen o cuando quieres el fichero oficial.
Pandas (opcional)
Para análisis, exporta las observaciones de una serie a un DataFrame de pandas.
Es una dependencia opcional — el core no depende de pandas:
pip install ine-api[dataframe]
from ine import Client
with Client() as c:
series = c.datos.serie("53262") # -> list[DatosSerie]
df = series[0].to_dataframe() # -> pd.DataFrame (requiere el extra `dataframe`)
# columnas: fecha, valor, anyo, fk_periodo, secreto
Si pandas no está instalado, to_dataframe() lanza un ImportError con la pista
de instalación.
Licencia
- El código de este cliente está bajo la licencia MIT (ver
LICENSE). - Los datos del INE distribuidos a través de su API están bajo la licencia Creative Commons Attribution 4.0 (CC BY 4.0). Al usarlos debes atribuirlos al INE (Instituto Nacional de Estadística). Consulta los términos de reutilización del INE.
Esta librería no está afiliada al INE. Es un cliente de la comunidad.
Contribuir
Las contribuciones son bienvenidas. Configuración del entorno:
uv sync
Los gates de CI que debe pasar cualquier cambio son:
uv run ruff check . && uv run mypy ine && uv run pytest
Para errores, ideas o contribuciones, abre un issue en GitHub.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ine_api-1.0.0.tar.gz.
File metadata
- Download URL: ine_api-1.0.0.tar.gz
- Upload date:
- Size: 79.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34c451c138a3842f05b6f37f187844ee0a4f1368be8a142725fa86ac5c5a9473
|
|
| MD5 |
592ad1f5608d7c9e894d8598b9e027f4
|
|
| BLAKE2b-256 |
d407926088661de2a9461fb7b2c90124113b755fe567e556b5d45a181ffe40a1
|
Provenance
The following attestation bundles were made for ine_api-1.0.0.tar.gz:
Publisher:
release.yml on juanmicl/ine-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ine_api-1.0.0.tar.gz -
Subject digest:
34c451c138a3842f05b6f37f187844ee0a4f1368be8a142725fa86ac5c5a9473 - Sigstore transparency entry: 2138501142
- Sigstore integration time:
-
Permalink:
juanmicl/ine-api@1b7da5bced3888af594577c505d7db9fd5e7008c -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/juanmicl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1b7da5bced3888af594577c505d7db9fd5e7008c -
Trigger Event:
push
-
Statement type:
File details
Details for the file ine_api-1.0.0-py3-none-any.whl.
File metadata
- Download URL: ine_api-1.0.0-py3-none-any.whl
- Upload date:
- Size: 42.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08a32361d178f2c9ae9761eee27140dff396d4ab8eea84650b00392c3689ed34
|
|
| MD5 |
1d0b18e3665ce8d042c468f54da74a5f
|
|
| BLAKE2b-256 |
08712975498aea94272f5defbb9cd5e4d6fb77710a8a86911cd796731ac6b8f2
|
Provenance
The following attestation bundles were made for ine_api-1.0.0-py3-none-any.whl:
Publisher:
release.yml on juanmicl/ine-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ine_api-1.0.0-py3-none-any.whl -
Subject digest:
08a32361d178f2c9ae9761eee27140dff396d4ab8eea84650b00392c3689ed34 - Sigstore transparency entry: 2138501167
- Sigstore integration time:
-
Permalink:
juanmicl/ine-api@1b7da5bced3888af594577c505d7db9fd5e7008c -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/juanmicl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1b7da5bced3888af594577c505d7db9fd5e7008c -
Trigger Event:
push
-
Statement type: