leneda-client 0.3.0__py3-none-any.whl → 0.5.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 +94 -64
- leneda/exceptions.py +0 -6
- leneda/version.py +1 -1
- {leneda_client-0.3.0.dist-info → leneda_client-0.5.0.dist-info}/METADATA +6 -2
- leneda_client-0.5.0.dist-info/RECORD +11 -0
- {leneda_client-0.3.0.dist-info → leneda_client-0.5.0.dist-info}/WHEEL +1 -1
- leneda_client-0.3.0.dist-info/RECORD +0 -11
- {leneda_client-0.3.0.dist-info → leneda_client-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {leneda_client-0.3.0.dist-info → leneda_client-0.5.0.dist-info}/top_level.txt +0 -0
leneda/client.py
CHANGED
@@ -10,9 +10,10 @@ import logging
|
|
10
10
|
from datetime import datetime, timedelta
|
11
11
|
from typing import Any, Dict, List, Optional, Union
|
12
12
|
|
13
|
-
import
|
13
|
+
import aiohttp
|
14
|
+
from aiohttp import ClientTimeout
|
14
15
|
|
15
|
-
from .exceptions import ForbiddenException,
|
16
|
+
from .exceptions import ForbiddenException, UnauthorizedException
|
16
17
|
from .models import (
|
17
18
|
AggregatedMeteringData,
|
18
19
|
MeteringData,
|
@@ -27,8 +28,15 @@ class LenedaClient:
|
|
27
28
|
"""Client for the Leneda API."""
|
28
29
|
|
29
30
|
BASE_URL = "https://api.leneda.lu/api"
|
31
|
+
DEFAULT_TIMEOUT = ClientTimeout(total=30) # 30 seconds total timeout
|
30
32
|
|
31
|
-
def __init__(
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
api_key: str,
|
36
|
+
energy_id: str,
|
37
|
+
debug: bool = False,
|
38
|
+
timeout: Optional[ClientTimeout] = None,
|
39
|
+
):
|
32
40
|
"""
|
33
41
|
Initialize the Leneda API client.
|
34
42
|
|
@@ -36,9 +44,11 @@ class LenedaClient:
|
|
36
44
|
api_key: Your Leneda API key
|
37
45
|
energy_id: Your Energy ID
|
38
46
|
debug: Enable debug logging
|
47
|
+
timeout: Optional timeout settings for requests
|
39
48
|
"""
|
40
49
|
self.api_key = api_key
|
41
50
|
self.energy_id = energy_id
|
51
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
42
52
|
|
43
53
|
# Set up headers for API requests
|
44
54
|
self.headers = {
|
@@ -53,7 +63,7 @@ class LenedaClient:
|
|
53
63
|
logger.setLevel(logging.DEBUG)
|
54
64
|
logger.debug("Debug logging enabled for Leneda client")
|
55
65
|
|
56
|
-
def _make_request(
|
66
|
+
async def _make_request(
|
57
67
|
self,
|
58
68
|
method: str,
|
59
69
|
endpoint: str,
|
@@ -75,7 +85,7 @@ class LenedaClient:
|
|
75
85
|
Raises:
|
76
86
|
UnauthorizedException: If the API returns a 401 status code
|
77
87
|
ForbiddenException: If the API returns a 403 status code
|
78
|
-
|
88
|
+
aiohttp.ClientError: For other request errors
|
79
89
|
json.JSONDecodeError: If the response cannot be parsed as JSON
|
80
90
|
"""
|
81
91
|
url = f"{self.BASE_URL}/{endpoint}"
|
@@ -88,52 +98,42 @@ class LenedaClient:
|
|
88
98
|
logger.debug(f"Request data: {json.dumps(json_data, indent=2)}")
|
89
99
|
|
90
100
|
try:
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
except requests.exceptions.HTTPError as e:
|
101
|
+
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
102
|
+
async with session.request(
|
103
|
+
method=method, url=url, headers=self.headers, params=params, json=json_data
|
104
|
+
) as response:
|
105
|
+
# Check for HTTP errors
|
106
|
+
if response.status == 401:
|
107
|
+
raise UnauthorizedException(
|
108
|
+
"API authentication failed. Please check your API key and energy ID."
|
109
|
+
)
|
110
|
+
if response.status == 403:
|
111
|
+
raise ForbiddenException(
|
112
|
+
"Access forbidden. This may be due to Leneda's geoblocking or other access restrictions."
|
113
|
+
)
|
114
|
+
response.raise_for_status()
|
115
|
+
|
116
|
+
# Parse the response
|
117
|
+
if response.content:
|
118
|
+
response_data = await response.json()
|
119
|
+
logger.debug(f"Response status: {response.status}")
|
120
|
+
logger.debug(f"Response data: {json.dumps(response_data, indent=2)}")
|
121
|
+
return response_data
|
122
|
+
else:
|
123
|
+
logger.debug(f"Response status: {response.status} (no content)")
|
124
|
+
return {}
|
125
|
+
|
126
|
+
except aiohttp.ClientError as e:
|
118
127
|
# Handle HTTP errors
|
119
128
|
logger.error(f"HTTP error: {e}")
|
120
|
-
if hasattr(e, "response") and e.response is not None:
|
121
|
-
logger.error(f"Response status: {e.response.status_code}")
|
122
|
-
logger.error(f"Response body: {e.response.text}")
|
123
|
-
raise
|
124
|
-
|
125
|
-
except requests.exceptions.RequestException as e:
|
126
|
-
# Handle other request errors
|
127
|
-
logger.error(f"Request error: {e}")
|
128
129
|
raise
|
129
130
|
|
130
131
|
except json.JSONDecodeError as e:
|
131
132
|
# Handle JSON parsing errors
|
132
133
|
logger.error(f"JSON decode error: {e}")
|
133
|
-
logger.error(f"Response text: {response.text}")
|
134
134
|
raise
|
135
135
|
|
136
|
-
def get_metering_data(
|
136
|
+
async def get_metering_data(
|
137
137
|
self,
|
138
138
|
metering_point_code: str,
|
139
139
|
obis_code: ObisCode,
|
@@ -167,12 +167,12 @@ class LenedaClient:
|
|
167
167
|
}
|
168
168
|
|
169
169
|
# Make the request
|
170
|
-
response_data = self._make_request(method="GET", endpoint=endpoint, params=params)
|
170
|
+
response_data = await self._make_request(method="GET", endpoint=endpoint, params=params)
|
171
171
|
|
172
172
|
# Parse the response into a MeteringData object
|
173
173
|
return MeteringData.from_dict(response_data)
|
174
174
|
|
175
|
-
def get_aggregated_metering_data(
|
175
|
+
async def get_aggregated_metering_data(
|
176
176
|
self,
|
177
177
|
metering_point_code: str,
|
178
178
|
obis_code: ObisCode,
|
@@ -212,12 +212,12 @@ class LenedaClient:
|
|
212
212
|
}
|
213
213
|
|
214
214
|
# Make the request
|
215
|
-
response_data = self._make_request(method="GET", endpoint=endpoint, params=params)
|
215
|
+
response_data = await self._make_request(method="GET", endpoint=endpoint, params=params)
|
216
216
|
|
217
217
|
# Parse the response into an AggregatedMeteringData object
|
218
218
|
return AggregatedMeteringData.from_dict(response_data)
|
219
219
|
|
220
|
-
def request_metering_data_access(
|
220
|
+
async def request_metering_data_access(
|
221
221
|
self,
|
222
222
|
from_energy_id: str,
|
223
223
|
from_name: str,
|
@@ -246,47 +246,77 @@ class LenedaClient:
|
|
246
246
|
}
|
247
247
|
|
248
248
|
# Make the request
|
249
|
-
response_data = self._make_request(method="POST", endpoint=endpoint, json_data=data)
|
249
|
+
response_data = await self._make_request(method="POST", endpoint=endpoint, json_data=data)
|
250
250
|
|
251
251
|
return response_data
|
252
252
|
|
253
|
-
def
|
253
|
+
async def probe_metering_point_obis_code(
|
254
|
+
self, metering_point_code: str, obis_code: ObisCode
|
255
|
+
) -> bool:
|
254
256
|
"""
|
255
|
-
|
257
|
+
Probe if a metering point provides data for a specific OBIS code.
|
258
|
+
|
259
|
+
NOTE: This method is essentially a best guess since the Leneda API does not provide a way to check
|
260
|
+
if a metering point provides data for a specific OBIS code or whether a metering point code is valid
|
256
261
|
|
257
|
-
This method checks if a metering point
|
258
|
-
for aggregated metering data. If the unit property in the response is null,
|
259
|
-
|
262
|
+
This method checks if a metering point provides data for the specified OBIS code by making a request
|
263
|
+
for aggregated metering data. If the unit property in the response is null, it indicates that either:
|
264
|
+
- The metering point is invalid, or
|
265
|
+
- The metering point does not provide data for the specified OBIS code
|
260
266
|
|
261
267
|
Args:
|
262
|
-
metering_point_code: The metering point code to
|
268
|
+
metering_point_code: The metering point code to probe
|
269
|
+
obis_code: The OBIS code to check for data availability
|
263
270
|
|
264
271
|
Returns:
|
265
|
-
bool: True if the metering point
|
272
|
+
bool: True if the metering point provides data for the specified OBIS code, False otherwise
|
266
273
|
|
267
274
|
Raises:
|
268
275
|
UnauthorizedException: If the API returns a 401 status code
|
269
276
|
ForbiddenException: If the API returns a 403 status code
|
270
|
-
|
277
|
+
aiohttp.ClientError: For other request errors
|
271
278
|
"""
|
272
279
|
# Use arbitrary time window
|
273
280
|
end_date = datetime.now()
|
274
281
|
start_date = end_date - timedelta(weeks=4)
|
275
282
|
|
276
|
-
# Try to get aggregated data for
|
277
|
-
result = self.get_aggregated_metering_data(
|
283
|
+
# Try to get aggregated data for the specified OBIS code
|
284
|
+
result = await self.get_aggregated_metering_data(
|
278
285
|
metering_point_code=metering_point_code,
|
279
|
-
obis_code=
|
286
|
+
obis_code=obis_code,
|
280
287
|
start_date=start_date,
|
281
288
|
end_date=end_date,
|
282
289
|
aggregation_level="Month",
|
283
290
|
transformation_mode="Accumulation",
|
284
291
|
)
|
285
292
|
|
286
|
-
#
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
293
|
+
# Return True if we got data (unit is not None), False otherwise
|
294
|
+
return result.unit is not None
|
295
|
+
|
296
|
+
async def get_supported_obis_codes(self, metering_point_code: str) -> List[ObisCode]:
|
297
|
+
"""
|
298
|
+
Get all OBIS codes that are supported by a given metering point.
|
299
|
+
|
300
|
+
NOTE: Please see the documentation of the probe_metering_point_obis_code method about best guess
|
301
|
+
behaviour. If this method returns an empty list, chances are high that the metering point code
|
302
|
+
is invalid or that the Energy ID has no access to it.
|
303
|
+
|
304
|
+
This method probes each OBIS code defined in the ObisCode enum to determine
|
305
|
+
which ones are supported by the specified metering point.
|
291
306
|
|
292
|
-
|
307
|
+
Args:
|
308
|
+
metering_point_code: The metering point code to check
|
309
|
+
|
310
|
+
Returns:
|
311
|
+
List[ObisCode]: A list of OBIS codes that are supported by the metering point
|
312
|
+
|
313
|
+
Raises:
|
314
|
+
UnauthorizedException: If the API returns a 401 status code
|
315
|
+
ForbiddenException: If the API returns a 403 status code
|
316
|
+
aiohttp.ClientError: For other request errors
|
317
|
+
"""
|
318
|
+
supported_codes = []
|
319
|
+
for obis_code in ObisCode:
|
320
|
+
if await self.probe_metering_point_obis_code(metering_point_code, obis_code):
|
321
|
+
supported_codes.append(obis_code)
|
322
|
+
return supported_codes
|
leneda/exceptions.py
CHANGED
@@ -19,9 +19,3 @@ 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 InvalidMeteringPointException(LenedaException):
|
25
|
-
"""Raised when a metering point code is invalid or not accessible."""
|
26
|
-
|
27
|
-
pass
|
leneda/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: leneda-client
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: Python client for the Leneda energy data platform
|
5
5
|
Home-page: https://github.com/fedus/leneda-client
|
6
6
|
Author: fedus
|
@@ -22,8 +22,9 @@ Classifier: Topic :: Utilities
|
|
22
22
|
Requires-Python: >=3.8
|
23
23
|
Description-Content-Type: text/markdown
|
24
24
|
License-File: LICENSE
|
25
|
-
Requires-Dist:
|
25
|
+
Requires-Dist: aiohttp>=3.9.0
|
26
26
|
Requires-Dist: python-dateutil>=2.8.2
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23.0
|
27
28
|
Dynamic: author
|
28
29
|
Dynamic: author-email
|
29
30
|
Dynamic: classifier
|
@@ -46,6 +47,9 @@ Dynamic: summary
|
|
46
47
|
|
47
48
|
A Python client for interacting with the Leneda energy data platform API.
|
48
49
|
|
50
|
+
PLEASE NOTE: As long as the library is in a version below 1.0.0, breaking changes
|
51
|
+
may also be introduced between minor version bumps.
|
52
|
+
|
49
53
|
## Overview
|
50
54
|
|
51
55
|
This client provides a simple interface to the Leneda API, which allows users to:
|
@@ -0,0 +1,11 @@
|
|
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,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
leneda/__init__.py,sha256=-BHIXBfTTbX6EKDHr2Bq7rPzsPNYrrkgs9AJqWJR_1Q,732
|
2
|
-
leneda/client.py,sha256=X2seOTrUYLKuLxDJT6ryGdKfNelsOhVq7lenJjI3kmM,10825
|
3
|
-
leneda/exceptions.py,sha256=a84T3uSDQsMiKyFUZTbJTAp3hELfI1GQDriqv6aMDb8,600
|
4
|
-
leneda/models.py,sha256=jdU2cIZZDExUSiSfz9zaYjJepr0m3v_x5b1fyOaEI8Q,7930
|
5
|
-
leneda/obis_codes.py,sha256=VfsJQN1U80eZ5g1bIteDCLkkmBQ0AIkkm_zNAeM1Dog,7507
|
6
|
-
leneda/version.py,sha256=wtPxgfjm82dK-e3EK0shDP4_7DlFk8hkRkOIPd6mV58,50
|
7
|
-
leneda_client-0.3.0.dist-info/licenses/LICENSE,sha256=nAhDs625lK6v8oLqjWCABKBKlwxVoRDFXQvoZPfOtKQ,1062
|
8
|
-
leneda_client-0.3.0.dist-info/METADATA,sha256=RHArkKFQqY--FL7-RCwTUQTaJvMWLvdswIYbM-8Yr1U,3344
|
9
|
-
leneda_client-0.3.0.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
|
10
|
-
leneda_client-0.3.0.dist-info/top_level.txt,sha256=PANScm25ep7WLjKiph0fhJPb8s_sa_uLHemnBpQBaJ8,7
|
11
|
-
leneda_client-0.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|