python-duco-connectivity 0.3.0__tar.gz → 0.4.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.
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/PKG-INFO +1 -1
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/pyproject.toml +1 -1
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/__init__.py +2 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/client.py +9 -4
- python_duco_connectivity-0.4.0/src/duco_connectivity/exceptions.py +53 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/python_duco_connectivity.egg-info/PKG-INFO +1 -1
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_client.py +22 -4
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_exceptions.py +29 -0
- python_duco_connectivity-0.3.0/src/duco_connectivity/exceptions.py +0 -24
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/LICENSE +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/README.md +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/setup.cfg +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/__main__.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/cli.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/models.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/py.typed +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/python_duco_connectivity.egg-info/SOURCES.txt +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/python_duco_connectivity.egg-info/dependency_links.txt +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/python_duco_connectivity.egg-info/entry_points.txt +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/python_duco_connectivity.egg-info/requires.txt +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/python_duco_connectivity.egg-info/top_level.txt +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_api_reference.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_cli.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_local_sample_validation.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_models.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_pytest_live_support.py +0 -0
- {python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_replay_helpers.py +0 -0
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/__init__.py
RENAMED
|
@@ -7,6 +7,7 @@ from .exceptions import (
|
|
|
7
7
|
DucoConnectionError,
|
|
8
8
|
DucoError,
|
|
9
9
|
DucoRateLimitError,
|
|
10
|
+
DucoResponseError,
|
|
10
11
|
DucoWriteLimitError,
|
|
11
12
|
)
|
|
12
13
|
from .models import (
|
|
@@ -93,6 +94,7 @@ __all__ = [
|
|
|
93
94
|
"DucoConnectionError",
|
|
94
95
|
"DucoError",
|
|
95
96
|
"DucoRateLimitError",
|
|
97
|
+
"DucoResponseError",
|
|
96
98
|
"DucoWriteLimitError",
|
|
97
99
|
"DiagComponent",
|
|
98
100
|
"DiagStatus",
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/client.py
RENAMED
|
@@ -9,7 +9,12 @@ from urllib.parse import urlsplit
|
|
|
9
9
|
|
|
10
10
|
import aiohttp
|
|
11
11
|
|
|
12
|
-
from .exceptions import
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
DucoConnectionError,
|
|
14
|
+
DucoError,
|
|
15
|
+
DucoResponseError,
|
|
16
|
+
DucoWriteLimitError,
|
|
17
|
+
)
|
|
13
18
|
from .models import (
|
|
14
19
|
ActionItem,
|
|
15
20
|
ActionItemList,
|
|
@@ -215,18 +220,18 @@ class DucoClient:
|
|
|
215
220
|
path,
|
|
216
221
|
)
|
|
217
222
|
if response.status == 429:
|
|
223
|
+
body = await response.text()
|
|
218
224
|
_LOGGER.debug(
|
|
219
225
|
"Write limit reached for %s %s%s",
|
|
220
226
|
method,
|
|
221
227
|
self._base_url,
|
|
222
228
|
path,
|
|
223
229
|
)
|
|
224
|
-
raise DucoWriteLimitError()
|
|
230
|
+
raise DucoWriteLimitError(path=path, body=body)
|
|
225
231
|
|
|
226
232
|
if response.status >= 400:
|
|
227
233
|
body = await response.text()
|
|
228
|
-
|
|
229
|
-
raise DucoError(msg)
|
|
234
|
+
raise DucoResponseError(response.status, path, body)
|
|
230
235
|
|
|
231
236
|
try:
|
|
232
237
|
return await response.json(content_type=None)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Exceptions raised by the Duco client."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DucoError(Exception):
|
|
5
|
+
"""Base class for client errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DucoConnectionError(DucoError):
|
|
9
|
+
"""Raised when the client cannot reach the box."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DucoResponseError(DucoError):
|
|
13
|
+
"""Raised when the box responds with an HTTP error status."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
status: int,
|
|
18
|
+
path: str,
|
|
19
|
+
body: str = "",
|
|
20
|
+
*,
|
|
21
|
+
message: str | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.status = status
|
|
24
|
+
self.path = path
|
|
25
|
+
self.body = body
|
|
26
|
+
if message is None:
|
|
27
|
+
detail = f"Unexpected response {status} for {path}"
|
|
28
|
+
if body.strip():
|
|
29
|
+
detail = f"{detail}: {body}"
|
|
30
|
+
else:
|
|
31
|
+
detail = message
|
|
32
|
+
super().__init__(detail)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DucoWriteLimitError(DucoResponseError):
|
|
36
|
+
"""Raised when the box rejects writes because its budget is exhausted."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
remaining: int | None = None,
|
|
41
|
+
*,
|
|
42
|
+
path: str = "",
|
|
43
|
+
body: str = "",
|
|
44
|
+
) -> None:
|
|
45
|
+
self.remaining = remaining
|
|
46
|
+
detail = "Duco write capacity exhausted"
|
|
47
|
+
if remaining is not None:
|
|
48
|
+
detail = f"{detail} ({remaining} writes remaining)"
|
|
49
|
+
super().__init__(429, path, body, message=detail)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Backward-compatible alias for the old python-duco-client exception name.
|
|
53
|
+
DucoRateLimitError = DucoWriteLimitError
|
|
@@ -27,6 +27,7 @@ from duco_connectivity import (
|
|
|
27
27
|
DucoClient,
|
|
28
28
|
DucoConnectionError,
|
|
29
29
|
DucoError,
|
|
30
|
+
DucoResponseError,
|
|
30
31
|
DucoWriteLimitError,
|
|
31
32
|
InfoGroup,
|
|
32
33
|
InfoZone,
|
|
@@ -3320,16 +3321,25 @@ async def test_timeout_raises_duco_connection_error() -> None:
|
|
|
3320
3321
|
await client.async_get_api_info()
|
|
3321
3322
|
|
|
3322
3323
|
|
|
3323
|
-
async def
|
|
3324
|
-
"""HTTP >= 400 should raise
|
|
3324
|
+
async def test_http_error_raises_duco_response_error() -> None:
|
|
3325
|
+
"""HTTP >= 400 should raise DucoResponseError with HTTP metadata."""
|
|
3325
3326
|
mock_response = _response(status=500, json_payload={}, text_payload="boom")
|
|
3326
3327
|
|
|
3327
3328
|
async with aiohttp.ClientSession() as session:
|
|
3328
3329
|
client = DucoClient(session=session, host="192.0.2.94")
|
|
3329
3330
|
with patch.object(session, "request", _request(mock_response)):
|
|
3330
|
-
with pytest.raises(
|
|
3331
|
+
with pytest.raises(
|
|
3332
|
+
DucoResponseError,
|
|
3333
|
+
match="Unexpected response 500 for /api: boom",
|
|
3334
|
+
) as err_info:
|
|
3331
3335
|
await client.async_get_api_info()
|
|
3332
3336
|
|
|
3337
|
+
err = err_info.value
|
|
3338
|
+
assert isinstance(err, DucoError)
|
|
3339
|
+
assert err.status == 500
|
|
3340
|
+
assert err.path == "/api"
|
|
3341
|
+
assert err.body == "boom"
|
|
3342
|
+
|
|
3333
3343
|
|
|
3334
3344
|
async def test_invalid_json_raises_duco_error(api_info_data: dict[str, object]) -> None:
|
|
3335
3345
|
"""Non-JSON responses should raise DucoError."""
|
|
@@ -3355,10 +3365,18 @@ async def test_write_limit_error_is_raised() -> None:
|
|
|
3355
3365
|
async with aiohttp.ClientSession() as session:
|
|
3356
3366
|
client = DucoClient(session=session, host="192.0.2.94")
|
|
3357
3367
|
with patch.object(session, "request", MagicMock(return_value=request_context)):
|
|
3358
|
-
with pytest.raises(
|
|
3368
|
+
with pytest.raises(
|
|
3369
|
+
DucoWriteLimitError,
|
|
3370
|
+
match="Duco write capacity exhausted",
|
|
3371
|
+
) as err_info:
|
|
3359
3372
|
await client.async_set_ventilation_state(1, "MAN2")
|
|
3360
3373
|
|
|
3361
3374
|
request_context.__aexit__.assert_awaited_once()
|
|
3375
|
+
err = err_info.value
|
|
3376
|
+
assert isinstance(err, DucoError)
|
|
3377
|
+
assert err.status == 429
|
|
3378
|
+
assert err.path == "/action/nodes/1"
|
|
3379
|
+
assert err.body == ""
|
|
3362
3380
|
|
|
3363
3381
|
|
|
3364
3382
|
async def test_get_actions_returns_typed_items(
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Tests for the public exceptions."""
|
|
2
2
|
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
3
5
|
from duco_connectivity import (
|
|
4
6
|
DucoConnectionError,
|
|
5
7
|
DucoError,
|
|
6
8
|
DucoRateLimitError,
|
|
9
|
+
DucoResponseError,
|
|
7
10
|
DucoWriteLimitError,
|
|
8
11
|
)
|
|
9
12
|
|
|
@@ -20,6 +23,24 @@ def test_write_limit_error_inherits_from_base_error() -> None:
|
|
|
20
23
|
assert isinstance(err, DucoError)
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
def test_response_error_stores_http_metadata() -> None:
|
|
27
|
+
"""DucoResponseError should expose status, path, and body."""
|
|
28
|
+
err = DucoResponseError(404, "/info", "missing")
|
|
29
|
+
assert err.status == 404
|
|
30
|
+
assert err.path == "/info"
|
|
31
|
+
assert err.body == "missing"
|
|
32
|
+
assert str(err) == "Unexpected response 404 for /info: missing"
|
|
33
|
+
assert isinstance(err, DucoError)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("body", ["", " "])
|
|
37
|
+
def test_response_error_omits_empty_body_from_default_message(body: str) -> None:
|
|
38
|
+
"""DucoResponseError should omit the body suffix when no body content exists."""
|
|
39
|
+
err = DucoResponseError(404, "/info", body)
|
|
40
|
+
|
|
41
|
+
assert str(err) == "Unexpected response 404 for /info"
|
|
42
|
+
|
|
43
|
+
|
|
23
44
|
def test_write_limit_error_stores_remaining_count() -> None:
|
|
24
45
|
"""DucoWriteLimitError should expose the remaining count when present."""
|
|
25
46
|
err = DucoWriteLimitError(remaining=10)
|
|
@@ -33,3 +54,11 @@ def test_rate_limit_error_alias_points_to_write_limit_error() -> None:
|
|
|
33
54
|
err = DucoRateLimitError(remaining=3)
|
|
34
55
|
assert isinstance(err, DucoWriteLimitError)
|
|
35
56
|
assert err.remaining == 3
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_write_limit_error_exposes_http_metadata() -> None:
|
|
60
|
+
"""DucoWriteLimitError should expose HTTP metadata for 429 responses."""
|
|
61
|
+
err = DucoWriteLimitError(path="/config", body="rate limited")
|
|
62
|
+
assert err.status == 429
|
|
63
|
+
assert err.path == "/config"
|
|
64
|
+
assert err.body == "rate limited"
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"""Exceptions raised by the Duco client."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class DucoError(Exception):
|
|
5
|
-
"""Base class for client errors."""
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class DucoConnectionError(DucoError):
|
|
9
|
-
"""Raised when the client cannot reach the box."""
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DucoWriteLimitError(DucoError):
|
|
13
|
-
"""Raised when the box rejects writes because its budget is exhausted."""
|
|
14
|
-
|
|
15
|
-
def __init__(self, remaining: int | None = None) -> None:
|
|
16
|
-
self.remaining = remaining
|
|
17
|
-
detail = "Duco write capacity exhausted"
|
|
18
|
-
if remaining is not None:
|
|
19
|
-
detail = f"{detail} ({remaining} writes remaining)"
|
|
20
|
-
super().__init__(detail)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Backward-compatible alias for the old python-duco-client exception name.
|
|
24
|
-
DucoRateLimitError = DucoWriteLimitError
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/__main__.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/cli.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/models.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/src/duco_connectivity/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_api_reference.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_pytest_live_support.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.3.0 → python_duco_connectivity-0.4.0}/tests/test_replay_helpers.py
RENAMED
|
File without changes
|