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