Skip to content

Commit 19d71b1

Browse files
mr-milesiMicknl
authored andcommitted
Move action-error handling to a separate function
Lift behaviour to higher level so caller can specify the behaviour Specifically to avoid throwing exceptions on missing path errors when getting DeviceInfo
1 parent a46d319 commit 19d71b1

2 files changed

Lines changed: 133 additions & 43 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Logic to spot and create ActionErrorExceptions"""
2+
3+
from sagemcom_api.const import (
4+
XMO_ACCESS_RESTRICTION_ERR,
5+
XMO_AUTHENTICATION_ERR,
6+
XMO_LOGIN_RETRY_ERR,
7+
XMO_MAX_SESSION_COUNT_ERR,
8+
XMO_NON_WRITABLE_PARAMETER_ERR,
9+
XMO_NO_ERR,
10+
XMO_REQUEST_ACTION_ERR,
11+
XMO_UNKNOWN_PATH_ERR
12+
)
13+
from sagemcom_api.exceptions import (
14+
AccessRestrictionException,
15+
AuthenticationException,
16+
LoginRetryErrorException,
17+
MaximumSessionCountException,
18+
NonWritableParameterException,
19+
UnknownException,
20+
UnknownPathException
21+
)
22+
23+
24+
class ActionErrorHandler:
25+
"""Raised when a requested action has an error"""
26+
27+
KNOWN_EXCEPTIONS = (
28+
XMO_AUTHENTICATION_ERR,
29+
XMO_ACCESS_RESTRICTION_ERR,
30+
XMO_NON_WRITABLE_PARAMETER_ERR,
31+
XMO_UNKNOWN_PATH_ERR,
32+
XMO_MAX_SESSION_COUNT_ERR,
33+
XMO_LOGIN_RETRY_ERR
34+
)
35+
36+
@staticmethod
37+
def throw_if(response):
38+
"""For anywhere that needs the old single-exception behaviour"""
39+
40+
if response["reply"]["error"]["description"] != XMO_REQUEST_ACTION_ERR:
41+
return
42+
43+
actions = response["reply"]["actions"]
44+
for action in actions:
45+
46+
action_error = action["error"]
47+
action_error_desc = action_error["description"]
48+
49+
if action_error_desc == XMO_NO_ERR:
50+
continue
51+
52+
raise ActionErrorHandler.from_error_description(action_error, action_error_desc)
53+
54+
@staticmethod
55+
def is_unknown_exception(desc):
56+
"""
57+
True/false if the ActionError is one of our known types
58+
"""
59+
60+
return False if desc == XMO_NO_ERR else desc not in ActionErrorHandler.KNOWN_EXCEPTIONS
61+
62+
@staticmethod
63+
def from_error_description(action_error, action_error_desc):
64+
"""
65+
Create the correct exception from an error, for the caller to throw
66+
"""
67+
# pylint: disable=too-many-return-statements
68+
69+
if action_error_desc == XMO_AUTHENTICATION_ERR:
70+
return AuthenticationException(action_error)
71+
72+
if action_error_desc == XMO_ACCESS_RESTRICTION_ERR:
73+
return AccessRestrictionException(action_error)
74+
75+
if action_error_desc == XMO_NON_WRITABLE_PARAMETER_ERR:
76+
return NonWritableParameterException(action_error)
77+
78+
if action_error_desc == XMO_UNKNOWN_PATH_ERR:
79+
return UnknownPathException(action_error)
80+
81+
if action_error_desc == XMO_MAX_SESSION_COUNT_ERR:
82+
return MaximumSessionCountException(action_error)
83+
84+
if action_error_desc == XMO_LOGIN_RETRY_ERR:
85+
return LoginRetryErrorException(action_error)
86+
87+
return UnknownException(action_error)

sagemcom_api/client.py

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import hashlib
67
import json
78
import math
@@ -21,33 +22,25 @@
2122
ServerDisconnectedError,
2223
TCPConnector,
2324
)
25+
from .action_error_exception_handler import ActionErrorHandler
2426

2527
from .const import (
2628
API_ENDPOINT,
2729
DEFAULT_TIMEOUT,
2830
DEFAULT_USER_AGENT,
2931
UINT_MAX,
30-
XMO_ACCESS_RESTRICTION_ERR,
31-
XMO_AUTHENTICATION_ERR,
3232
XMO_INVALID_SESSION_ERR,
33-
XMO_LOGIN_RETRY_ERR,
34-
XMO_MAX_SESSION_COUNT_ERR,
3533
XMO_NO_ERR,
36-
XMO_NON_WRITABLE_PARAMETER_ERR,
3734
XMO_REQUEST_ACTION_ERR,
3835
XMO_REQUEST_NO_ERR,
39-
XMO_UNKNOWN_PATH_ERR,
4036
)
4137
from .enums import EncryptionMethod
4238
from .exceptions import (
43-
AccessRestrictionException,
4439
AuthenticationException,
4540
BadRequestException,
4641
InvalidSessionException,
4742
LoginRetryErrorException,
4843
LoginTimeoutException,
49-
MaximumSessionCountException,
50-
NonWritableParameterException,
5144
UnauthorizedException,
5245
UnknownException,
5346
UnknownPathException,
@@ -191,8 +184,19 @@ def __get_response(self, response, index=0):
191184

192185
return value
193186

194-
def __get_response_value(self, response, index=0):
187+
def __get_response_value(self, response, index=0, throw_on_action_error: bool = False):
195188
"""Retrieve response value from value."""
189+
if throw_on_action_error:
190+
try:
191+
error = response["reply"]["actions"][index]["error"]
192+
except (KeyError, IndexError):
193+
error = None
194+
195+
if error is not None:
196+
error_description = error["description"]
197+
if error_description != XMO_NO_ERR:
198+
raise ActionErrorHandler.from_error_description(error, error_description)
199+
196200
try:
197201
value = self.__get_response(response, index)["value"]
198202
except KeyError:
@@ -239,37 +243,10 @@ async def __post(self, url, data):
239243
self._request_id = -1
240244
raise InvalidSessionException(error)
241245

242-
# Error in one of the actions
246+
# Unknown error in one of the actions
243247
if error["description"] == XMO_REQUEST_ACTION_ERR:
244-
# pylint:disable=fixme
245-
# TODO How to support multiple actions + error handling?
246-
actions = result["reply"]["actions"]
247-
for action in actions:
248-
action_error = action["error"]
249-
action_error_desc = action_error["description"]
250-
251-
if action_error_desc == XMO_NO_ERR:
252-
continue
253-
254-
if action_error_desc == XMO_AUTHENTICATION_ERR:
255-
raise AuthenticationException(action_error)
256-
257-
if action_error_desc == XMO_ACCESS_RESTRICTION_ERR:
258-
raise AccessRestrictionException(action_error)
259-
260-
if action_error_desc == XMO_NON_WRITABLE_PARAMETER_ERR:
261-
raise NonWritableParameterException(action_error)
262-
263-
if action_error_desc == XMO_UNKNOWN_PATH_ERR:
264-
raise UnknownPathException(action_error)
265-
266-
if action_error_desc == XMO_MAX_SESSION_COUNT_ERR:
267-
raise MaximumSessionCountException(action_error)
268-
269-
if action_error_desc == XMO_LOGIN_RETRY_ERR:
270-
raise LoginRetryErrorException(action_error)
271-
272-
raise UnknownException(action_error)
248+
# leave this to the layer above as there may be multiple actions
249+
pass
273250

274251
return result
275252

@@ -331,7 +308,9 @@ async def login(self):
331308

332309
try:
333310
response = await self.__api_request_async([actions], True)
334-
except TimeoutError as exception:
311+
ActionErrorHandler.throw_if(response)
312+
313+
except asyncio.TimeoutError as exception:
335314
raise LoginTimeoutException(
336315
"Login request timed-out. This could be caused by using the wrong encryption method, or using a (non) SSL connection."
337316
) from exception
@@ -349,7 +328,8 @@ async def logout(self):
349328
"""Log out of the Sagemcom F@st device."""
350329
actions = {"id": 0, "method": "logOut"}
351330

352-
await self.__api_request_async([actions], False)
331+
response = await self.__api_request_async([actions], False)
332+
ActionErrorHandler.throw_if(response)
353333

354334
self._session_id = -1
355335
self._server_nonce = ""
@@ -390,6 +370,12 @@ async def get_encryption_method(self):
390370
on_backoff=retry_login,
391371
)
392372
async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> dict:
373+
async def get_value_by_xpath(
374+
self,
375+
xpath: str,
376+
options: dict | None = None,
377+
suppress_action_errors: bool = False,
378+
) -> dict:
393379
"""Retrieve raw value from router using XPath.
394380
395381
:param xpath: path expression
@@ -403,6 +389,9 @@ async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> d
403389
}
404390

405391
response = await self.__api_request_async([actions], False)
392+
if not suppress_action_errors:
393+
ActionErrorHandler.throw_if(response)
394+
406395
data = self.__get_response_value(response)
407396

408397
return data
@@ -419,6 +408,12 @@ async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> d
419408
on_backoff=retry_login,
420409
)
421410
async def get_values_by_xpaths(self, xpaths, options: dict | None = None) -> dict:
411+
async def get_values_by_xpaths(
412+
self,
413+
xpaths,
414+
options: dict | None = None,
415+
suppress_action_errors: bool = False,
416+
) -> dict:
422417
"""Retrieve raw values from router using XPath.
423418
424419
:param xpaths: Dict of key to xpath expression
@@ -435,6 +430,9 @@ async def get_values_by_xpaths(self, xpaths, options: dict | None = None) -> dic
435430
]
436431

437432
response = await self.__api_request_async(actions, False)
433+
if not suppress_action_errors:
434+
ActionErrorHandler.throw_if(response)
435+
438436
values = [self.__get_response_value(response, i) for i in range(len(xpaths))]
439437
data = dict(zip(xpaths.keys(), values, strict=True))
440438

@@ -467,6 +465,7 @@ async def set_value_by_xpath(self, xpath: str, value: str, options: dict | None
467465
}
468466

469467
response = await self.__api_request_async([actions], False)
468+
ActionErrorHandler.throw_if(response)
470469

471470
return response
472471

@@ -495,7 +494,9 @@ async def get_device_info(self) -> DeviceInfo:
495494
"product_class": "Device/DeviceInfo/ProductClass",
496495
"serial_number": "Device/DeviceInfo/SerialNumber",
497496
"software_version": "Device/DeviceInfo/SoftwareVersion",
498-
}
497+
},
498+
# missing values converted to empty string
499+
suppress_action_errors=True
499500
)
500501
data["manufacturer"] = "Sagemcom"
501502

@@ -562,6 +563,8 @@ async def reboot(self):
562563
}
563564

564565
response = await self.__api_request_async([action], False)
566+
ActionErrorHandler.throw_if(response)
567+
565568
data = self.__get_response_value(response)
566569

567570
return data

0 commit comments

Comments
 (0)