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 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 requests
13
+ import aiohttp
14
+ from aiohttp import ClientTimeout
14
15
 
15
- from .exceptions import ForbiddenException, InvalidMeteringPointException, UnauthorizedException
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__(self, api_key: str, energy_id: str, debug: bool = False):
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
- requests.exceptions.RequestException: For other request errors
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
- # Make the request
92
- response = requests.request(
93
- method=method, url=url, headers=self.headers, params=params, json=json_data
94
- )
95
-
96
- # Check for HTTP errors
97
- if response.status_code == 401:
98
- raise UnauthorizedException(
99
- "API authentication failed. Please check your API key and energy ID."
100
- )
101
- if response.status_code == 403:
102
- raise ForbiddenException(
103
- "Access forbidden. This may be due to Leneda's geoblocking or other access restrictions."
104
- )
105
- response.raise_for_status()
106
-
107
- # Parse the response
108
- if response.content:
109
- response_data = response.json()
110
- logger.debug(f"Response status: {response.status_code}")
111
- logger.debug(f"Response data: {json.dumps(response_data, indent=2)}")
112
- return response_data
113
- else:
114
- logger.debug(f"Response status: {response.status_code} (no content)")
115
- return {}
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 test_metering_point(self, metering_point_code: str) -> bool:
253
+ async def probe_metering_point_obis_code(
254
+ self, metering_point_code: str, obis_code: ObisCode
255
+ ) -> bool:
254
256
  """
255
- Test if a metering point code is valid and accessible.
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 code is valid by making a request
258
- for aggregated metering data. If the unit property in the response is null,
259
- it indicates that the metering point is invalid or not accessible.
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 test
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 is valid and accessible, False otherwise
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
- requests.exceptions.RequestException: For other request errors
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 electricity consumption
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=ObisCode.ELEC_CONSUMPTION_ACTIVE,
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
- # If we get here and the unit is None, the metering point is invalid
287
- if result.unit is None:
288
- raise InvalidMeteringPointException(
289
- f"Metering point {metering_point_code} is invalid or not accessible"
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
- return True
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,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.5.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leneda-client
3
- Version: 0.3.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: requests>=2.25.0
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,