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 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._make_request(method="GET", endpoint=endpoint, params=params)
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._make_request(method="GET", endpoint=endpoint, params=params)
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
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leneda-client
3
- Version: 0.5.0
3
+ Version: 0.6.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
@@ -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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
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=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,,