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 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 requests
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__(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,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(self, metering_point_code: str, obis_code: ObisCode) -> bool:
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
- requests.exceptions.RequestException: For other request errors
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
- requests.exceptions.RequestException: For other request errors
316
+ aiohttp.ClientError: For other request errors
315
317
  """
316
- return [
317
- obis_code
318
- for obis_code in ObisCode
319
- if self.probe_metering_point_obis_code(metering_point_code, obis_code)
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,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leneda-client
3
- Version: 0.4.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.3.1)
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=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,,