python-siseli 0.1.0__py3-none-any.whl → 0.1.2__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.
- {python_siseli-0.1.0.dist-info → python_siseli-0.1.2.dist-info}/METADATA +1 -1
- {python_siseli-0.1.0.dist-info → python_siseli-0.1.2.dist-info}/RECORD +6 -6
- siseli/auth.py +79 -6
- siseli/exceptions.py +20 -1
- {python_siseli-0.1.0.dist-info → python_siseli-0.1.2.dist-info}/WHEEL +0 -0
- {python_siseli-0.1.0.dist-info → python_siseli-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
siseli/__init__.py,sha256=_sHO0M0medXnJz_ZTtHKyy8DP6e40wuf5ubEGLC1kBs,1035
|
|
2
2
|
siseli/_utils.py,sha256=QT-ooemKKAIEHICNI2QErPvRkFIkUGo6ncUFCJM1kGc,727
|
|
3
3
|
siseli/alarms.py,sha256=Mc_CV8kUBFTXu8c48NstX8Lc6Q0bPTnhjwehx9sbCkQ,4853
|
|
4
|
-
siseli/auth.py,sha256=
|
|
4
|
+
siseli/auth.py,sha256=I8WwJgy1go1MGxjowkVx-lvCtneh5rRtbs9DV9yO4T8,6470
|
|
5
5
|
siseli/client.py,sha256=Eap7MTrnctdskt6kOzbns7SqUt27ZDjMlIKVshfcfN8,14200
|
|
6
6
|
siseli/config.py,sha256=J8raq7Yf4gCyjO9ISNoTrk_EYNEGrESV56O-aHfDvBU,3278
|
|
7
7
|
siseli/const.py,sha256=M8bBrc8cH0OyEGb9158_emux_dfknvikGQqm09iQ8CA,174
|
|
8
8
|
siseli/dashboard.py,sha256=JQHfi8orKs5uWsGGfoTvA14A9M_R9yIAYuGCxb4zTsM,3325
|
|
9
9
|
siseli/device.py,sha256=crix14pYakrDaFmomPC3Uwgdq-1boZ-ZaEg2HgzXibs,2762
|
|
10
10
|
siseli/dictionary.py,sha256=VSO83uXPaGLPzKGRmpROuWQKivXN7DNOmXN8QQJ-Y-w,1120
|
|
11
|
-
siseli/exceptions.py,sha256=
|
|
11
|
+
siseli/exceptions.py,sha256=y9ws9c2vr5Q3bZxLwkXybUqQooq57YEEXAN0wO3_fxk,1420
|
|
12
12
|
siseli/history.py,sha256=CgBvDfZBIrrewu7PeRv08-Zr4boEVOY8Xevruu-ge-I,3869
|
|
13
13
|
siseli/state.py,sha256=d7Exlf7FzF8O_CcF-NbAt6kq8qaR4fFhjGW8028kbrM,5800
|
|
14
14
|
siseli/station.py,sha256=e1OK4OIry9WUjT8KggQI5b4Acypm9D31mWAMTjDGFew,5531
|
|
@@ -23,7 +23,7 @@ siseli/models/dictionary.py,sha256=d6KCu77UMlPcIDON8G3sjuytkSfbI5go1y3I_jAGTX8,4
|
|
|
23
23
|
siseli/models/history.py,sha256=dx26VWLJosucMvwL36vojvAguvkMiv5GEKTllUtZV0Y,791
|
|
24
24
|
siseli/models/state.py,sha256=VK5CNqMLRkY1w9HrcUhvaO_6J-rjqP7VwvbppCFpucI,2393
|
|
25
25
|
siseli/models/station.py,sha256=02OLINAVblnKkF73ZwKxYvPtUFROsu4-g544oDg8ApA,1783
|
|
26
|
-
python_siseli-0.1.
|
|
27
|
-
python_siseli-0.1.
|
|
28
|
-
python_siseli-0.1.
|
|
29
|
-
python_siseli-0.1.
|
|
26
|
+
python_siseli-0.1.2.dist-info/METADATA,sha256=xnZJLpywI8nzDQEqwUgUsN9Pbx5iL7bnSqV3v326MFs,1407
|
|
27
|
+
python_siseli-0.1.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
28
|
+
python_siseli-0.1.2.dist-info/top_level.txt,sha256=Dofr9c7hGvRRV6ij6aoPuAZqxEjkgtsBeN4KtFHbGgo,7
|
|
29
|
+
python_siseli-0.1.2.dist-info/RECORD,,
|
siseli/auth.py
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
|
+
import logging
|
|
6
7
|
from datetime import UTC, datetime
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
9
10
|
|
|
10
|
-
from .exceptions import AuthenticationError
|
|
11
|
+
from .exceptions import AuthenticationError, NetworkError
|
|
11
12
|
from .models.auth import TokenInfo
|
|
12
13
|
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
def _md5(text: str) -> str:
|
|
15
18
|
# The Siseli Cloud API requires the MD5 hash of the plaintext password.
|
|
@@ -25,6 +28,28 @@ def _parse_dt(value: str | None) -> datetime | None:
|
|
|
25
28
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
26
29
|
|
|
27
30
|
|
|
31
|
+
def _extract_api_message(response: httpx.Response) -> str | None:
|
|
32
|
+
"""Extract a safe, short error message from an HTTP error response.
|
|
33
|
+
|
|
34
|
+
Tries to parse the body as JSON and returns the value of the first
|
|
35
|
+
recognised error field (``error``, ``message``, ``detail``). Falls back
|
|
36
|
+
to the first 200 characters of the raw text body so there is always
|
|
37
|
+
*something* useful in logs. Never returns passwords, tokens, or auth
|
|
38
|
+
headers — those are never present in response bodies.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
body = response.json()
|
|
42
|
+
if isinstance(body, dict):
|
|
43
|
+
for key in ("error", "message", "detail"):
|
|
44
|
+
value = body.get(key)
|
|
45
|
+
if value and isinstance(value, str):
|
|
46
|
+
return value[:200]
|
|
47
|
+
except Exception: # noqa: BLE001
|
|
48
|
+
pass
|
|
49
|
+
text = (response.text or "").strip()
|
|
50
|
+
return text[:200] if text else None
|
|
51
|
+
|
|
52
|
+
|
|
28
53
|
class Auth:
|
|
29
54
|
"""Manages credentials and the current access token.
|
|
30
55
|
|
|
@@ -41,21 +66,68 @@ class Auth:
|
|
|
41
66
|
async def login(self, http: httpx.AsyncClient) -> TokenInfo:
|
|
42
67
|
"""Authenticate and store the returned tokens.
|
|
43
68
|
|
|
44
|
-
Raises :exc:`~siseli.exceptions.AuthenticationError` on
|
|
69
|
+
Raises :exc:`~siseli.exceptions.AuthenticationError` on HTTP-level
|
|
70
|
+
auth failures (4xx). Raises :exc:`~siseli.exceptions.NetworkError`
|
|
71
|
+
on network / timeout errors so callers can distinguish the two cases.
|
|
45
72
|
"""
|
|
73
|
+
endpoint = "/apis/login/account"
|
|
74
|
+
_LOGGER.debug("Siseli auth: POST %s (account=%r)", endpoint, self._account)
|
|
75
|
+
|
|
46
76
|
try:
|
|
47
77
|
response = await http.post(
|
|
48
|
-
|
|
78
|
+
endpoint,
|
|
49
79
|
json={"account": self._account, "password": self._password_hash},
|
|
50
80
|
)
|
|
51
|
-
|
|
81
|
+
except httpx.TimeoutException as exc:
|
|
82
|
+
_LOGGER.warning("Siseli auth: request timed out for account=%r", self._account)
|
|
83
|
+
raise NetworkError(f"Login request timed out: {exc}") from exc
|
|
52
84
|
except httpx.HTTPError as exc:
|
|
53
|
-
|
|
85
|
+
_LOGGER.warning(
|
|
86
|
+
"Siseli auth: network error for account=%r: %s", self._account, exc
|
|
87
|
+
)
|
|
88
|
+
raise NetworkError(f"Login request failed: {exc}") from exc
|
|
89
|
+
|
|
90
|
+
http_status = response.status_code
|
|
91
|
+
_LOGGER.debug(
|
|
92
|
+
"Siseli auth: response status=%d for account=%r", http_status, self._account
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# For HTTP error responses, extract a safe diagnostic message and raise
|
|
96
|
+
# AuthenticationError (4xx) or NetworkError (5xx).
|
|
97
|
+
if not response.is_success:
|
|
98
|
+
api_message = _extract_api_message(response)
|
|
99
|
+
_LOGGER.warning(
|
|
100
|
+
"Siseli auth: failed for account=%r — HTTP %d%s",
|
|
101
|
+
self._account,
|
|
102
|
+
http_status,
|
|
103
|
+
f", api_message={api_message!r}" if api_message else "",
|
|
104
|
+
)
|
|
105
|
+
diag = f"HTTP {http_status}"
|
|
106
|
+
if api_message:
|
|
107
|
+
diag = f"{diag}: {api_message}"
|
|
108
|
+
if http_status >= 500:
|
|
109
|
+
raise NetworkError(f"Login request failed: {diag}")
|
|
110
|
+
raise AuthenticationError(
|
|
111
|
+
f"Login failed: {diag}",
|
|
112
|
+
http_status=http_status,
|
|
113
|
+
api_message=api_message,
|
|
114
|
+
)
|
|
54
115
|
|
|
55
116
|
body = response.json()
|
|
56
117
|
if body.get("code") != 0:
|
|
118
|
+
api_message = body.get("message") or body.get("error") or body.get("detail")
|
|
119
|
+
if api_message:
|
|
120
|
+
api_message = str(api_message)
|
|
121
|
+
_LOGGER.warning(
|
|
122
|
+
"Siseli auth: API returned non-zero code=%r for account=%r, message=%r",
|
|
123
|
+
body.get("code"),
|
|
124
|
+
self._account,
|
|
125
|
+
api_message,
|
|
126
|
+
)
|
|
57
127
|
raise AuthenticationError(
|
|
58
|
-
f"Login failed (code {body.get('code')}): {
|
|
128
|
+
f"Login failed (code {body.get('code')}): {api_message}",
|
|
129
|
+
http_status=http_status,
|
|
130
|
+
api_message=api_message,
|
|
59
131
|
)
|
|
60
132
|
|
|
61
133
|
data = body["data"]
|
|
@@ -68,6 +140,7 @@ class Auth:
|
|
|
68
140
|
account=data.get("account", self._account),
|
|
69
141
|
user_id=data.get("userId", ""),
|
|
70
142
|
)
|
|
143
|
+
_LOGGER.debug("Siseli auth: login successful for account=%r", self._account)
|
|
71
144
|
return self._token_info
|
|
72
145
|
|
|
73
146
|
@property
|
siseli/exceptions.py
CHANGED
|
@@ -8,7 +8,26 @@ class SiseliError(Exception):
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class AuthenticationError(SiseliError):
|
|
11
|
-
"""Raised when authentication fails.
|
|
11
|
+
"""Raised when authentication fails.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
http_status: HTTP status code returned by the server, or *None* when
|
|
15
|
+
the failure occurred before a response was received.
|
|
16
|
+
api_message: Short error message extracted from the API response body
|
|
17
|
+
(e.g. the ``message`` or ``error`` field), or *None* when
|
|
18
|
+
unavailable. Never contains passwords, tokens, or other secrets.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
*,
|
|
25
|
+
http_status: int | None = None,
|
|
26
|
+
api_message: str | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.http_status = http_status
|
|
29
|
+
self.api_message = api_message
|
|
30
|
+
super().__init__(message)
|
|
12
31
|
|
|
13
32
|
|
|
14
33
|
class TokenExpiredError(AuthenticationError):
|
|
File without changes
|
|
File without changes
|