omixom-data 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.
- omixom_data-0.3.0/.gitignore +31 -0
- omixom_data-0.3.0/CHANGELOG.md +17 -0
- omixom_data-0.3.0/PKG-INFO +147 -0
- omixom_data-0.3.0/README.md +136 -0
- omixom_data-0.3.0/pyproject.toml +64 -0
- omixom_data-0.3.0/src/omixom_data/__init__.py +68 -0
- omixom_data-0.3.0/src/omixom_data/client.py +277 -0
- omixom_data-0.3.0/src/omixom_data/errors.py +51 -0
- omixom_data-0.3.0/src/omixom_data/events.py +30 -0
- omixom_data-0.3.0/src/omixom_data/feed.py +290 -0
- omixom_data-0.3.0/src/omixom_data/schemas.py +120 -0
- omixom_data-0.3.0/src/omixom_data/state.py +45 -0
- omixom_data-0.3.0/tests/conftest.py +285 -0
- omixom_data-0.3.0/tests/test_client.py +202 -0
- omixom_data-0.3.0/tests/test_feed.py +380 -0
- omixom_data-0.3.0/tox.ini +25 -0
- omixom_data-0.3.0/uv.lock +678 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
.coverage.*
|
|
13
|
+
htmlcov/
|
|
14
|
+
coverage.xml
|
|
15
|
+
*.cover
|
|
16
|
+
|
|
17
|
+
# Virtualenvs
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
|
|
21
|
+
# Tox
|
|
22
|
+
.tox/
|
|
23
|
+
|
|
24
|
+
# Editor / OS
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
.DS_Store
|
|
28
|
+
|
|
29
|
+
# Env files locales
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.0](https://github.com/omixom/dataapi/compare/omixom-data-v0.2.0...omixom-data-v0.3.0) (2026-08-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **client:** agrega Feed.add_modules para sumar sensores a un feed existente ([0734267](https://github.com/omixom/dataapi/commit/07342674cfdfce0f2c00d0a5517ac8cd7e274c3a))
|
|
9
|
+
|
|
10
|
+
## [0.2.0](https://github.com/omixom/dataapi/compare/omixom-data-v0.1.0...omixom-data-v0.2.0) (2026-08-31)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **client:** agrega el cliente python omixom-data ([2b31484](https://github.com/omixom/dataapi/commit/2b31484c51981202ad15ffebdb26f67825910c1e))
|
|
16
|
+
|
|
17
|
+
## Changelog
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: omixom-data
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Cliente Python de la Omixom Data API v3: lectura de series y réplica incremental de mediciones.
|
|
5
|
+
Author: Omixom
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: pydantic>=2.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# omixom-data
|
|
13
|
+
|
|
14
|
+
Cliente Python de la [Omixom Data API v3](https://clima.omixom.com/api/v3/docs): lectura de
|
|
15
|
+
series de mediciones y réplica incremental para mantener una copia siempre al día.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install omixom-data
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requiere Python 3.11+ y un token de acceso de la red Omixom.
|
|
22
|
+
|
|
23
|
+
## Mantener una copia al día
|
|
24
|
+
|
|
25
|
+
El caso típico es replicar las mediciones de un grupo de equipos y seguirlas en el tiempo. El
|
|
26
|
+
`Feed` encapsula todo el protocolo (bootstrap paginado, cursores, coherencia del snapshot) y lo
|
|
27
|
+
entrega en **batches**: cada batch es una página de trabajo acotado que trae sus eventos y el
|
|
28
|
+
estado que la deja atrás.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from omixom_data import Client, MeasurementDeleted
|
|
32
|
+
|
|
33
|
+
with Client(token="...") as client:
|
|
34
|
+
feed = client.feed([30125, 30126]) # toda la historia de cada equipo
|
|
35
|
+
|
|
36
|
+
for batch in feed.batches(): # bootstrap + cambios, hasta estar al día
|
|
37
|
+
for event in batch.events:
|
|
38
|
+
if isinstance(event, MeasurementDeleted):
|
|
39
|
+
store.delete(event.station, event.module, event.time)
|
|
40
|
+
else:
|
|
41
|
+
store.upsert(event.station, event.module, event.time, event.value)
|
|
42
|
+
save(batch.state.model_dump_json()) # checkpoint alineado a la página
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
El contrato es **aplicar primero, guardar después** (idealmente ambos en una transacción del
|
|
46
|
+
store propio). Con ese orden, cortar el proceso en cualquier punto (incluso a mitad de un
|
|
47
|
+
backfill de días) deja como peor caso una página re-aplicada, y aplicar eventos es idempotente
|
|
48
|
+
por diseño: `MeasurementUpserted` deja la copia en ese valor, `MeasurementDeleted` la quita,
|
|
49
|
+
re-aplicar no cambia nada. Cada batch persistido es progreso ganado; `break` entre batches es
|
|
50
|
+
siempre seguro.
|
|
51
|
+
|
|
52
|
+
Para volver a sincronizar (el próximo poll o la próxima corrida del proceso):
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from omixom_data import Client, FeedState
|
|
56
|
+
|
|
57
|
+
with Client(token="...") as client:
|
|
58
|
+
feed = client.resume(FeedState.model_validate_json(saved))
|
|
59
|
+
for batch in feed.batches(): # solo lo que falta desde el checkpoint
|
|
60
|
+
apply_all(batch.events)
|
|
61
|
+
save(batch.state.model_dump_json())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`batches()` es re-llamable: cada llamada avanza hasta quedar al día y la siguiente retoma desde
|
|
65
|
+
ahí, así que el loop de un daemon es `batches()` + esperar el intervalo deseado. El avance es
|
|
66
|
+
por módulo (el estado guarda un cursor por serie) y los requests van por estación: el bootstrap
|
|
67
|
+
de varios equipos avanza round-robin para solapar sus límites de uso.
|
|
68
|
+
|
|
69
|
+
## Acotar el rango
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from datetime import UTC, datetime
|
|
73
|
+
|
|
74
|
+
feed = client.feed([30125], date_from=datetime(2023, 1, 1, tzinfo=UTC)) # desde 2023
|
|
75
|
+
feed = client.feed(
|
|
76
|
+
[30125],
|
|
77
|
+
date_from=datetime(2023, 1, 1, tzinfo=UTC),
|
|
78
|
+
date_to=datetime(2024, 1, 1, tzinfo=UTC), # 2023 completo
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Sin fechas, cada equipo se replica desde su fecha de instalación. Con `date_from` solo, la
|
|
83
|
+
réplica sigue recibiendo lo nuevo; con ambas, la ventana queda cerrada pero las correcciones a
|
|
84
|
+
datos de esa ventana siguen llegando. `modules` acota a esos módulos, de cualquiera de las
|
|
85
|
+
estaciones dadas (una estación sin módulos seleccionados no genera requests); el conjunto
|
|
86
|
+
replicado queda fijado al crear el feed, así que un sensor instalado después no se suma solo
|
|
87
|
+
(ver abajo).
|
|
88
|
+
`categories` filtra qué tipo de dato replicar; con ese filtro, un punto corregido hacia una
|
|
89
|
+
categoría no seleccionada llega como borrado (salió de la vista replicada).
|
|
90
|
+
|
|
91
|
+
## Sumar módulos a un feed existente
|
|
92
|
+
|
|
93
|
+
Un sensor instalado después de crear el feed se suma con `add_modules`, sobre un feed nuevo o
|
|
94
|
+
retomado; sin lista de módulos entran todos los del equipo que falten:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
feed = client.resume(FeedState.model_validate_json(saved))
|
|
98
|
+
feed.add_modules(30125) # o add_modules(30125, [4812]) para uno puntual
|
|
99
|
+
|
|
100
|
+
for batch in feed.batches(): # el nuevo hace su bootstrap; el resto sigue donde estaba
|
|
101
|
+
apply_all(batch.events)
|
|
102
|
+
save(batch.state.model_dump_json())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Devuelve los ids agregados (los ya rastreados se omiten) y rechaza con `ValueError` un módulo
|
|
106
|
+
que no pertenece al equipo. También sirve para sumar una estación nueva al feed. El alta queda
|
|
107
|
+
persistida con el `state` del próximo batch, así que si el proceso corta antes, la próxima corrida
|
|
108
|
+
tiene que volver a llamarlo.
|
|
109
|
+
|
|
110
|
+
## Lecturas puntuales
|
|
111
|
+
|
|
112
|
+
Para consultas de una sola vez, sin réplica:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
client.stations() # equipos accesibles con el token
|
|
116
|
+
client.station(30125) # fecha de instalación y módulos
|
|
117
|
+
|
|
118
|
+
for point in client.series( # la serie completa de una ventana, en streaming
|
|
119
|
+
30125,
|
|
120
|
+
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
|
121
|
+
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
|
122
|
+
):
|
|
123
|
+
print(point.module, point.time, point.value)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`series` camina todas las páginas por adentro repitiendo el cursor de la primera, así el
|
|
127
|
+
resultado entero es un corte coherente de la base aunque haya escrituras concurrentes. Para
|
|
128
|
+
materializar la serie, `list(client.series(...))`.
|
|
129
|
+
|
|
130
|
+
Debajo de eso está el acceso crudo página por página (`series_page`, `changes_page`), donde el
|
|
131
|
+
manejo del cursor queda a cargo del caller: para paginar de forma coherente hay que repetir el
|
|
132
|
+
request con `date_from` igual al `next_from` recibido y `cursor` igual al `cursor` recibido.
|
|
133
|
+
|
|
134
|
+
## Errores y límites de uso
|
|
135
|
+
|
|
136
|
+
Los errores de la API llegan como excepciones tipadas bajo `OmixomDataError`:
|
|
137
|
+
`AuthenticationError`, `NotFoundError`, `InvalidRequestError`, `RateLimitedError` y
|
|
138
|
+
`ServerError`. Ante un 429 el cliente espera lo que indique `Retry-After` y reintenta solo;
|
|
139
|
+
`Client(..., wait_on_rate_limit=False)` desactiva la espera y levanta `RateLimitedError` con el
|
|
140
|
+
tiempo sugerido en `retry_after`.
|
|
141
|
+
|
|
142
|
+
## Desarrollo
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uv sync
|
|
146
|
+
uv run tox # style + tests + cobertura
|
|
147
|
+
```
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# omixom-data
|
|
2
|
+
|
|
3
|
+
Cliente Python de la [Omixom Data API v3](https://clima.omixom.com/api/v3/docs): lectura de
|
|
4
|
+
series de mediciones y réplica incremental para mantener una copia siempre al día.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install omixom-data
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requiere Python 3.11+ y un token de acceso de la red Omixom.
|
|
11
|
+
|
|
12
|
+
## Mantener una copia al día
|
|
13
|
+
|
|
14
|
+
El caso típico es replicar las mediciones de un grupo de equipos y seguirlas en el tiempo. El
|
|
15
|
+
`Feed` encapsula todo el protocolo (bootstrap paginado, cursores, coherencia del snapshot) y lo
|
|
16
|
+
entrega en **batches**: cada batch es una página de trabajo acotado que trae sus eventos y el
|
|
17
|
+
estado que la deja atrás.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from omixom_data import Client, MeasurementDeleted
|
|
21
|
+
|
|
22
|
+
with Client(token="...") as client:
|
|
23
|
+
feed = client.feed([30125, 30126]) # toda la historia de cada equipo
|
|
24
|
+
|
|
25
|
+
for batch in feed.batches(): # bootstrap + cambios, hasta estar al día
|
|
26
|
+
for event in batch.events:
|
|
27
|
+
if isinstance(event, MeasurementDeleted):
|
|
28
|
+
store.delete(event.station, event.module, event.time)
|
|
29
|
+
else:
|
|
30
|
+
store.upsert(event.station, event.module, event.time, event.value)
|
|
31
|
+
save(batch.state.model_dump_json()) # checkpoint alineado a la página
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
El contrato es **aplicar primero, guardar después** (idealmente ambos en una transacción del
|
|
35
|
+
store propio). Con ese orden, cortar el proceso en cualquier punto (incluso a mitad de un
|
|
36
|
+
backfill de días) deja como peor caso una página re-aplicada, y aplicar eventos es idempotente
|
|
37
|
+
por diseño: `MeasurementUpserted` deja la copia en ese valor, `MeasurementDeleted` la quita,
|
|
38
|
+
re-aplicar no cambia nada. Cada batch persistido es progreso ganado; `break` entre batches es
|
|
39
|
+
siempre seguro.
|
|
40
|
+
|
|
41
|
+
Para volver a sincronizar (el próximo poll o la próxima corrida del proceso):
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from omixom_data import Client, FeedState
|
|
45
|
+
|
|
46
|
+
with Client(token="...") as client:
|
|
47
|
+
feed = client.resume(FeedState.model_validate_json(saved))
|
|
48
|
+
for batch in feed.batches(): # solo lo que falta desde el checkpoint
|
|
49
|
+
apply_all(batch.events)
|
|
50
|
+
save(batch.state.model_dump_json())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`batches()` es re-llamable: cada llamada avanza hasta quedar al día y la siguiente retoma desde
|
|
54
|
+
ahí, así que el loop de un daemon es `batches()` + esperar el intervalo deseado. El avance es
|
|
55
|
+
por módulo (el estado guarda un cursor por serie) y los requests van por estación: el bootstrap
|
|
56
|
+
de varios equipos avanza round-robin para solapar sus límites de uso.
|
|
57
|
+
|
|
58
|
+
## Acotar el rango
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from datetime import UTC, datetime
|
|
62
|
+
|
|
63
|
+
feed = client.feed([30125], date_from=datetime(2023, 1, 1, tzinfo=UTC)) # desde 2023
|
|
64
|
+
feed = client.feed(
|
|
65
|
+
[30125],
|
|
66
|
+
date_from=datetime(2023, 1, 1, tzinfo=UTC),
|
|
67
|
+
date_to=datetime(2024, 1, 1, tzinfo=UTC), # 2023 completo
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Sin fechas, cada equipo se replica desde su fecha de instalación. Con `date_from` solo, la
|
|
72
|
+
réplica sigue recibiendo lo nuevo; con ambas, la ventana queda cerrada pero las correcciones a
|
|
73
|
+
datos de esa ventana siguen llegando. `modules` acota a esos módulos, de cualquiera de las
|
|
74
|
+
estaciones dadas (una estación sin módulos seleccionados no genera requests); el conjunto
|
|
75
|
+
replicado queda fijado al crear el feed, así que un sensor instalado después no se suma solo
|
|
76
|
+
(ver abajo).
|
|
77
|
+
`categories` filtra qué tipo de dato replicar; con ese filtro, un punto corregido hacia una
|
|
78
|
+
categoría no seleccionada llega como borrado (salió de la vista replicada).
|
|
79
|
+
|
|
80
|
+
## Sumar módulos a un feed existente
|
|
81
|
+
|
|
82
|
+
Un sensor instalado después de crear el feed se suma con `add_modules`, sobre un feed nuevo o
|
|
83
|
+
retomado; sin lista de módulos entran todos los del equipo que falten:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
feed = client.resume(FeedState.model_validate_json(saved))
|
|
87
|
+
feed.add_modules(30125) # o add_modules(30125, [4812]) para uno puntual
|
|
88
|
+
|
|
89
|
+
for batch in feed.batches(): # el nuevo hace su bootstrap; el resto sigue donde estaba
|
|
90
|
+
apply_all(batch.events)
|
|
91
|
+
save(batch.state.model_dump_json())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Devuelve los ids agregados (los ya rastreados se omiten) y rechaza con `ValueError` un módulo
|
|
95
|
+
que no pertenece al equipo. También sirve para sumar una estación nueva al feed. El alta queda
|
|
96
|
+
persistida con el `state` del próximo batch, así que si el proceso corta antes, la próxima corrida
|
|
97
|
+
tiene que volver a llamarlo.
|
|
98
|
+
|
|
99
|
+
## Lecturas puntuales
|
|
100
|
+
|
|
101
|
+
Para consultas de una sola vez, sin réplica:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
client.stations() # equipos accesibles con el token
|
|
105
|
+
client.station(30125) # fecha de instalación y módulos
|
|
106
|
+
|
|
107
|
+
for point in client.series( # la serie completa de una ventana, en streaming
|
|
108
|
+
30125,
|
|
109
|
+
date_from=datetime(2024, 1, 1, tzinfo=UTC),
|
|
110
|
+
date_to=datetime(2024, 2, 1, tzinfo=UTC),
|
|
111
|
+
):
|
|
112
|
+
print(point.module, point.time, point.value)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`series` camina todas las páginas por adentro repitiendo el cursor de la primera, así el
|
|
116
|
+
resultado entero es un corte coherente de la base aunque haya escrituras concurrentes. Para
|
|
117
|
+
materializar la serie, `list(client.series(...))`.
|
|
118
|
+
|
|
119
|
+
Debajo de eso está el acceso crudo página por página (`series_page`, `changes_page`), donde el
|
|
120
|
+
manejo del cursor queda a cargo del caller: para paginar de forma coherente hay que repetir el
|
|
121
|
+
request con `date_from` igual al `next_from` recibido y `cursor` igual al `cursor` recibido.
|
|
122
|
+
|
|
123
|
+
## Errores y límites de uso
|
|
124
|
+
|
|
125
|
+
Los errores de la API llegan como excepciones tipadas bajo `OmixomDataError`:
|
|
126
|
+
`AuthenticationError`, `NotFoundError`, `InvalidRequestError`, `RateLimitedError` y
|
|
127
|
+
`ServerError`. Ante un 429 el cliente espera lo que indique `Retry-After` y reintenta solo;
|
|
128
|
+
`Client(..., wait_on_rate_limit=False)` desactiva la espera y levanta `RateLimitedError` con el
|
|
129
|
+
tiempo sugerido en `retry_after`.
|
|
130
|
+
|
|
131
|
+
## Desarrollo
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
uv sync
|
|
135
|
+
uv run tox # style + tests + cobertura
|
|
136
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "omixom-data"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Cliente Python de la Omixom Data API v3: lectura de series y réplica incremental de mediciones."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Omixom" }]
|
|
9
|
+
dependencies = ["httpx>=0.27", "pydantic>=2.7"]
|
|
10
|
+
|
|
11
|
+
[dependency-groups]
|
|
12
|
+
dev = [
|
|
13
|
+
"pytest>=8",
|
|
14
|
+
"pytest-cov>=5",
|
|
15
|
+
"ruff>=0.6",
|
|
16
|
+
"ty>=0.0.1a1",
|
|
17
|
+
"tox>=4.21",
|
|
18
|
+
"tox-uv>=1.13",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["hatchling"]
|
|
23
|
+
build-backend = "hatchling.build"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/omixom_data"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
line-length = 100
|
|
30
|
+
target-version = "py311"
|
|
31
|
+
src = ["src", "tests"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "T20", "D"]
|
|
35
|
+
ignore = ["D105", "D107"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint.pydocstyle]
|
|
38
|
+
convention = "google"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint.per-file-ignores]
|
|
41
|
+
"tests/**" = ["D", "S101"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.format]
|
|
44
|
+
quote-style = "double"
|
|
45
|
+
indent-style = "space"
|
|
46
|
+
|
|
47
|
+
[tool.ty.environment]
|
|
48
|
+
python-version = "3.11"
|
|
49
|
+
root = ["src", "."]
|
|
50
|
+
|
|
51
|
+
[tool.ty.src]
|
|
52
|
+
include = ["src", "tests"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = ["--strict-markers", "--strict-config", "--import-mode=importlib", "-ra"]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.run]
|
|
59
|
+
source = ["omixom_data"]
|
|
60
|
+
branch = true
|
|
61
|
+
|
|
62
|
+
[tool.coverage.report]
|
|
63
|
+
fail_under = 95
|
|
64
|
+
show_missing = true
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Cliente Python de la Omixom Data API v3.
|
|
2
|
+
|
|
3
|
+
El camino recomendado es el feed de réplica en batches: cada batch es una página de trabajo
|
|
4
|
+
acotado que trae sus eventos y el estado que la deja atrás.
|
|
5
|
+
|
|
6
|
+
from omixom_data import Client
|
|
7
|
+
|
|
8
|
+
with Client(token="...") as client:
|
|
9
|
+
feed = client.feed([30125, 30126])
|
|
10
|
+
for batch in feed.batches():
|
|
11
|
+
apply_all(batch.events) # aplicar primero
|
|
12
|
+
save(batch.state.model_dump_json()) # persistir después
|
|
13
|
+
|
|
14
|
+
Y para continuar en la próxima corrida, `client.resume(FeedState.model_validate_json(saved))`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from omixom_data.client import DEFAULT_BASE_URL, Client
|
|
18
|
+
from omixom_data.errors import (
|
|
19
|
+
AuthenticationError,
|
|
20
|
+
InvalidRequestError,
|
|
21
|
+
NotFoundError,
|
|
22
|
+
OmixomDataError,
|
|
23
|
+
RateLimitedError,
|
|
24
|
+
ServerError,
|
|
25
|
+
StateError,
|
|
26
|
+
)
|
|
27
|
+
from omixom_data.events import FeedEvent, MeasurementDeleted, MeasurementUpserted
|
|
28
|
+
from omixom_data.feed import Batch, Feed
|
|
29
|
+
from omixom_data.schemas import (
|
|
30
|
+
Category,
|
|
31
|
+
Change,
|
|
32
|
+
ChangesPage,
|
|
33
|
+
Measurement,
|
|
34
|
+
ModuleDetail,
|
|
35
|
+
SeriesModule,
|
|
36
|
+
SeriesPage,
|
|
37
|
+
StationDetail,
|
|
38
|
+
StationSummary,
|
|
39
|
+
)
|
|
40
|
+
from omixom_data.state import FeedState, ModuleState
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"DEFAULT_BASE_URL",
|
|
44
|
+
"AuthenticationError",
|
|
45
|
+
"Batch",
|
|
46
|
+
"Category",
|
|
47
|
+
"Change",
|
|
48
|
+
"ChangesPage",
|
|
49
|
+
"Client",
|
|
50
|
+
"Feed",
|
|
51
|
+
"FeedEvent",
|
|
52
|
+
"FeedState",
|
|
53
|
+
"InvalidRequestError",
|
|
54
|
+
"Measurement",
|
|
55
|
+
"MeasurementDeleted",
|
|
56
|
+
"MeasurementUpserted",
|
|
57
|
+
"ModuleDetail",
|
|
58
|
+
"ModuleState",
|
|
59
|
+
"NotFoundError",
|
|
60
|
+
"OmixomDataError",
|
|
61
|
+
"RateLimitedError",
|
|
62
|
+
"SeriesModule",
|
|
63
|
+
"SeriesPage",
|
|
64
|
+
"ServerError",
|
|
65
|
+
"StateError",
|
|
66
|
+
"StationDetail",
|
|
67
|
+
"StationSummary",
|
|
68
|
+
]
|