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.
@@ -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
+ ]