Skip to main content

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).

PyPI Python License CI typed


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:

  1. HTTP 200 con cuerpo de error. El INE responde 200 OK con 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 a INELogicalError.
  2. Redirecciones al resolver códigos. Al pedir por código alfanumérico (p. ej. IPC), la API hace un 301 al Id numérico. El cliente sigue redirecciones por defecto.
  3. Los 404 devuelven HTML, no JSON. Un recurso inexistente responde 404 con una página HTML. El cliente lo detecta por estado y por content-type y lo traduce a INENotFoundError / INEParseError.
  4. Tablas demasiado grandes. El INE rechaza ciertas tablas (p. ej. el Padrón) con un 200 de "restricciones de volumen". El cliente lo traduce a INEVolumeError y ofrece download_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 gruposAND (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)
)
  • langLang.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 y 429/5xx, con backoff y respeto a Retry-After. 0 los desactiva. Solo aplica cuando el cliente construye su propio httpx.Client.
  • httpx_client — inyección de dependencias: si pasas tu propio httpx.Client, se respeta tal cual (sin reintentos ni cabeceras propias). Útil para tests (con respx) 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)ttl en segundos; maxsize opcional (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 Cache puede compartirirse entre varios Client (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_table vive en la raíz del Client (no bajo un namespace) porque es un servicio distinto al de la API JSON.
  • path dado → streama al fichero y devuelve pathlib.Path; path=Nonebytes (se carga entero en memoria).
  • lang por 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.tabla es mejor (filtrable con nult/date/tv, tipado). download_table es 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ine_api-1.0.0.tar.gz (79.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ine_api-1.0.0-py3-none-any.whl (42.5 kB view details)

Uploaded Python 3

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

Hashes for ine_api-1.0.0.tar.gz
Algorithm Hash digest
SHA256 34c451c138a3842f05b6f37f187844ee0a4f1368be8a142725fa86ac5c5a9473
MD5 592ad1f5608d7c9e894d8598b9e027f4
BLAKE2b-256 d407926088661de2a9461fb7b2c90124113b755fe567e556b5d45a181ffe40a1

See more details on using hashes here.

Provenance

The following attestation bundles were made for ine_api-1.0.0.tar.gz:

Publisher: release.yml on juanmicl/ine-api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for ine_api-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 08a32361d178f2c9ae9761eee27140dff396d4ab8eea84650b00392c3689ed34
MD5 1d0b18e3665ce8d042c468f54da74a5f
BLAKE2b-256 08712975498aea94272f5defbb9cd5e4d6fb77710a8a86911cd796731ac6b8f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for ine_api-1.0.0-py3-none-any.whl:

Publisher: release.yml on juanmicl/ine-api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page