myelectricaldatapy 2.4.7__tar.gz → 2.4.9__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 (52) hide show
  1. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/PKG-INFO +1 -1
  2. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/__init__.py +2 -0
  3. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/api.py +6 -1
  4. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/auth.py +14 -1
  5. myelectricaldatapy-2.4.9/myelectricaldatapy/exceptions.py +46 -0
  6. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy.egg-info/PKG-INFO +1 -1
  7. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy.egg-info/SOURCES.txt +1 -0
  8. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy.egg-info/scm_file_list.json +1 -0
  9. myelectricaldatapy-2.4.9/myelectricaldatapy.egg-info/scm_version.json +8 -0
  10. myelectricaldatapy-2.4.9/tests/test_auth.py +57 -0
  11. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/test_load_data.py +36 -0
  12. myelectricaldatapy-2.4.7/myelectricaldatapy/exceptions.py +0 -25
  13. myelectricaldatapy-2.4.7/myelectricaldatapy.egg-info/scm_version.json +0 -8
  14. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.devcontainer/devcontainer-lock.json +0 -0
  15. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.devcontainer/devcontainer.json +0 -0
  16. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.github/dependabot.yml +0 -0
  17. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.github/workflows/auto-approve.yml +0 -0
  18. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.github/workflows/python-lint.yml +0 -0
  19. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.github/workflows/python-publish.yml +0 -0
  20. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.github/workflows/release.yml +0 -0
  21. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.gitignore +0 -0
  22. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.pre-commit-config.yaml +0 -0
  23. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/.vscode/settings.json +0 -0
  24. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/LICENSE +0 -0
  25. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/README.md +0 -0
  26. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/_secrets.yaml +0 -0
  27. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/example.py +0 -0
  28. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/analytics.py +0 -0
  29. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/const.py +0 -0
  30. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/mypdl.py +0 -0
  31. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/py.typed +0 -0
  32. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/types.py +0 -0
  33. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy/tz.py +0 -0
  34. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
  35. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy.egg-info/requires.txt +0 -0
  36. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/myelectricaldatapy.egg-info/top_level.txt +0 -0
  37. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/pyproject.toml +0 -0
  38. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/setup.cfg +0 -0
  39. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/__init__.py +0 -0
  40. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/conftest.py +0 -0
  41. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/consts.py +0 -0
  42. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/access.json +0 -0
  43. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/address.json +0 -0
  44. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/contract.json +0 -0
  45. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/daily.json +0 -0
  46. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/detail.json +0 -0
  47. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/ecowatt.json +0 -0
  48. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/identity.json +0 -0
  49. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/invalid_access.json +0 -0
  50. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/fixtures/tempo.json +0 -0
  51. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/tests/test_analytics.py +0 -0
  52. {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.9}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myelectricaldatapy
3
- Version: 2.4.7
3
+ Version: 2.4.9
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
@@ -8,6 +8,7 @@ from .exceptions import (
8
8
  HttpRequestError,
9
9
  LimitReached,
10
10
  PayloadError,
11
+ ThrottlingError,
11
12
  TimeoutExceededError,
12
13
  )
13
14
  from .mypdl import EnedisByPDL
@@ -68,6 +69,7 @@ __all__ = [
68
69
  "TempoDays",
69
70
  "TempoLabels",
70
71
  "TempoPrice",
72
+ "ThrottlingError",
71
73
  "TimeoutExceededError",
72
74
  "UsagePoint",
73
75
  "UsagePointAddress",
@@ -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,11 @@ class Enedis:
275
275
  DataCollect,
276
276
  await self.async_fetch_datas(service, pdl, start, end),
277
277
  )
278
+ except LimitReached:
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
+ raise
278
283
  except EnedisException as error:
279
284
  raise_error = True
280
285
  logger.error(error)
@@ -14,6 +14,7 @@ from .exceptions import (
14
14
  HttpRequestError,
15
15
  LimitReached,
16
16
  PayloadError,
17
+ ThrottlingError,
17
18
  TimeoutExceededError,
18
19
  )
19
20
 
@@ -70,8 +71,20 @@ class EnedisAuth:
70
71
  return await response.text()
71
72
 
72
73
  try:
73
- return await response.json()
74
+ result = await response.json()
74
75
  except (ClientError, ValueError) as error:
75
76
  raise PayloadError(
76
77
  "Malformed JSON response from MyElectricalData."
77
78
  ) from error
79
+
80
+ # The gateway signals throttling with an HTTP 200 status and a body
81
+ # carrying the APIM error code 900804, so it never reaches
82
+ # ``raise_for_status``. Surface it as a dedicated exception.
83
+ if isinstance(result, Mapping) and (
84
+ result.get("code") == "900804"
85
+ or result.get("message") == "Message throttled out"
86
+ ):
87
+ detail = result.get("description") or result.get("message") or result
88
+ raise ThrottlingError(detail, next_access_time=result.get("nextAccessTime"))
89
+
90
+ return result
@@ -0,0 +1,46 @@
1
+ """Class exception."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class EnedisException(Exception):
7
+ """Enedis exception."""
8
+
9
+
10
+ class LimitReached(EnedisException):
11
+ """Limit reached exception."""
12
+
13
+
14
+ class ThrottlingError(LimitReached):
15
+ """Raised when MyElectricalData throttles the request.
16
+
17
+ The gateway answers with an HTTP 200 status and a body such as::
18
+
19
+ {"code": "900804", "message": "Message throttled out",
20
+ "description": "You have exceeded your quota ...",
21
+ "nextAccessTime": "2026-Sep-05 16:00:00+0000 UTC"}
22
+
23
+ ``next_access_time`` carries the raw ``nextAccessTime`` string when the
24
+ payload provides it, so callers can back off until the quota resets.
25
+ """
26
+
27
+ def __init__(self, detail: Any, next_access_time: str | None = None) -> None:
28
+ """Init."""
29
+ super().__init__(detail)
30
+ self.next_access_time = next_access_time
31
+
32
+
33
+ class TimeoutExceededError(EnedisException):
34
+ """Timeout exceeded exception."""
35
+
36
+
37
+ class HttpRequestError(EnedisException):
38
+ """Http request error."""
39
+
40
+
41
+ class PayloadError(EnedisException):
42
+ """Raised when an API payload cannot be decoded or does not match the schema."""
43
+
44
+
45
+ class AnalyticsError(EnedisException):
46
+ """Raised when analytics computation fails on the provided dataset."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myelectricaldatapy
3
- Version: 2.4.7
3
+ Version: 2.4.9
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
@@ -35,6 +35,7 @@ tests/__init__.py
35
35
  tests/conftest.py
36
36
  tests/consts.py
37
37
  tests/test_analytics.py
38
+ tests/test_auth.py
38
39
  tests/test_load_data.py
39
40
  tests/fixtures/access.json
40
41
  tests/fixtures/address.json
@@ -38,6 +38,7 @@
38
38
  "tests/fixtures/invalid_access.json",
39
39
  "tests/fixtures/tempo.json",
40
40
  "tests/test_analytics.py",
41
+ "tests/test_auth.py",
41
42
  "tests/test_load_data.py",
42
43
  "uv.lock"
43
44
  ]
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.4.9",
3
+ "distance": 0,
4
+ "node": "g09d1e042b2bb1d14c1f0d68f161e24e163c8052f",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-11"
8
+ }
@@ -0,0 +1,57 @@
1
+ """Tests for the low-level Enedis auth layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from unittest.mock import AsyncMock, MagicMock
7
+
8
+ import pytest
9
+
10
+ from myelectricaldatapy import EnedisException, LimitReached, ThrottlingError
11
+ from myelectricaldatapy.auth import EnedisAuth
12
+
13
+ from .consts import TOKEN
14
+
15
+ THROTTLE_PAYLOAD = {
16
+ "code": "900804",
17
+ "message": "Message throttled out",
18
+ "description": (
19
+ "You have exceeded your quota .You can access API after "
20
+ "2026-Sep-05 16:00:00+0000 UTC"
21
+ ),
22
+ "nextAccessTime": "2026-Sep-05 16:00:00+0000 UTC",
23
+ }
24
+
25
+
26
+ def _mock_session(payload: dict, status: int = 200) -> MagicMock:
27
+ """Return a session whose request yields ``payload`` with the given status."""
28
+ body = json.dumps(payload).encode()
29
+ response = MagicMock()
30
+ response.read = AsyncMock(return_value=body)
31
+ response.raise_for_status = MagicMock()
32
+ response.headers = {"Content-Type": "application/json"}
33
+ response.status = status
34
+ response.json = AsyncMock(return_value=payload)
35
+ session = MagicMock()
36
+ session.request = AsyncMock(return_value=response)
37
+ return session
38
+
39
+
40
+ async def test_throttling_returns_dedicated_exception() -> None:
41
+ """An HTTP 200 throttling body is raised as :class:`ThrottlingError`."""
42
+ auth = EnedisAuth(_mock_session(THROTTLE_PAYLOAD), TOKEN)
43
+
44
+ with pytest.raises(ThrottlingError) as excinfo:
45
+ await auth.async_request("/valid_access/01234567890")
46
+
47
+ error = excinfo.value
48
+ assert error.next_access_time == "2026-Sep-05 16:00:00+0000 UTC"
49
+ assert isinstance(error, LimitReached)
50
+ assert isinstance(error, EnedisException)
51
+
52
+
53
+ async def test_regular_payload_is_returned() -> None:
54
+ """A normal JSON body is passed through untouched."""
55
+ auth = EnedisAuth(_mock_session({"valid": True}), TOKEN)
56
+
57
+ assert await auth.async_request("/valid_access/01234567890") == {"valid": True}
@@ -16,6 +16,7 @@ from myelectricaldatapy import (
16
16
  EnedisException,
17
17
  LimitReached,
18
18
  Subscription,
19
+ ThrottlingError,
19
20
  )
20
21
  from myelectricaldatapy.tz import LOCAL_TIMEZONE
21
22
 
@@ -249,3 +250,38 @@ async def test_exception(
249
250
  pass
250
251
  assert api.last_access is not None
251
252
  assert api.access.valid is True
253
+
254
+
255
+ async def test_details_throttling_propagates(
256
+ mock_access,
257
+ session,
258
+ ) -> None:
259
+ """A ThrottlingError raised while fetching a details chunk must reach the caller.
260
+
261
+ Regression test: ``Enedis._async_get_details`` used to catch every
262
+ ``EnedisException`` per 7-day chunk and only log it, so callers (and
263
+ their ``next_access_time`` back-off logic) never saw the throttling.
264
+ Deliberately bypasses the ``mock_enedis`` fixture, which stubs
265
+ ``async_get_details_consumption`` wholesale and would skip the internal
266
+ per-chunk loop this test targets.
267
+ """
268
+ throttling = ThrottlingError(
269
+ "Throttled", next_access_time="2026-Sep-05 16:00:00+0000 UTC"
270
+ )
271
+ with (
272
+ patch.object(
273
+ myelectricaldatapy.Enedis,
274
+ "async_valid_access",
275
+ return_value=mock_access,
276
+ ),
277
+ patch.object(
278
+ myelectricaldatapy.Enedis,
279
+ "async_fetch_datas",
280
+ side_effect=throttling,
281
+ ),
282
+ ):
283
+ api = EnedisByPDL(pdl=PDL, token=TOKEN, session=session)
284
+ api.set_data_fetch(DETAIL_CONSUM)
285
+ with pytest.raises(ThrottlingError) as exc_info:
286
+ await api.async_update()
287
+ assert exc_info.value.next_access_time == "2026-Sep-05 16:00:00+0000 UTC"
@@ -1,25 +0,0 @@
1
- """Class exception."""
2
-
3
-
4
- class EnedisException(Exception):
5
- """Enedis exception."""
6
-
7
-
8
- class LimitReached(EnedisException):
9
- """Limit reached exception."""
10
-
11
-
12
- class TimeoutExceededError(EnedisException):
13
- """Timeout exceeded exception."""
14
-
15
-
16
- class HttpRequestError(EnedisException):
17
- """Http request error."""
18
-
19
-
20
- class PayloadError(EnedisException):
21
- """Raised when an API payload cannot be decoded or does not match the schema."""
22
-
23
-
24
- class AnalyticsError(EnedisException):
25
- """Raised when analytics computation fails on the provided dataset."""
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.4.7",
3
- "distance": 0,
4
- "node": "g85859d7b8fa97063c80cc6a908044430ab80231c",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-05"
8
- }