leneda-client 0.4.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 +58 -56
- leneda/version.py +1 -1
- {leneda_client-0.4.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.4.0.dist-info → leneda_client-0.5.0.dist-info}/WHEEL +1 -1
- leneda_client-0.4.0.dist-info/RECORD +0 -11
- {leneda_client-0.4.0.dist-info → leneda_client-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {leneda_client-0.4.0.dist-info → leneda_client-0.5.0.dist-info}/top_level.txt +0 -0
leneda/client.py
CHANGED
@@ -10,7 +10,8 @@ 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
16
|
from .exceptions import ForbiddenException, UnauthorizedException
|
16
17
|
from .models import (
|
@@ -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,11 +246,13 @@ 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 probe_metering_point_obis_code(
|
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.
|
256
258
|
|
@@ -272,14 +274,14 @@ class LenedaClient:
|
|
272
274
|
Raises:
|
273
275
|
UnauthorizedException: If the API returns a 401 status code
|
274
276
|
ForbiddenException: If the API returns a 403 status code
|
275
|
-
|
277
|
+
aiohttp.ClientError: For other request errors
|
276
278
|
"""
|
277
279
|
# Use arbitrary time window
|
278
280
|
end_date = datetime.now()
|
279
281
|
start_date = end_date - timedelta(weeks=4)
|
280
282
|
|
281
283
|
# Try to get aggregated data for the specified OBIS code
|
282
|
-
result = self.get_aggregated_metering_data(
|
284
|
+
result = await self.get_aggregated_metering_data(
|
283
285
|
metering_point_code=metering_point_code,
|
284
286
|
obis_code=obis_code,
|
285
287
|
start_date=start_date,
|
@@ -291,7 +293,7 @@ class LenedaClient:
|
|
291
293
|
# Return True if we got data (unit is not None), False otherwise
|
292
294
|
return result.unit is not None
|
293
295
|
|
294
|
-
def get_supported_obis_codes(self, metering_point_code: str) -> List[ObisCode]:
|
296
|
+
async def get_supported_obis_codes(self, metering_point_code: str) -> List[ObisCode]:
|
295
297
|
"""
|
296
298
|
Get all OBIS codes that are supported by a given metering point.
|
297
299
|
|
@@ -311,10 +313,10 @@ class LenedaClient:
|
|
311
313
|
Raises:
|
312
314
|
UnauthorizedException: If the API returns a 401 status code
|
313
315
|
ForbiddenException: If the API returns a 403 status code
|
314
|
-
|
316
|
+
aiohttp.ClientError: For other request errors
|
315
317
|
"""
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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/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=RcANz6UweQ13fJHKwWP1V5dWnqSwcG4Ot9ws1grhZqQ,12250
|
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=e6aGg4sxTYiyshAbW98gDoekO-J6qYKpZNzhVZhaYUk,50
|
7
|
-
leneda_client-0.4.0.dist-info/licenses/LICENSE,sha256=nAhDs625lK6v8oLqjWCABKBKlwxVoRDFXQvoZPfOtKQ,1062
|
8
|
-
leneda_client-0.4.0.dist-info/METADATA,sha256=TGPWdTziOm6yt9HLE8GKtbDxUaU-N9GHcNH9vsAbdzw,3344
|
9
|
-
leneda_client-0.4.0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
10
|
-
leneda_client-0.4.0.dist-info/top_level.txt,sha256=PANScm25ep7WLjKiph0fhJPb8s_sa_uLHemnBpQBaJ8,7
|
11
|
-
leneda_client-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|