myelectricaldatapy 2.4.7__tar.gz → 2.4.8__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.
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/PKG-INFO +1 -1
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/__init__.py +2 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/auth.py +14 -1
- myelectricaldatapy-2.4.8/myelectricaldatapy/exceptions.py +46 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/PKG-INFO +1 -1
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/SOURCES.txt +1 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/scm_file_list.json +1 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/scm_version.json +2 -2
- myelectricaldatapy-2.4.8/tests/test_auth.py +57 -0
- myelectricaldatapy-2.4.7/myelectricaldatapy/exceptions.py +0 -25
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.devcontainer/devcontainer-lock.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.devcontainer/devcontainer.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.github/dependabot.yml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.github/workflows/auto-approve.yml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.github/workflows/python-lint.yml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.github/workflows/python-publish.yml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.github/workflows/release.yml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.gitignore +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.pre-commit-config.yaml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/.vscode/settings.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/LICENSE +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/README.md +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/_secrets.yaml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/example.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/analytics.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/api.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/const.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/mypdl.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/py.typed +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/types.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy/tz.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/requires.txt +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/top_level.txt +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/pyproject.toml +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/setup.cfg +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/__init__.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/conftest.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/consts.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/access.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/address.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/contract.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/daily.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/detail.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/ecowatt.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/identity.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/invalid_access.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/fixtures/tempo.json +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/test_analytics.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/tests/test_load_data.py +0 -0
- {myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/uv.lock +0 -0
|
@@ -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",
|
|
@@ -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
|
-
|
|
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."""
|
|
@@ -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}
|
|
@@ -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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/requires.txt
RENAMED
|
File without changes
|
{myelectricaldatapy-2.4.7 → myelectricaldatapy-2.4.8}/myelectricaldatapy.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|