leneda-client 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
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.
- leneda/client.py +82 -6
- leneda/exceptions.py +6 -0
- leneda/models.py +9 -0
- leneda/py.typed +0 -0
- leneda/version.py +1 -1
- {leneda_client-0.5.0.dist-info → leneda_client-0.6.0.dist-info}/METADATA +1 -1
- leneda_client-0.6.0.dist-info/RECORD +12 -0
- {leneda_client-0.5.0.dist-info → leneda_client-0.6.0.dist-info}/WHEEL +1 -1
- leneda_client-0.5.0.dist-info/RECORD +0 -11
- {leneda_client-0.5.0.dist-info → leneda_client-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {leneda_client-0.5.0.dist-info → leneda_client-0.6.0.dist-info}/top_level.txt +0 -0
leneda/client.py
CHANGED
@@ -8,14 +8,15 @@ energy consumption and production data for electricity and gas.
|
|
8
8
|
import json
|
9
9
|
import logging
|
10
10
|
from datetime import datetime, timedelta
|
11
|
-
from typing import Any, Dict, List, Optional, Union
|
11
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
|
12
12
|
|
13
13
|
import aiohttp
|
14
|
-
from aiohttp import ClientTimeout
|
14
|
+
from aiohttp import ClientResponseError, ClientTimeout
|
15
15
|
|
16
|
-
from .exceptions import ForbiddenException, UnauthorizedException
|
16
|
+
from .exceptions import ForbiddenException, MeteringPointNotFoundException, UnauthorizedException
|
17
17
|
from .models import (
|
18
18
|
AggregatedMeteringData,
|
19
|
+
AuthenticationProbeResult,
|
19
20
|
MeteringData,
|
20
21
|
)
|
21
22
|
from .obis_codes import ObisCode
|
@@ -115,7 +116,7 @@ class LenedaClient:
|
|
115
116
|
|
116
117
|
# Parse the response
|
117
118
|
if response.content:
|
118
|
-
response_data = await response.json()
|
119
|
+
response_data: dict = await response.json()
|
119
120
|
logger.debug(f"Response status: {response.status}")
|
120
121
|
logger.debug(f"Response data: {json.dumps(response_data, indent=2)}")
|
121
122
|
return response_data
|
@@ -133,6 +134,39 @@ class LenedaClient:
|
|
133
134
|
logger.error(f"JSON decode error: {e}")
|
134
135
|
raise
|
135
136
|
|
137
|
+
async def _make_metering_request(
|
138
|
+
self, request_callable: Callable[..., Awaitable[dict[Any, Any]]], *args: Any, **kwargs: Any
|
139
|
+
) -> dict[Any, Any]:
|
140
|
+
"""
|
141
|
+
Make a request to a metering data endpoint with 404 error handling.
|
142
|
+
|
143
|
+
This wrapper around any request callable specifically handles 404 errors for metering data
|
144
|
+
endpoints by raising MeteringPointNotFoundException.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
request_callable: The callable to execute (e.g., self._make_request)
|
148
|
+
*args: Arguments to pass to the callable
|
149
|
+
**kwargs: Keyword arguments to pass to the callable
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
The JSON response from the API
|
153
|
+
|
154
|
+
Raises:
|
155
|
+
MeteringPointNotFoundException: If the API returns a 404 status code
|
156
|
+
UnauthorizedException: If the API returns a 401 status code
|
157
|
+
ForbiddenException: If the API returns a 403 status code
|
158
|
+
aiohttp.ClientError: For other request errors
|
159
|
+
json.JSONDecodeError: If the response cannot be parsed as JSON
|
160
|
+
"""
|
161
|
+
try:
|
162
|
+
return await request_callable(*args, **kwargs)
|
163
|
+
except aiohttp.ClientResponseError as e:
|
164
|
+
if e.status == 404:
|
165
|
+
raise MeteringPointNotFoundException(
|
166
|
+
"Metering point not found. The requested metering point may not exist or you may not have access to it."
|
167
|
+
)
|
168
|
+
raise
|
169
|
+
|
136
170
|
async def get_metering_data(
|
137
171
|
self,
|
138
172
|
metering_point_code: str,
|
@@ -167,7 +201,9 @@ class LenedaClient:
|
|
167
201
|
}
|
168
202
|
|
169
203
|
# Make the request
|
170
|
-
response_data = await self.
|
204
|
+
response_data = await self._make_metering_request(
|
205
|
+
self._make_request, method="GET", endpoint=endpoint, params=params
|
206
|
+
)
|
171
207
|
|
172
208
|
# Parse the response into a MeteringData object
|
173
209
|
return MeteringData.from_dict(response_data)
|
@@ -212,7 +248,9 @@ class LenedaClient:
|
|
212
248
|
}
|
213
249
|
|
214
250
|
# Make the request
|
215
|
-
response_data = await self.
|
251
|
+
response_data = await self._make_metering_request(
|
252
|
+
self._make_request, method="GET", endpoint=endpoint, params=params
|
253
|
+
)
|
216
254
|
|
217
255
|
# Parse the response into an AggregatedMeteringData object
|
218
256
|
return AggregatedMeteringData.from_dict(response_data)
|
@@ -320,3 +358,41 @@ class LenedaClient:
|
|
320
358
|
if await self.probe_metering_point_obis_code(metering_point_code, obis_code):
|
321
359
|
supported_codes.append(obis_code)
|
322
360
|
return supported_codes
|
361
|
+
|
362
|
+
async def probe_credentials(self) -> AuthenticationProbeResult:
|
363
|
+
"""
|
364
|
+
Probe if credentials are valid.
|
365
|
+
|
366
|
+
NOTE: This is an experimental function, as the Leneda API does not provide a native way to verify credentials only.
|
367
|
+
Use with caution, may break or yield unexpected results.
|
368
|
+
|
369
|
+
This method attempts to verify authentication by making a request to the metering data access endpoint
|
370
|
+
with invalid parameters. If the API returns a 400 status code, it indicates that authentication is successful
|
371
|
+
but the request parameters are invalid. If it returns a 401 status code, authentication has failed.
|
372
|
+
|
373
|
+
We make a request with invalid parameters because we don't want to actually lodge a metering data access request,
|
374
|
+
we just want to verify that the credentials are valid.
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
AuthenticationProbeResult: SUCCESS if authentication is valid, FAILURE if authentication failed,
|
378
|
+
or UNKNOWN if the result cannot be determined
|
379
|
+
|
380
|
+
Raises:
|
381
|
+
ForbiddenException: If the API returns a 403 status code
|
382
|
+
"""
|
383
|
+
try:
|
384
|
+
await self.request_metering_data_access("", "", [], [])
|
385
|
+
except UnauthorizedException:
|
386
|
+
return AuthenticationProbeResult.FAILURE
|
387
|
+
except ClientResponseError as e:
|
388
|
+
# We expect a 400 response if authentication is successful and our request is invalid
|
389
|
+
if e.status == 400:
|
390
|
+
# Update the config entry with new token
|
391
|
+
return AuthenticationProbeResult.SUCCESS
|
392
|
+
return AuthenticationProbeResult.UNKNOWN
|
393
|
+
except ForbiddenException:
|
394
|
+
raise
|
395
|
+
except Exception:
|
396
|
+
return AuthenticationProbeResult.UNKNOWN
|
397
|
+
|
398
|
+
return AuthenticationProbeResult.UNKNOWN
|
leneda/exceptions.py
CHANGED
@@ -19,3 +19,9 @@ class ForbiddenException(LenedaException):
|
|
19
19
|
"""Raised when access is forbidden (403 Forbidden), typically due to geoblocking or other access restrictions."""
|
20
20
|
|
21
21
|
pass
|
22
|
+
|
23
|
+
|
24
|
+
class MeteringPointNotFoundException(LenedaException):
|
25
|
+
"""Raised when a metering point is not found."""
|
26
|
+
|
27
|
+
pass
|
leneda/models.py
CHANGED
@@ -8,6 +8,7 @@ making it easier to work with the data in a type-safe manner.
|
|
8
8
|
import logging
|
9
9
|
from dataclasses import dataclass, field
|
10
10
|
from datetime import datetime
|
11
|
+
from enum import Enum
|
11
12
|
from typing import Any, Dict, List
|
12
13
|
|
13
14
|
from dateutil import parser
|
@@ -237,3 +238,11 @@ class AggregatedMeteringData:
|
|
237
238
|
f"AggregatedMeteringData(unit={self.unit}, "
|
238
239
|
f"items_count={len(self.aggregated_time_series)})"
|
239
240
|
)
|
241
|
+
|
242
|
+
|
243
|
+
class AuthenticationProbeResult(Enum):
|
244
|
+
"""Result of an authentication probe."""
|
245
|
+
|
246
|
+
SUCCESS = "SUCCESS"
|
247
|
+
FAILURE = "FAILURE"
|
248
|
+
UNKNOWN = "UNKNOWN"
|
leneda/py.typed
ADDED
File without changes
|
leneda/version.py
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
leneda/__init__.py,sha256=-BHIXBfTTbX6EKDHr2Bq7rPzsPNYrrkgs9AJqWJR_1Q,732
|
2
|
+
leneda/client.py,sha256=v41E1jFqe5MLMAoSOCtSYm8ZYWJwXCo-rBmKwMpDjB4,15875
|
3
|
+
leneda/exceptions.py,sha256=eyQpEYdLAU_BkXbZeldS6XgXeQJEIKp4dBeyzwlKOfA,580
|
4
|
+
leneda/models.py,sha256=Dwn99_h9D7bKnBZY_3WJ5bwA6XyBliEYK6A9kBhKtmk,8111
|
5
|
+
leneda/obis_codes.py,sha256=VfsJQN1U80eZ5g1bIteDCLkkmBQ0AIkkm_zNAeM1Dog,7507
|
6
|
+
leneda/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
leneda/version.py,sha256=5C-KzeFwke_CL13dukzT7o-_j-Sv7xz_3L5EqVn3hI0,50
|
8
|
+
leneda_client-0.6.0.dist-info/licenses/LICENSE,sha256=nAhDs625lK6v8oLqjWCABKBKlwxVoRDFXQvoZPfOtKQ,1062
|
9
|
+
leneda_client-0.6.0.dist-info/METADATA,sha256=kVCSco-qeUMd6V0mr6RF02C4Ivb87VAW8hFNsSKfSyQ,3515
|
10
|
+
leneda_client-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
leneda_client-0.6.0.dist-info/top_level.txt,sha256=PANScm25ep7WLjKiph0fhJPb8s_sa_uLHemnBpQBaJ8,7
|
12
|
+
leneda_client-0.6.0.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
leneda/__init__.py,sha256=-BHIXBfTTbX6EKDHr2Bq7rPzsPNYrrkgs9AJqWJR_1Q,732
|
2
|
-
leneda/client.py,sha256=aATrm5lI6O97cvVpwdtXGQA5kF3P6I4PZIE6G7y50AQ,12413
|
3
|
-
leneda/exceptions.py,sha256=q00gjI5VwXAMF2I1gXfQidZMzbCF6UOSo4i1Wnb-inU,460
|
4
|
-
leneda/models.py,sha256=jdU2cIZZDExUSiSfz9zaYjJepr0m3v_x5b1fyOaEI8Q,7930
|
5
|
-
leneda/obis_codes.py,sha256=VfsJQN1U80eZ5g1bIteDCLkkmBQ0AIkkm_zNAeM1Dog,7507
|
6
|
-
leneda/version.py,sha256=ScwrsoTG4Jp3gCphFH5AJNnhCl94poCw0wfA4QqStu0,50
|
7
|
-
leneda_client-0.5.0.dist-info/licenses/LICENSE,sha256=nAhDs625lK6v8oLqjWCABKBKlwxVoRDFXQvoZPfOtKQ,1062
|
8
|
-
leneda_client-0.5.0.dist-info/METADATA,sha256=lDNsYdQxINFuJtLRcqG2rGW1zRIc9z-34_Kzx3BnSp4,3515
|
9
|
-
leneda_client-0.5.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
10
|
-
leneda_client-0.5.0.dist-info/top_level.txt,sha256=PANScm25ep7WLjKiph0fhJPb8s_sa_uLHemnBpQBaJ8,7
|
11
|
-
leneda_client-0.5.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|