myelectricaldatapy 2.4.8__tar.gz → 2.5.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.
Files changed (51) hide show
  1. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/PKG-INFO +1 -1
  2. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/api.py +11 -1
  3. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/exceptions.py +8 -1
  4. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/mypdl.py +20 -1
  5. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy.egg-info/PKG-INFO +1 -1
  6. myelectricaldatapy-2.5.0/myelectricaldatapy.egg-info/scm_version.json +8 -0
  7. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/test_load_data.py +100 -0
  8. myelectricaldatapy-2.4.8/myelectricaldatapy.egg-info/scm_version.json +0 -8
  9. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.devcontainer/devcontainer-lock.json +0 -0
  10. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.devcontainer/devcontainer.json +0 -0
  11. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.github/dependabot.yml +0 -0
  12. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.github/workflows/auto-approve.yml +0 -0
  13. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.github/workflows/python-lint.yml +0 -0
  14. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.github/workflows/python-publish.yml +0 -0
  15. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.github/workflows/release.yml +0 -0
  16. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.gitignore +0 -0
  17. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.pre-commit-config.yaml +0 -0
  18. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/.vscode/settings.json +0 -0
  19. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/LICENSE +0 -0
  20. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/README.md +0 -0
  21. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/_secrets.yaml +0 -0
  22. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/example.py +0 -0
  23. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/__init__.py +0 -0
  24. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/analytics.py +0 -0
  25. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/auth.py +0 -0
  26. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/const.py +0 -0
  27. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/py.typed +0 -0
  28. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/types.py +0 -0
  29. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy/tz.py +0 -0
  30. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy.egg-info/SOURCES.txt +0 -0
  31. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
  32. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy.egg-info/requires.txt +0 -0
  33. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy.egg-info/scm_file_list.json +0 -0
  34. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/myelectricaldatapy.egg-info/top_level.txt +0 -0
  35. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/pyproject.toml +0 -0
  36. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/setup.cfg +0 -0
  37. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/__init__.py +0 -0
  38. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/conftest.py +0 -0
  39. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/consts.py +0 -0
  40. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/access.json +0 -0
  41. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/address.json +0 -0
  42. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/contract.json +0 -0
  43. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/daily.json +0 -0
  44. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/detail.json +0 -0
  45. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/ecowatt.json +0 -0
  46. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/identity.json +0 -0
  47. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/invalid_access.json +0 -0
  48. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/fixtures/tempo.json +0 -0
  49. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/test_analytics.py +0 -0
  50. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/tests/test_auth.py +0 -0
  51. {myelectricaldatapy-2.4.8 → myelectricaldatapy-2.5.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myelectricaldatapy
3
- Version: 2.4.8
3
+ Version: 2.5.0
4
4
  Summary: Fetch Linky data from myelectricaldata.fr
5
5
  Author-email: Cyr-ius <cyr-ius@ipocus.net>
6
6
  License-Expression: GPL-3.0-or-later
@@ -11,7 +11,7 @@ from pydantic import BaseModel, ValidationError
11
11
 
12
12
  from .auth import EnedisAuth
13
13
  from .const import DAILY_CONSUM, DAILY_PROD, DETAIL_CONSUM, DETAIL_PROD
14
- from .exceptions import EnedisException, PayloadError
14
+ from .exceptions import EnedisException, LimitReached, PayloadError
15
15
  from .types import (
16
16
  AccessResponse,
17
17
  Contract,
@@ -275,6 +275,16 @@ class Enedis:
275
275
  DataCollect,
276
276
  await self.async_fetch_datas(service, pdl, start, end),
277
277
  )
278
+ except LimitReached as error:
279
+ # Quota/throttling errors apply to every remaining chunk too,
280
+ # so let the caller see them (e.g. to back off until
281
+ # ThrottlingError.next_access_time) instead of swallowing them.
282
+ # Whatever chunks already succeeded this call are attached to
283
+ # the exception so the caller can still persist that partial
284
+ # progress instead of discarding it and re-requesting the
285
+ # same full range (and hitting the same quota wall) next time.
286
+ error.partial_data = data
287
+ raise
278
288
  except EnedisException as error:
279
289
  raise_error = True
280
290
  logger.error(error)
@@ -8,7 +8,14 @@ class EnedisException(Exception):
8
8
 
9
9
 
10
10
  class LimitReached(EnedisException):
11
- """Limit reached exception."""
11
+ """Limit reached exception.
12
+
13
+ ``partial_data`` optionally carries whatever a paginated fetch (e.g.
14
+ ``Enedis._async_get_details``) already collected before hitting the
15
+ limit, so callers can persist that progress instead of discarding it.
16
+ """
17
+
18
+ partial_data: Any = None
12
19
 
13
20
 
14
21
  class ThrottlingError(LimitReached):
@@ -386,7 +386,26 @@ class EnedisByPDL:
386
386
  end = attr[ATTR_END]
387
387
  fn = attr[ATTR_FN]
388
388
 
389
- dataset: DataCollect | None = await fn(self.pdl, start, end)
389
+ try:
390
+ dataset: DataCollect | None = await fn(self.pdl, start, end)
391
+ except LimitReached as error:
392
+ # A paginated (detail) fetch may have already collected some
393
+ # chunks before hitting the quota/throttle wall. Persist that
394
+ # partial progress -- same as a normal successful fetch --
395
+ # instead of losing it and re-requesting the whole range
396
+ # (and the same wall) on the next cycle.
397
+ partial = error.partial_data
398
+ readings = partial.meter_reading.interval_reading if partial else []
399
+ if readings:
400
+ self._params[mode].update(
401
+ {
402
+ "data": [
403
+ reading.model_dump(exclude_none=True)
404
+ for reading in readings
405
+ ]
406
+ }
407
+ )
408
+ raise
390
409
 
391
410
  if dataset is None:
392
411
  raise EnedisException("Data collection is empty")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myelectricaldatapy
3
- Version: 2.4.8
3
+ Version: 2.5.0
4
4
  Summary: Fetch Linky data from myelectricaldata.fr
5
5
  Author-email: Cyr-ius <cyr-ius@ipocus.net>
6
6
  License-Expression: GPL-3.0-or-later
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.5.0",
3
+ "distance": 0,
4
+ "node": "g04b14d4dd379114e4a869f878e146c5ed2765401",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-26"
8
+ }
@@ -16,7 +16,9 @@ from myelectricaldatapy import (
16
16
  EnedisException,
17
17
  LimitReached,
18
18
  Subscription,
19
+ ThrottlingError,
19
20
  )
21
+ from myelectricaldatapy.const import ATTR_CONSUM
20
22
  from myelectricaldatapy.tz import LOCAL_TIMEZONE
21
23
 
22
24
  from .consts import PDL, TOKEN
@@ -249,3 +251,101 @@ async def test_exception(
249
251
  pass
250
252
  assert api.last_access is not None
251
253
  assert api.access.valid is True
254
+
255
+
256
+ async def test_details_throttling_propagates(
257
+ mock_access,
258
+ session,
259
+ ) -> None:
260
+ """A ThrottlingError raised while fetching a details chunk must reach the caller.
261
+
262
+ Regression test: ``Enedis._async_get_details`` used to catch every
263
+ ``EnedisException`` per 7-day chunk and only log it, so callers (and
264
+ their ``next_access_time`` back-off logic) never saw the throttling.
265
+ Deliberately bypasses the ``mock_enedis`` fixture, which stubs
266
+ ``async_get_details_consumption`` wholesale and would skip the internal
267
+ per-chunk loop this test targets.
268
+ """
269
+ throttling = ThrottlingError(
270
+ "Throttled", next_access_time="2026-Sep-05 16:00:00+0000 UTC"
271
+ )
272
+ with (
273
+ patch.object(
274
+ myelectricaldatapy.Enedis,
275
+ "async_valid_access",
276
+ return_value=mock_access,
277
+ ),
278
+ patch.object(
279
+ myelectricaldatapy.Enedis,
280
+ "async_fetch_datas",
281
+ side_effect=throttling,
282
+ ),
283
+ ):
284
+ api = EnedisByPDL(pdl=PDL, token=TOKEN, session=session)
285
+ api.set_data_fetch(DETAIL_CONSUM)
286
+ with pytest.raises(ThrottlingError) as exc_info:
287
+ await api.async_update()
288
+ assert exc_info.value.next_access_time == "2026-Sep-05 16:00:00+0000 UTC"
289
+
290
+
291
+ async def test_details_partial_progress_kept_on_throttle(
292
+ mock_access,
293
+ session,
294
+ ) -> None:
295
+ """Chunks fetched before a mid-range throttle must not be discarded.
296
+
297
+ Regression test: once ``_async_get_details`` correctly re-raises on a
298
+ throttled chunk (see ``test_details_throttling_propagates``), it still
299
+ discarded whatever earlier 7-day chunks it had already collected in the
300
+ same call, forcing the next cycle to re-request the whole range -- and
301
+ hit the same quota wall again -- instead of making forward progress.
302
+ """
303
+ first_chunk = {
304
+ "meter_reading": {
305
+ "interval_reading": [
306
+ {"value": "1000", "date": "2023-01-01 00:30:00"},
307
+ ]
308
+ }
309
+ }
310
+ throttling = ThrottlingError(
311
+ "Throttled", next_access_time="2026-Sep-05 16:00:00+0000 UTC"
312
+ )
313
+
314
+ with (
315
+ patch.object(
316
+ myelectricaldatapy.Enedis,
317
+ "async_valid_access",
318
+ return_value=mock_access,
319
+ ),
320
+ # Contract/address lookups also go through `async_fetch_datas`; stub
321
+ # them directly so they don't consume the chunk side_effect below.
322
+ patch.object(
323
+ myelectricaldatapy.Enedis, "async_get_contract", return_value=None
324
+ ),
325
+ patch.object(myelectricaldatapy.Enedis, "async_get_address", return_value=None),
326
+ patch.object(
327
+ myelectricaldatapy.Enedis,
328
+ "async_fetch_datas",
329
+ side_effect=[first_chunk, throttling],
330
+ ),
331
+ ):
332
+ api = EnedisByPDL(pdl=PDL, token=TOKEN, session=session)
333
+ # 14 days split into 7-day chunks: the first succeeds, the second
334
+ # (and last) throttles.
335
+ api.set_data_fetch(
336
+ DETAIL_CONSUM,
337
+ start=dt.strptime("2023-01-01", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
338
+ end=dt.strptime("2023-01-15", "%Y-%m-%d").replace(tzinfo=LOCAL_TIMEZONE),
339
+ )
340
+ with pytest.raises(ThrottlingError) as exc_info:
341
+ await api.async_update()
342
+
343
+ # The exception itself carries what was already collected...
344
+ partial = exc_info.value.partial_data
345
+ assert partial is not None
346
+ assert len(partial.meter_reading.interval_reading) == 1
347
+
348
+ # ...and it was persisted onto the fetch params, so `api.stats` (read by
349
+ # callers unconditionally, even after this exception) reflects it
350
+ # instead of coming back empty.
351
+ assert api._params[ATTR_CONSUM]["data"]
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.4.8",
3
- "distance": 0,
4
- "node": "g5cd9f3854591c39f30943a62859e23b7590d7a81",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-05"
8
- }