omixom-data
Cliente Python de la Omixom Data API v3 para la consulta de series de mediciones y la réplica incremental que mantiene una copia local sincronizada.
pip install omixom-data
Requiere Python 3.11 o superior y un token de acceso de la red Omixom.
Réplica incremental
El caso de uso principal es replicar las mediciones de un conjunto de estaciones y mantener esa
réplica actualizada en el tiempo. El Feed encapsula el protocolo completo (bootstrap paginado,
cursores, coherencia del snapshot) y entrega el resultado en batches, donde cada batch
contiene una página de eventos junto con el estado que queda tras aplicarla.
from omixom_data import Client, MeasurementDeleted
with Client(token="...") as client:
feed = client.feed([30125, 30126]) # la historia completa de cada estación
for batch in feed.batches(): # bootstrap y cambios pendientes
for event in batch.events:
if isinstance(event, MeasurementDeleted):
store.delete(event.station, event.module, event.time)
else:
store.upsert(event.station, event.module, event.time, event.value)
save(batch.state.model_dump_json()) # checkpoint alineado a la página
El contrato es aplicar primero y persistir el estado después, idealmente ambos dentro de una
transacción del almacenamiento propio. Con ese orden, una interrupción en cualquier punto
(incluso durante un backfill extenso) tiene como peor caso una página re-aplicada, y aplicar
eventos es idempotente (MeasurementUpserted fija el valor, MeasurementDeleted lo elimina y
re-aplicarlos no altera el resultado). Cada batch persistido consolida el avance; interrumpir el
recorrido entre batches es siempre seguro.
Sincronización continua
Un proceso de sincronización continua reúne las piezas anteriores, retomando el estado
persistido si existe (client.resume), procesando los batches pendientes y esperando el
intervalo de consulta.
El estado se guarda serializado en el mismo store que recibe los datos, y cada batch se
confirma en una única transacción que abarca los eventos y el checkpoint.
import time
from omixom_data import Client, FeedState, MeasurementDeleted, MeasurementUpserted
POLL_INTERVAL = 300 # segundos
STATIONS = [30125, 30126]
def apply(event: MeasurementUpserted | MeasurementDeleted) -> None:
if isinstance(event, MeasurementDeleted):
store.delete(event.station, event.module, event.time)
else:
store.upsert(event.station, event.module, event.time, event.value)
with Client(token="...") as client:
saved = store.load_state() # el JSON guardado en la corrida anterior, o None
if saved is not None:
feed = client.resume(FeedState.model_validate_json(saved))
else:
feed = client.feed(STATIONS)
while True:
for batch in feed.batches(): # solo lo pendiente desde el último checkpoint
with store.transaction(): # eventos y checkpoint juntos, todo o nada
for event in batch.events:
apply(event)
store.save_state(batch.state.model_dump_json())
time.sleep(POLL_INTERVAL)
Con esa transacción por batch el checkpoint queda exactamente alineado con lo aplicado,
porque una falla a mitad de un batch revierte todo y la corrida siguiente retoma desde el checkpoint
anterior, sin estados intermedios. Si el estado no puede vivir en el store y se guarda aparte
(por ejemplo, en un archivo JSON local, escrito de forma atómica vía un temporal y
Path.replace), la garantía se relaja, y una interrupción entre aplicar y guardar implica
re-aplicar la última página en la corrida siguiente, lo que no altera el resultado por la
idempotencia de los eventos.
batches() puede invocarse repetidamente sobre el mismo feed, ya que cada llamada avanza
hasta alcanzar el estado actual y la siguiente retoma desde ese punto, por lo que el ciclo del
proceso es invocarlo y esperar el intervalo, manteniendo el Client abierto para reutilizar
las conexiones. El bootstrap se pide por estación, con varias estaciones avanzando round-robin
para distribuir los límites de uso, mientras que el seguimiento de cambios usa un único cursor
para todo el feed y pide en cada request los cambios de la lista explícita de módulos seguidos,
de todas sus estaciones (POST /api/v3/measurements/changes), de modo que un poll sin atraso
cuesta un solo request. El estado que guardó una versión anterior del cliente (version 1, con
un cursor por módulo) se sigue leyendo, ya que FeedState.model_validate_json lo migra al
formato actual al cargarlo. Las versiones anteriores del cliente seguían los cambios con una
ruta por estación que la API ya no ofrece, así que dejan de funcionar y hay que actualizarlas.
Acotar el rango
from datetime import UTC, datetime
feed = client.feed([30125], date_from=datetime(2023, 1, 1, tzinfo=UTC)) # desde 2023
feed = client.feed(
[30125],
date_from=datetime(2023, 1, 1, tzinfo=UTC),
date_to=datetime(2024, 1, 1, tzinfo=UTC), # 2023 completo
)
Sin fechas, cada estación se replica desde su fecha de instalación. Con date_from únicamente,
la réplica continúa recibiendo los datos nuevos; con ambas fechas la ventana queda cerrada,
aunque las correcciones sobre datos de esa ventana siguen llegando. modules restringe la
réplica a esos módulos, de cualquiera de las estaciones indicadas (una estación sin módulos
seleccionados no genera requests); el conjunto replicado queda fijado al crear el feed, por lo
que un sensor instalado con posterioridad no se incorpora automáticamente (ver la sección
siguiente). categories filtra el tipo de dato a replicar; con ese filtro, un punto corregido
hacia una categoría no seleccionada se recibe como borrado, dado que salió de la vista
replicada.
Incorporar módulos a un feed existente
Un módulo instalado después de crear el feed se incorpora con add_modules, sobre un feed nuevo
o retomado; sin lista de módulos se incorporan todos los de la estación que aún no estén
rastreados.
feed = client.resume(FeedState.model_validate_json(saved))
feed.add_modules(30125) # o add_modules(30125, [4812]) para un módulo puntual
for batch in feed.batches(): # el módulo nuevo hace su bootstrap; el resto continúa
apply_all(batch.events)
save(batch.state.model_dump_json())
El método devuelve los ids incorporados (los ya rastreados se omiten) y levanta ValueError
ante un módulo que no pertenece a la estación. Sirve también para incorporar una estación nueva
al feed. El alta queda persistida con el state del batch siguiente; si el proceso se
interrumpe antes, la corrida siguiente debe repetir la llamada.
El seguimiento pide solo los módulos que el feed rastrea y que ya completaron su bootstrap. Un
módulo que el feed no rastrea (un sensor nuevo, una estación que no está en el feed o un módulo
fuera de modules) entra a la réplica recién con su bootstrap, que trae su historia completa; a
partir de ahí sus cambios siguen llegando con el resto.
Módulos fuera del alcance
Un módulo seguido sale del alcance del token cuando la cuenta pierde el acceso a su estación,
cuando el módulo se deshabilita o cuando deja de existir. La API no distingue esos casos y lo
devuelve en missing. El feed no se corta: el módulo queda stale, su copia se congela en el
último cursor en que estuvo cubierto y el resto de los módulos sigue avanzando. Cada batch lista
en batch.stale los módulos del feed que quedaron fuera del alcance, y state.modules[id].stale
lo registra en el estado.
for batch in feed.batches():
apply_all(batch.events)
save(batch.state.model_dump_json())
if batch.stale:
log.warning("módulos fuera del alcance: %s", batch.stale)
El seguimiento sigue pidiendo los módulos stale, así que la API avisa cuando vuelven sin
requests de más. La excepción es un módulo que no terminó su bootstrap: cada llamada a
batches() consulta el detalle de su estación para saber si volvió. Cuando un módulo vuelve, el
batch de fase recovery trae sus cambios desde el cursor en que quedó, junto con los demás que
volvieron, hasta alcanzar el cursor del feed. Ahí vuelve al seguimiento común. Lo mismo vale
para un módulo cuya estación sale del alcance antes de terminar su bootstrap: el bootstrap
espera y sigue desde donde quedó.
Un módulo stale no tiene tope de tiempo y el feed nunca lo da de baja solo. Si el consumidor
decide que no va a volver, feed.remove_modules([id]) deja de seguirlo, y la copia de ese
módulo queda a cargo del consumidor.
Lecturas puntuales
Las lecturas puntuales resuelven consultas únicas, sin réplica de por medio.
client.stations() # estaciones accesibles con el token
client.station(30125) # fecha de instalación y módulos
for point in client.series( # la serie completa de una ventana, en streaming
30125,
date_from=datetime(2024, 1, 1, tzinfo=UTC),
date_to=datetime(2024, 2, 1, tzinfo=UTC),
):
print(point.module, point.time, point.value)
series recorre internamente todas las páginas repitiendo el cursor de la primera, de modo que
el resultado completo es un corte coherente de la base aun ante escrituras concurrentes. Para
materializar la serie completa, list(client.series(...)).
Por debajo existe el acceso crudo página por página (series_page de una estación y
account_changes_page de una lista de módulos de cualquier estación), donde el manejo del
cursor queda a cargo del caller. Para paginar la serie de forma coherente se repite el request
con date_from igual al next_from recibido y cursor igual al cursor recibido. Para los
cambios se repite el request con el cursor recibido mientras has_more venga en true, y un
módulo en missing no avanza con ese cursor.
Errores y límites de uso
Los errores de la API se levantan como excepciones tipadas bajo OmixomDataError
(AuthenticationError, NotFoundError, InvalidRequestError, RateLimitedError y
ServerError). Ante un 429 el cliente espera el tiempo indicado en Retry-After y reintenta
automáticamente; Client(..., wait_on_rate_limit=False) desactiva esa espera y levanta
RateLimitedError con el tiempo sugerido en retry_after.
Metadata
Release files for omixom-data 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| omixom_data-0.4.0.tar.gz | 61.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| omixom_data-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 80.6 kB
Release files / omixom_data-0.4.0.tar.gz
| Download URL | omixom_data-0.4.0.tar.gz |
|---|---|
| Size | 61.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
61f93231279ae40404c575dee72ebcfc8242e9bc4c78740cc0e0e4d8b9157bc9
|
|
BLAKE2b-256 checksum How to use checksums |
fa9db97466ca7a9a95cfdbaf0cf8940c51a1481a00aa35fe46adec8a73ee2c08
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / omixom_data-0.4.0-py3-none-any.whl
| Download URL | omixom_data-0.4.0-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ff9478b9db1345815e7a4aefc96c5207f1139123ae89e541e3210c9808f3e69e
|
|
BLAKE2b-256 checksum How to use checksums |
f4fef414f27dfe594075d1ece252b633bb3e05d8af69dbed05e3e73618bc9403
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for omixom-data 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.