myelectricaldatapy 2.2.8__tar.gz → 2.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.
Files changed (52) hide show
  1. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/release.yml +0 -8
  2. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/PKG-INFO +25 -2
  3. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/README.md +24 -0
  4. myelectricaldatapy-2.3.0/myelectricaldatapy/__init__.py +48 -0
  5. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/analytics.py +7 -5
  6. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/api.py +14 -16
  7. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/auth.py +5 -5
  8. myelectricaldatapy-2.3.0/myelectricaldatapy/const.py +25 -0
  9. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/mypdl.py +165 -163
  10. myelectricaldatapy-2.3.0/myelectricaldatapy/types.py +71 -0
  11. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/tz.py +23 -2
  12. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/PKG-INFO +25 -2
  13. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/SOURCES.txt +2 -0
  14. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/requires.txt +0 -1
  15. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/scm_file_list.json +3 -1
  16. myelectricaldatapy-2.3.0/myelectricaldatapy.egg-info/scm_version.json +8 -0
  17. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/pyproject.toml +0 -1
  18. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/conftest.py +2 -28
  19. myelectricaldatapy-2.3.0/tests/test_analytics.py +346 -0
  20. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/test_load_data.py +21 -14
  21. myelectricaldatapy-2.3.0/uv.lock +1008 -0
  22. myelectricaldatapy-2.2.8/myelectricaldatapy/__init__.py +0 -19
  23. myelectricaldatapy-2.2.8/myelectricaldatapy/const.py +0 -21
  24. myelectricaldatapy-2.2.8/myelectricaldatapy.egg-info/scm_version.json +0 -8
  25. myelectricaldatapy-2.2.8/tests/test_analytics.py +0 -361
  26. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.devcontainer/devcontainer-lock.json +0 -0
  27. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.devcontainer/devcontainer.json +0 -0
  28. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/dependabot.yml +0 -0
  29. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/auto-approve.yml +0 -0
  30. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/python-lint.yml +0 -0
  31. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.github/workflows/python-publish.yml +0 -0
  32. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.gitignore +0 -0
  33. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.pre-commit-config.yaml +0 -0
  34. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/.vscode/settings.json +0 -0
  35. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/LICENSE +0 -0
  36. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/_secrets.yaml +0 -0
  37. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/example.py +0 -0
  38. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/exceptions.py +0 -0
  39. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy/py.typed +0 -0
  40. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/dependency_links.txt +0 -0
  41. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/myelectricaldatapy.egg-info/top_level.txt +0 -0
  42. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/setup.cfg +0 -0
  43. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/__init__.py +0 -0
  44. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/consts.py +0 -0
  45. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/access.json +0 -0
  46. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/address.json +0 -0
  47. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/contract.json +0 -0
  48. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/daily.json +0 -0
  49. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/detail.json +0 -0
  50. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/ecowatt.json +0 -0
  51. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/invalid_access.json +0 -0
  52. {myelectricaldatapy-2.2.8 → myelectricaldatapy-2.3.0}/tests/fixtures/tempo.json +0 -0
@@ -25,11 +25,3 @@ jobs:
25
25
  draft: false
26
26
  prerelease: ${{ contains(github.ref_name, '-') }}
27
27
  token: ${{ secrets.GITHUB_TOKEN }}
28
- publish:
29
- needs: release
30
- permissions:
31
- contents: write
32
- packages: write
33
- id-token: write
34
- if: ${{ ! contains(github.ref_name, '-') }}
35
- uses: ./.github/workflows/python-publish.yml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myelectricaldatapy
3
- Version: 2.2.8
3
+ Version: 2.3.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
@@ -21,7 +21,6 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: aiohttp>=3.8.1
23
23
  Requires-Dist: pandas>=2.0.2
24
- Requires-Dist: voluptuous>=0.13.1
25
24
  Provides-Extra: dev
26
25
  Requires-Dist: prek; extra == "dev"
27
26
  Requires-Dist: ruff; extra == "dev"
@@ -76,6 +75,30 @@ $ python setup.py install
76
75
  - async_load (start: datetime, end: datetime) Return None - Load Data in power_Data attribute
77
76
  - async_refresh Return None - Refresh power_Data , tempo_day and ecowatt attributes.
78
77
 
78
+ ## Timezone
79
+
80
+ Enedis/RTE APIs return naive timestamps that represent local wall-clock time. This
81
+ library needs to know which timezone that is in order to compute things like
82
+ off-peak hours or hourly statistics correctly.
83
+
84
+ By default it falls back to the timezone of the host machine running the code
85
+ (`/etc/localtime`), but that has no reason to match the timezone your
86
+ application is actually configured for (e.g. Home Assistant's
87
+ `hass.config.time_zone`). Pass it explicitly to avoid a fixed-offset shift in
88
+ the reported times:
89
+
90
+ ```python
91
+ api = EnedisByPDL(token=TOKEN, pdl=PDL, timezone=ZoneInfo("Europe/Paris"))
92
+ ```
93
+
94
+ Alternatively, set it once globally for every instance:
95
+
96
+ ```python
97
+ from myelectricaldatapy import set_local_timezone
98
+
99
+ set_local_timezone(ZoneInfo("Europe/Paris"))
100
+ ```
101
+
79
102
  ## Get started
80
103
 
81
104
  ```python
@@ -41,6 +41,30 @@ $ python setup.py install
41
41
  - async_load (start: datetime, end: datetime) Return None - Load Data in power_Data attribute
42
42
  - async_refresh Return None - Refresh power_Data , tempo_day and ecowatt attributes.
43
43
 
44
+ ## Timezone
45
+
46
+ Enedis/RTE APIs return naive timestamps that represent local wall-clock time. This
47
+ library needs to know which timezone that is in order to compute things like
48
+ off-peak hours or hourly statistics correctly.
49
+
50
+ By default it falls back to the timezone of the host machine running the code
51
+ (`/etc/localtime`), but that has no reason to match the timezone your
52
+ application is actually configured for (e.g. Home Assistant's
53
+ `hass.config.time_zone`). Pass it explicitly to avoid a fixed-offset shift in
54
+ the reported times:
55
+
56
+ ```python
57
+ api = EnedisByPDL(token=TOKEN, pdl=PDL, timezone=ZoneInfo("Europe/Paris"))
58
+ ```
59
+
60
+ Alternatively, set it once globally for every instance:
61
+
62
+ ```python
63
+ from myelectricaldatapy import set_local_timezone
64
+
65
+ set_local_timezone(ZoneInfo("Europe/Paris"))
66
+ ```
67
+
44
68
  ## Get started
45
69
 
46
70
  ```python
@@ -0,0 +1,48 @@
1
+ """myelectricaldatapy package."""
2
+
3
+ from .api import Enedis
4
+ from .const import *
5
+ from .exceptions import (
6
+ EnedisException,
7
+ HttpRequestError,
8
+ LimitReached,
9
+ TimeoutExceededError,
10
+ )
11
+ from .mypdl import EnedisByPDL, FormatError, validate_cumsum, validate_prices
12
+ from .types import (
13
+ ConsumptionCollect,
14
+ Cum,
15
+ EnergyCollect,
16
+ Mode,
17
+ Prices,
18
+ ProductionCollect,
19
+ Service,
20
+ StandardPrice,
21
+ Subscription,
22
+ TempoPrice,
23
+ )
24
+ from .tz import get_local_timezone, set_local_timezone
25
+
26
+ __all__ = [
27
+ "ConsumptionCollect",
28
+ "Cum",
29
+ "Enedis",
30
+ "EnedisByPDL",
31
+ "EnedisException",
32
+ "EnergyCollect",
33
+ "FormatError",
34
+ "HttpRequestError",
35
+ "LimitReached",
36
+ "Mode",
37
+ "Prices",
38
+ "ProductionCollect",
39
+ "Service",
40
+ "StandardPrice",
41
+ "Subscription",
42
+ "TempoPrice",
43
+ "TimeoutExceededError",
44
+ "get_local_timezone",
45
+ "set_local_timezone",
46
+ "validate_cumsum",
47
+ "validate_prices",
48
+ ]
@@ -1,23 +1,25 @@
1
1
  """Class for analytics."""
2
2
 
3
- from datetime import datetime as dt, timedelta
3
+ from datetime import datetime as dt, timedelta, tzinfo as _tzinfo
4
4
  import re
5
5
  from typing import Any
6
6
 
7
7
  import pandas as pd
8
8
 
9
9
  from .const import ATTR_OFFPEAK, ATTR_STANDARD, TEMPO_DAYS
10
- from .tz import LOCAL_TIMEZONE
10
+ from .tz import get_local_timezone
11
11
 
12
12
 
13
13
  class EnedisAnalytics:
14
14
  """Data analytics."""
15
15
 
16
- local_timezone = LOCAL_TIMEZONE
17
-
18
- def __init__(self, data: Any) -> None:
16
+ def __init__(self, data: Any, timezone: _tzinfo | None = None) -> None:
19
17
  """Initialize Dataframe."""
20
18
  self.df = pd.DataFrame(data)
19
+ # Resolved per instance (not at import/class-definition time) so a
20
+ # timezone set later via tz.set_local_timezone(), or passed here
21
+ # explicitly, is honored.
22
+ self.local_timezone = timezone or get_local_timezone()
21
23
 
22
24
  def get_data_analytics(
23
25
  self,
@@ -9,9 +9,10 @@ from typing import Any
9
9
  from aiohttp import ClientSession
10
10
 
11
11
  from .auth import EnedisAuth
12
- from .const import DAILY_CONSUM, DAILY_PROD, DETAIL_CONSUM, DETAIL_PROD, TIMEOUT
12
+ from .const import DAILY_CONSUM, DAILY_PROD, DETAIL_CONSUM, DETAIL_PROD
13
13
  from .exceptions import EnedisException
14
- from .tz import LOCAL_TIMEZONE, as_local, local_now
14
+ from .types import Service
15
+ from .tz import as_local, get_local_timezone, local_now
15
16
 
16
17
  _LOGGER = logging.getLogger(__name__)
17
18
 
@@ -20,7 +21,7 @@ class Enedis:
20
21
  """Enedis API."""
21
22
 
22
23
  def __init__(
23
- self, token: str, session: ClientSession | None = None, timeout: int = TIMEOUT
24
+ self, token: str, session: ClientSession | None = None, timeout: int = 30
24
25
  ) -> None:
25
26
  """Initialize."""
26
27
  session = session or ClientSession()
@@ -30,15 +31,9 @@ class Enedis:
30
31
  self.last_access: date | None = None
31
32
 
32
33
  async def async_fetch_datas(
33
- self, service: str, pdl: str, start: dt | None = None, end: dt | None = None
34
+ self, service: Service, pdl: str, start: dt | None = None, end: dt | None = None
34
35
  ) -> Any:
35
- """Retrieve date from service.
36
-
37
- service: contracts, identity, contact, addresses,
38
- daily_consumption_max_power,
39
- daily_consumption, daily_production,
40
- consumption_load_curve, production_load_curve
41
- """
36
+ """Retrieve date from service."""
42
37
  self.last_access = local_now()
43
38
  path_range = ""
44
39
  if start and end:
@@ -126,16 +121,17 @@ class Enedis:
126
121
  if await self.async_has_offpeak(pdl) is True:
127
122
  # Off-peak windows are defined in local wall-clock time, so a
128
123
  # start given in another timezone must be converted first.
129
- start_time = as_local(start).astimezone(LOCAL_TIMEZONE).time()
124
+ local_timezone = get_local_timezone()
125
+ start_time = as_local(start).astimezone(local_timezone).time()
130
126
  for range_time in self.offpeaks:
131
127
  starting = (
132
128
  dt.strptime(range_time[0], "%HH%M")
133
- .replace(tzinfo=LOCAL_TIMEZONE)
129
+ .replace(tzinfo=local_timezone)
134
130
  .time()
135
131
  )
136
132
  ending = (
137
133
  dt.strptime(range_time[1], "%HH%M")
138
- .replace(tzinfo=LOCAL_TIMEZONE)
134
+ .replace(tzinfo=local_timezone)
139
135
  .time()
140
136
  )
141
137
  if starting < start_time <= ending:
@@ -168,7 +164,9 @@ class Enedis:
168
164
  "daily_consumption_max_power", pdl, start, end
169
165
  )
170
166
 
171
- async def _async_get_details(self, mode: str, pdl: str, start: dt, end: dt) -> Any:
167
+ async def _async_get_details(
168
+ self, service: Service, pdl: str, start: dt, end: dt
169
+ ) -> Any:
172
170
  """Get production details. (max: 7 days)."""
173
171
  data = None
174
172
  response: dict[str, Any] | None = None
@@ -177,7 +175,7 @@ class Enedis:
177
175
  start, end = interval
178
176
  try:
179
177
  if raise_error is False:
180
- response = await self.async_fetch_datas(mode, pdl, start, end)
178
+ response = await self.async_fetch_datas(service, pdl, start, end)
181
179
  except EnedisException as error:
182
180
  raise_error = True
183
181
  response = None
@@ -1,6 +1,7 @@
1
1
  """Class for Enedis Authentication (http://www.myelectricaldata.fr)."""
2
2
 
3
3
  import asyncio
4
+ from collections.abc import Mapping
4
5
  import json
5
6
  import logging
6
7
  import socket
@@ -8,7 +9,6 @@ from typing import Any
8
9
 
9
10
  from aiohttp import ClientError, ClientResponseError, ClientSession
10
11
 
11
- from .const import TIMEOUT, URL
12
12
  from .exceptions import (
13
13
  EnedisException,
14
14
  HttpRequestError,
@@ -17,14 +17,13 @@ from .exceptions import (
17
17
  )
18
18
 
19
19
  _LOGGER = logging.getLogger(__name__)
20
+ URL = "https://myelectricaldata.fr"
20
21
 
21
22
 
22
23
  class EnedisAuth:
23
24
  """Class for Enedis Auth API."""
24
25
 
25
- def __init__(
26
- self, session: ClientSession, token: str, timeout: int = TIMEOUT
27
- ) -> None:
26
+ def __init__(self, session: ClientSession, token: str, timeout: int = 30) -> None:
28
27
  """Init."""
29
28
  self.token = token
30
29
  self.timeout = timeout
@@ -50,7 +49,8 @@ class EnedisAuth:
50
49
  ) from error
51
50
  except ClientResponseError as error:
52
51
  message = contents.decode("utf8")
53
- if "application/json" in (error.headers or {}).get("Content-Type", ""):
52
+ headers: Mapping[str, str] = error.headers or {}
53
+ if "application/json" in headers.get("Content-Type", ""):
54
54
  msg = json.loads(message)
55
55
  if error.status == 409:
56
56
  raise LimitReached(msg.get("detail", msg))
@@ -0,0 +1,25 @@
1
+ """Constants."""
2
+
3
+ from typing import Final
4
+
5
+ ATTR_CUM_PRICE = "cum_price"
6
+ ATTR_CUM_VALUE = "cum_value"
7
+ ATTR_END = "end"
8
+ ATTR_FN = "function"
9
+ ATTR_HPHC = "hphc"
10
+ ATTR_INTERVALS = "intervals"
11
+ ATTR_OFFPEAK = "offpeak"
12
+ ATTR_PRICE: Final = "price"
13
+ ATTR_PRICES = "prices"
14
+ ATTR_STANDARD = "standard"
15
+ ATTR_START = "start"
16
+ ATTR_TEMPO = "tempo"
17
+ TEMPO_DAYS = ("blue", "white", "red")
18
+ SUBSCRIPTIONS = (ATTR_STANDARD, ATTR_HPHC, ATTR_TEMPO)
19
+ DEFAULT_SUBSCRIPTION = ATTR_STANDARD
20
+ CONSUMPTION: Final = "consumption"
21
+ DAILY_CONSUM: Final = "daily_consumption"
22
+ DAILY_PROD: Final = "daily_production"
23
+ DETAIL_CONSUM: Final = "consumption_load_curve"
24
+ DETAIL_PROD: Final = "production_load_curve"
25
+ PRODUCTION: Final = "production"