leneda-client 0.5.0__tar.gz → 0.6.0__tar.gz
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-0.5.0/src/leneda_client.egg-info → leneda_client-0.6.0}/PKG-INFO +1 -1
- {leneda_client-0.5.0 → leneda_client-0.6.0}/examples/advanced_usage.py +3 -3
- {leneda_client-0.5.0 → leneda_client-0.6.0}/examples/basic_usage.py +7 -7
- {leneda_client-0.5.0 → leneda_client-0.6.0}/pyproject.toml +3 -3
- {leneda_client-0.5.0 → leneda_client-0.6.0}/setup.py +2 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda/client.py +82 -6
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda/exceptions.py +6 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda/models.py +9 -0
- leneda_client-0.6.0/src/leneda/py.typed +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda/version.py +1 -1
- {leneda_client-0.5.0 → leneda_client-0.6.0/src/leneda_client.egg-info}/PKG-INFO +1 -1
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda_client.egg-info/SOURCES.txt +1 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/tests/test_client.py +142 -31
- {leneda_client-0.5.0 → leneda_client-0.6.0}/LICENSE +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/MANIFEST.in +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/README.md +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/requirements.txt +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/setup.cfg +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda/__init__.py +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda/obis_codes.py +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda_client.egg-info/dependency_links.txt +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda_client.egg-info/requires.txt +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/src/leneda_client.egg-info/top_level.txt +0 -0
- {leneda_client-0.5.0 → leneda_client-0.6.0}/tests/__init__.py +0 -0
@@ -36,7 +36,7 @@ logging.basicConfig(
|
|
36
36
|
logger = logging.getLogger("leneda_advanced_example")
|
37
37
|
|
38
38
|
|
39
|
-
def parse_arguments():
|
39
|
+
def parse_arguments() -> argparse.Namespace:
|
40
40
|
"""Parse command-line arguments."""
|
41
41
|
parser = argparse.ArgumentParser(description="Leneda API Client Advanced Usage Example")
|
42
42
|
|
@@ -94,7 +94,7 @@ def parse_arguments():
|
|
94
94
|
return parser.parse_args()
|
95
95
|
|
96
96
|
|
97
|
-
def get_credentials(args):
|
97
|
+
def get_credentials(args: argparse.Namespace) -> tuple[str, str]:
|
98
98
|
"""Get API credentials from arguments or environment variables."""
|
99
99
|
api_key = args.api_key or os.environ.get("LENEDA_API_KEY")
|
100
100
|
energy_id = args.energy_id or os.environ.get("LENEDA_ENERGY_ID")
|
@@ -159,7 +159,7 @@ def detect_anomalies(df: pd.DataFrame, threshold: float) -> pd.DataFrame:
|
|
159
159
|
return df
|
160
160
|
|
161
161
|
|
162
|
-
async def main():
|
162
|
+
async def main() -> None:
|
163
163
|
# Parse command-line arguments
|
164
164
|
args = parse_arguments()
|
165
165
|
|
@@ -29,7 +29,7 @@ logging.basicConfig(
|
|
29
29
|
logger = logging.getLogger("leneda_example")
|
30
30
|
|
31
31
|
|
32
|
-
def parse_arguments():
|
32
|
+
def parse_arguments() -> argparse.Namespace:
|
33
33
|
"""Parse command-line arguments."""
|
34
34
|
parser = argparse.ArgumentParser(description="Leneda API Client Basic Usage Example")
|
35
35
|
|
@@ -59,7 +59,7 @@ def parse_arguments():
|
|
59
59
|
return parser.parse_args()
|
60
60
|
|
61
61
|
|
62
|
-
def get_credentials(args):
|
62
|
+
def get_credentials(args: argparse.Namespace) -> tuple[str, str]:
|
63
63
|
"""Get API credentials from arguments or environment variables."""
|
64
64
|
api_key = args.api_key or os.environ.get("LENEDA_API_KEY")
|
65
65
|
energy_id = args.energy_id or os.environ.get("LENEDA_ENERGY_ID")
|
@@ -79,7 +79,7 @@ def get_credentials(args):
|
|
79
79
|
return api_key, energy_id
|
80
80
|
|
81
81
|
|
82
|
-
async def main():
|
82
|
+
async def main() -> None:
|
83
83
|
# Parse command-line arguments
|
84
84
|
args = parse_arguments()
|
85
85
|
|
@@ -148,11 +148,11 @@ async def main():
|
|
148
148
|
# Display all measurements
|
149
149
|
if aggregated_data.aggregated_time_series:
|
150
150
|
print("\nMonthly measurements:")
|
151
|
-
for
|
151
|
+
for metering_value in aggregated_data.aggregated_time_series:
|
152
152
|
print(
|
153
|
-
f"Period: {
|
154
|
-
f"Value: {
|
155
|
-
f"Calculated: {
|
153
|
+
f"Period: {metering_value.started_at.strftime('%Y-%m')}, "
|
154
|
+
f"Value: {metering_value.value} {aggregated_data.unit}, "
|
155
|
+
f"Calculated: {metering_value.calculated}"
|
156
156
|
)
|
157
157
|
|
158
158
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[tool.black]
|
6
6
|
line-length = 100
|
7
|
-
target-version = ['
|
7
|
+
target-version = ['py39', 'py310', 'py311']
|
8
8
|
include = '\.pyi?$'
|
9
9
|
|
10
10
|
[tool.isort]
|
@@ -14,12 +14,12 @@ multi_line_output = 3
|
|
14
14
|
src_paths = ["src", "tests"]
|
15
15
|
|
16
16
|
[tool.mypy]
|
17
|
-
python_version = "3.
|
17
|
+
python_version = "3.9"
|
18
18
|
warn_return_any = true
|
19
19
|
warn_unused_configs = true
|
20
20
|
disallow_untyped_defs = true
|
21
21
|
disallow_incomplete_defs = true
|
22
|
-
|
22
|
+
explicit_package_bases = true
|
23
23
|
|
24
24
|
[tool.pytest.ini_options]
|
25
25
|
testpaths = ["tests"]
|
@@ -35,6 +35,8 @@ setup(
|
|
35
35
|
url="https://github.com/fedus/leneda-client",
|
36
36
|
package_dir={"": "src"},
|
37
37
|
packages=find_packages(where="src"),
|
38
|
+
include_package_data=True,
|
39
|
+
package_data={"leneda": ["py.typed"]},
|
38
40
|
install_requires=requirements,
|
39
41
|
classifiers=[
|
40
42
|
"Development Status :: 4 - Beta",
|
@@ -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.
|
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.
|
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
|
@@ -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
|
@@ -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"
|
File without changes
|
@@ -3,25 +3,24 @@ Tests for the Leneda API client.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import json
|
6
|
-
import os
|
7
|
-
import sys
|
8
6
|
import unittest
|
9
|
-
from
|
7
|
+
from typing import Any
|
8
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
10
9
|
|
11
10
|
import aiohttp
|
12
11
|
import pytest
|
13
|
-
|
14
|
-
# Add the src directory to the path
|
15
|
-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
12
|
+
from aiohttp import ClientResponseError
|
16
13
|
|
17
14
|
from src.leneda import LenedaClient
|
18
15
|
from src.leneda.exceptions import (
|
19
16
|
ForbiddenException,
|
17
|
+
MeteringPointNotFoundException,
|
20
18
|
UnauthorizedException,
|
21
19
|
)
|
22
20
|
from src.leneda.models import (
|
23
21
|
AggregatedMeteringData,
|
24
22
|
AggregatedMeteringValue,
|
23
|
+
AuthenticationProbeResult,
|
25
24
|
MeteringData,
|
26
25
|
MeteringValue,
|
27
26
|
)
|
@@ -33,7 +32,7 @@ class TestLenedaClient:
|
|
33
32
|
"""Test cases for the LenedaClient class."""
|
34
33
|
|
35
34
|
@pytest.fixture(autouse=True)
|
36
|
-
def setup(self):
|
35
|
+
def setup(self) -> None:
|
37
36
|
"""Set up test fixtures."""
|
38
37
|
self.api_key = "test_api_key"
|
39
38
|
self.energy_id = "test_energy_id"
|
@@ -82,7 +81,7 @@ class TestLenedaClient:
|
|
82
81
|
}
|
83
82
|
|
84
83
|
@patch("aiohttp.ClientSession.request")
|
85
|
-
async def test_get_time_series(self, mock_request):
|
84
|
+
async def test_get_time_series(self, mock_request: Any) -> None:
|
86
85
|
"""Test getting time series data."""
|
87
86
|
# Set up the mock response
|
88
87
|
mock_response = AsyncMock()
|
@@ -131,7 +130,7 @@ class TestLenedaClient:
|
|
131
130
|
}
|
132
131
|
|
133
132
|
@patch("aiohttp.ClientSession.request")
|
134
|
-
async def test_get_aggregated_time_series(self, mock_request):
|
133
|
+
async def test_get_aggregated_time_series(self, mock_request: Any) -> None:
|
135
134
|
"""Test getting aggregated time series data."""
|
136
135
|
# Set up the mock response
|
137
136
|
mock_response = AsyncMock()
|
@@ -183,7 +182,7 @@ class TestLenedaClient:
|
|
183
182
|
}
|
184
183
|
|
185
184
|
@patch("aiohttp.ClientSession.request")
|
186
|
-
async def test_request_metering_data_access(self, mock_request):
|
185
|
+
async def test_request_metering_data_access(self, mock_request: Any) -> None:
|
187
186
|
"""Test requesting metering data access."""
|
188
187
|
# Set up the mock response
|
189
188
|
mock_response = AsyncMock()
|
@@ -224,14 +223,12 @@ class TestLenedaClient:
|
|
224
223
|
}
|
225
224
|
|
226
225
|
@patch("aiohttp.ClientSession.request")
|
227
|
-
async def test_unauthorized_error(self, mock_request):
|
226
|
+
async def test_unauthorized_error(self, mock_request: Any) -> None:
|
228
227
|
"""Test handling of 401 Unauthorized errors."""
|
229
228
|
# Set up the mock response with 401 status
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
mock_response.raise_for_status = lambda: None
|
234
|
-
mock_request.return_value.__aenter__.return_value = mock_response
|
229
|
+
mock_request.return_value.__aenter__.return_value = build_error_mock_response(
|
230
|
+
401, "Unauthorized"
|
231
|
+
)
|
235
232
|
|
236
233
|
# Call the method and check that it raises UnauthorizedException
|
237
234
|
with pytest.raises(UnauthorizedException) as exc_info:
|
@@ -246,14 +243,12 @@ class TestLenedaClient:
|
|
246
243
|
assert "API authentication failed" in str(exc_info.value)
|
247
244
|
|
248
245
|
@patch("aiohttp.ClientSession.request")
|
249
|
-
async def test_forbidden_error(self, mock_request):
|
246
|
+
async def test_forbidden_error(self, mock_request: Any) -> None:
|
250
247
|
"""Test handling of 403 Forbidden errors."""
|
251
248
|
# Set up the mock response with 403 status
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
mock_response.raise_for_status = lambda: None
|
256
|
-
mock_request.return_value.__aenter__.return_value = mock_response
|
249
|
+
mock_request.return_value.__aenter__.return_value = build_error_mock_response(
|
250
|
+
403, "Forbidden"
|
251
|
+
)
|
257
252
|
|
258
253
|
# Call the method and check that it raises ForbiddenException
|
259
254
|
with pytest.raises(ForbiddenException) as exc_info:
|
@@ -268,13 +263,15 @@ class TestLenedaClient:
|
|
268
263
|
assert "geoblocking" in str(exc_info.value)
|
269
264
|
|
270
265
|
@patch("aiohttp.ClientSession.request")
|
271
|
-
async def
|
272
|
-
"""Test error handling
|
266
|
+
async def test_metering_point_not_found_handling(self, mock_request: Any) -> None:
|
267
|
+
"""Test error handling when a metering point is not found."""
|
273
268
|
# Set up the mock response to raise an exception
|
274
|
-
mock_request.
|
269
|
+
mock_request.return_value.__aenter__.return_value = build_error_mock_response(
|
270
|
+
404, "Not found"
|
271
|
+
)
|
275
272
|
|
276
273
|
# Call the method and check that it raises an exception
|
277
|
-
with pytest.raises(
|
274
|
+
with pytest.raises(MeteringPointNotFoundException):
|
278
275
|
await self.client.get_metering_data(
|
279
276
|
"LU-METERING_POINT1",
|
280
277
|
ObisCode.ELEC_CONSUMPTION_ACTIVE,
|
@@ -283,7 +280,7 @@ class TestLenedaClient:
|
|
283
280
|
)
|
284
281
|
|
285
282
|
@patch("aiohttp.ClientSession.request")
|
286
|
-
async def test_probe_metering_point_obis_code_valid(self, mock_request):
|
283
|
+
async def test_probe_metering_point_obis_code_valid(self, mock_request: Any) -> None:
|
287
284
|
"""Test probe_metering_point_obis_code with a valid metering point and OBIS code."""
|
288
285
|
# Set up the mock response with valid data
|
289
286
|
mock_response = AsyncMock()
|
@@ -304,7 +301,7 @@ class TestLenedaClient:
|
|
304
301
|
mock_request.assert_called_once()
|
305
302
|
|
306
303
|
@patch("aiohttp.ClientSession.request")
|
307
|
-
async def test_probe_metering_point_obis_code_invalid(self, mock_request):
|
304
|
+
async def test_probe_metering_point_obis_code_invalid(self, mock_request: Any) -> None:
|
308
305
|
"""Test probe_metering_point_obis_code with an invalid metering point or unsupported OBIS code."""
|
309
306
|
# Set up the mock response with null unit
|
310
307
|
mock_response = AsyncMock()
|
@@ -325,11 +322,11 @@ class TestLenedaClient:
|
|
325
322
|
mock_request.assert_called_once()
|
326
323
|
|
327
324
|
@patch.object(LenedaClient, "probe_metering_point_obis_code")
|
328
|
-
async def test_get_supported_obis_codes(self, mock_probe):
|
325
|
+
async def test_get_supported_obis_codes(self, mock_probe: Any) -> None:
|
329
326
|
"""Test getting supported OBIS codes for a metering point."""
|
330
327
|
|
331
328
|
# Mock probe_metering_point_obis_code to return True for two codes, False otherwise
|
332
|
-
def side_effect(metering_point_code, obis_code):
|
329
|
+
def side_effect(metering_point_code: str, obis_code: ObisCode) -> bool:
|
333
330
|
return obis_code in [ObisCode.ELEC_CONSUMPTION_ACTIVE, ObisCode.ELEC_PRODUCTION_ACTIVE]
|
334
331
|
|
335
332
|
mock_probe.side_effect = side_effect
|
@@ -347,7 +344,7 @@ class TestLenedaClient:
|
|
347
344
|
assert mock_probe.call_count == len(ObisCode)
|
348
345
|
|
349
346
|
@patch.object(LenedaClient, "probe_metering_point_obis_code", return_value=False)
|
350
|
-
async def test_get_supported_obis_codes_none(self, mock_probe):
|
347
|
+
async def test_get_supported_obis_codes_none(self, mock_probe: Any) -> None:
|
351
348
|
"""Test getting supported OBIS codes when none are supported."""
|
352
349
|
# Call the method
|
353
350
|
result = await self.client.get_supported_obis_codes("INVALID-METERING-POINT")
|
@@ -359,6 +356,120 @@ class TestLenedaClient:
|
|
359
356
|
# Check that the probe was called for each OBIS code
|
360
357
|
assert mock_probe.call_count == len(ObisCode)
|
361
358
|
|
359
|
+
@patch("aiohttp.ClientSession.request")
|
360
|
+
async def test_probe_credentials_success(self, mock_request: Any) -> None:
|
361
|
+
"""Test probe_credentials with valid credentials (400 response expected)."""
|
362
|
+
# Set up the mock response with 400 status (expected for valid credentials with invalid request)
|
363
|
+
mock_request.return_value.__aenter__.return_value = build_error_mock_response(
|
364
|
+
400, "Bad request"
|
365
|
+
)
|
366
|
+
|
367
|
+
# Call the method
|
368
|
+
result = await self.client.probe_credentials()
|
369
|
+
|
370
|
+
# Check the result
|
371
|
+
assert result == AuthenticationProbeResult.SUCCESS
|
372
|
+
|
373
|
+
# Check that the request was made correctly
|
374
|
+
mock_request.assert_called_once()
|
375
|
+
call_kwargs = mock_request.call_args[1]
|
376
|
+
assert call_kwargs["method"] == "POST"
|
377
|
+
assert call_kwargs["url"] == "https://api.leneda.lu/api/metering-data-access-request"
|
378
|
+
assert call_kwargs["headers"] == {
|
379
|
+
"X-API-KEY": "test_api_key",
|
380
|
+
"X-ENERGY-ID": "test_energy_id",
|
381
|
+
"Content-Type": "application/json",
|
382
|
+
}
|
383
|
+
assert call_kwargs["json"] == {
|
384
|
+
"from": "",
|
385
|
+
"fromName": "",
|
386
|
+
"meteringPointCodes": [],
|
387
|
+
"obisCodes": [],
|
388
|
+
}
|
389
|
+
|
390
|
+
@patch("aiohttp.ClientSession.request")
|
391
|
+
async def test_probe_credentials_unauthorized(self, mock_request: Any) -> None:
|
392
|
+
"""Test probe_credentials with invalid credentials (401 response)."""
|
393
|
+
# Set up the mock response with 401 status
|
394
|
+
mock_request.return_value.__aenter__.return_value = build_error_mock_response(
|
395
|
+
401, "Unauthorized"
|
396
|
+
)
|
397
|
+
|
398
|
+
# Call the method
|
399
|
+
result = await self.client.probe_credentials()
|
400
|
+
|
401
|
+
# Check the result
|
402
|
+
assert result == AuthenticationProbeResult.FAILURE
|
403
|
+
|
404
|
+
# Check that the request was made correctly
|
405
|
+
mock_request.assert_called_once()
|
406
|
+
|
407
|
+
@patch("aiohttp.ClientSession.request")
|
408
|
+
async def test_probe_credentials_forbidden(self, mock_request: Any) -> None:
|
409
|
+
"""Test probe_credentials with forbidden access (403 response)."""
|
410
|
+
# Set up the mock response with 403 status
|
411
|
+
mock_request.return_value.__aenter__.return_value = build_error_mock_response(
|
412
|
+
403, "Forbidden"
|
413
|
+
)
|
414
|
+
|
415
|
+
# Call the method and check that it raises ForbiddenException
|
416
|
+
with pytest.raises(ForbiddenException):
|
417
|
+
await self.client.probe_credentials()
|
418
|
+
|
419
|
+
# Check that the request was made correctly
|
420
|
+
mock_request.assert_called_once()
|
421
|
+
|
422
|
+
@patch("aiohttp.ClientSession.request")
|
423
|
+
async def test_probe_credentials_unknown_response(self, mock_request: Any) -> None:
|
424
|
+
"""Test probe_credentials with unexpected response (500 response)."""
|
425
|
+
mock_request.return_value.__aenter__.return_value = build_error_mock_response(
|
426
|
+
500, "Internal server error"
|
427
|
+
)
|
428
|
+
|
429
|
+
# Call the method
|
430
|
+
result = await self.client.probe_credentials()
|
431
|
+
|
432
|
+
# Check the result
|
433
|
+
assert result == AuthenticationProbeResult.UNKNOWN
|
434
|
+
|
435
|
+
# Check that the request was made correctly
|
436
|
+
mock_request.assert_called_once()
|
437
|
+
|
438
|
+
@patch("aiohttp.ClientSession.request")
|
439
|
+
async def test_probe_credentials_network_error(self, mock_request: Any) -> None:
|
440
|
+
"""Test probe_credentials with network error."""
|
441
|
+
# Set up the mock to raise a network error
|
442
|
+
mock_request.side_effect = aiohttp.ClientError("Network error")
|
443
|
+
|
444
|
+
# Call the method
|
445
|
+
result = await self.client.probe_credentials()
|
446
|
+
|
447
|
+
# Check the result
|
448
|
+
assert result == AuthenticationProbeResult.UNKNOWN
|
449
|
+
|
450
|
+
# Check that the request was attempted
|
451
|
+
mock_request.assert_called_once()
|
452
|
+
|
453
|
+
|
454
|
+
def build_error_mock_response(status: int, message: str) -> AsyncMock:
|
455
|
+
"""Build a mock response for an error case."""
|
456
|
+
|
457
|
+
def raise_for_status() -> None:
|
458
|
+
raise ClientResponseError(
|
459
|
+
status=status,
|
460
|
+
history=(),
|
461
|
+
message=message,
|
462
|
+
headers=None,
|
463
|
+
request_info=MagicMock(),
|
464
|
+
)
|
465
|
+
|
466
|
+
mock_response = AsyncMock()
|
467
|
+
mock_response.status = status
|
468
|
+
mock_response.content = message
|
469
|
+
mock_response.raise_for_status = raise_for_status
|
470
|
+
|
471
|
+
return mock_response
|
472
|
+
|
362
473
|
|
363
474
|
if __name__ == "__main__":
|
364
475
|
unittest.main()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|