Skip to content

Commit 7d1b9c6

Browse files
committed
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 1f9c3cc commit 7d1b9c6

File tree

2 files changed

+120
-44
lines changed

2 files changed

+120
-44
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
68+
if action_error_desc == XMO_AUTHENTICATION_ERR:
69+
return AuthenticationException(action_error)
70+
71+
if action_error_desc == XMO_ACCESS_RESTRICTION_ERR:
72+
return AccessRestrictionException(action_error)
73+
74+
if action_error_desc == XMO_NON_WRITABLE_PARAMETER_ERR:
75+
return NonWritableParameterException(action_error)
76+
77+
if action_error_desc == XMO_UNKNOWN_PATH_ERR:
78+
return UnknownPathException(action_error)
79+
80+
if action_error_desc == XMO_MAX_SESSION_COUNT_ERR:
81+
return MaximumSessionCountException(action_error)
82+
83+
if action_error_desc == XMO_LOGIN_RETRY_ERR:
84+
return LoginRetryErrorException(action_error)
85+
86+
return UnknownException(action_error)

sagemcom_api/client.py

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,25 @@
2222
)
2323
import backoff
2424
import humps
25+
from .action_error_exception_handler import ActionErrorHandler
2526

2627
from .const import (
2728
API_ENDPOINT,
2829
DEFAULT_TIMEOUT,
2930
DEFAULT_USER_AGENT,
3031
UINT_MAX,
31-
XMO_ACCESS_RESTRICTION_ERR,
32-
XMO_AUTHENTICATION_ERR,
3332
XMO_INVALID_SESSION_ERR,
34-
XMO_LOGIN_RETRY_ERR,
35-
XMO_MAX_SESSION_COUNT_ERR,
3633
XMO_NO_ERR,
37-
XMO_NON_WRITABLE_PARAMETER_ERR,
3834
XMO_REQUEST_ACTION_ERR,
3935
XMO_REQUEST_NO_ERR,
40-
XMO_UNKNOWN_PATH_ERR,
4136
)
4237
from .enums import EncryptionMethod
4338
from .exceptions import (
44-
AccessRestrictionException,
4539
AuthenticationException,
4640
BadRequestException,
4741
InvalidSessionException,
4842
LoginRetryErrorException,
4943
LoginTimeoutException,
50-
MaximumSessionCountException,
51-
NonWritableParameterException,
5244
UnauthorizedException,
5345
UnknownException,
5446
UnknownPathException,
@@ -201,8 +193,19 @@ def __get_response(self, response, index=0):
201193

202194
return value
203195

204-
def __get_response_value(self, response, index=0):
196+
def __get_response_value(self, response, index=0, throw_on_action_error = False):
205197
"""Retrieve response value from value."""
198+
if throw_on_action_error:
199+
try:
200+
error = response["reply"]["actions"][index]["error"]
201+
except (KeyError, IndexError):
202+
error = None
203+
204+
if error is not None:
205+
error_description = error["description"]
206+
if error_description != XMO_NO_ERR:
207+
raise ActionErrorHandler.from_error_description(error, error_description)
208+
206209
try:
207210
value = self.__get_response(response, index)["value"]
208211
except KeyError:
@@ -251,37 +254,10 @@ async def __post(self, url, data):
251254
self._request_id = -1
252255
raise InvalidSessionException(error)
253256

254-
# Error in one of the actions
257+
# Unknown error in one of the actions
255258
if error["description"] == XMO_REQUEST_ACTION_ERR:
256-
# pylint:disable=fixme
257-
# TODO How to support multiple actions + error handling?
258-
actions = result["reply"]["actions"]
259-
for action in actions:
260-
action_error = action["error"]
261-
action_error_desc = action_error["description"]
262-
263-
if action_error_desc == XMO_NO_ERR:
264-
continue
265-
266-
if action_error_desc == XMO_AUTHENTICATION_ERR:
267-
raise AuthenticationException(action_error)
268-
269-
if action_error_desc == XMO_ACCESS_RESTRICTION_ERR:
270-
raise AccessRestrictionException(action_error)
271-
272-
if action_error_desc == XMO_NON_WRITABLE_PARAMETER_ERR:
273-
raise NonWritableParameterException(action_error)
274-
275-
if action_error_desc == XMO_UNKNOWN_PATH_ERR:
276-
raise UnknownPathException(action_error)
277-
278-
if action_error_desc == XMO_MAX_SESSION_COUNT_ERR:
279-
raise MaximumSessionCountException(action_error)
280-
281-
if action_error_desc == XMO_LOGIN_RETRY_ERR:
282-
raise LoginRetryErrorException(action_error)
283-
284-
raise UnknownException(action_error)
259+
# leave this to the layer above as there may be multiple actions
260+
pass
285261

286262
return result
287263

@@ -344,6 +320,8 @@ async def login(self):
344320

345321
try:
346322
response = await self.__api_request_async([actions], True)
323+
ActionErrorHandler.throw_if(response)
324+
347325
except asyncio.TimeoutError as exception:
348326
raise LoginTimeoutException(
349327
"Login request timed-out. This could be caused by using the wrong encryption method, or using a (non) SSL connection."
@@ -362,7 +340,8 @@ async def logout(self):
362340
"""Log out of the Sagemcom F@st device."""
363341
actions = {"id": 0, "method": "logOut"}
364342

365-
await self.__api_request_async([actions], False)
343+
response = await self.__api_request_async([actions], False)
344+
ActionErrorHandler.throw_if(response)
366345

367346
self._session_id = -1
368347
self._server_nonce = ""
@@ -404,7 +383,7 @@ async def get_encryption_method(self):
404383
max_tries=1,
405384
on_backoff=retry_login,
406385
)
407-
async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> dict:
386+
async def get_value_by_xpath(self, xpath: str, options: dict | None = None, suppress_action_errors = False) -> dict:
408387
"""
409388
Retrieve raw value from router using XPath.
410389
@@ -419,6 +398,9 @@ async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> d
419398
}
420399

421400
response = await self.__api_request_async([actions], False)
401+
if not suppress_action_errors:
402+
ActionErrorHandler.throw_if(response)
403+
422404
data = self.__get_response_value(response)
423405

424406
return data
@@ -434,7 +416,7 @@ async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> d
434416
max_tries=1,
435417
on_backoff=retry_login,
436418
)
437-
async def get_values_by_xpaths(self, xpaths, options: dict | None = None) -> dict:
419+
async def get_values_by_xpaths(self, xpaths, options: dict | None = None, suppress_action_errors = False) -> dict:
438420
"""
439421
Retrieve raw values from router using XPath.
440422
@@ -452,6 +434,9 @@ async def get_values_by_xpaths(self, xpaths, options: dict | None = None) -> dic
452434
]
453435

454436
response = await self.__api_request_async(actions, False)
437+
if not suppress_action_errors:
438+
ActionErrorHandler.throw_if(response)
439+
455440
values = [self.__get_response_value(response, i) for i in range(len(xpaths))]
456441
data = dict(zip(xpaths.keys(), values))
457442

@@ -487,6 +472,7 @@ async def set_value_by_xpath(
487472
}
488473

489474
response = await self.__api_request_async([actions], False)
475+
ActionErrorHandler.throw_if(response)
490476

491477
return response
492478

@@ -515,7 +501,9 @@ async def get_device_info(self) -> DeviceInfo:
515501
"product_class": "Device/DeviceInfo/ProductClass",
516502
"serial_number": "Device/DeviceInfo/SerialNumber",
517503
"software_version": "Device/DeviceInfo/SoftwareVersion",
518-
}
504+
},
505+
# missing values converted to empty string
506+
suppress_action_errors=True
519507
)
520508
data["manufacturer"] = "Sagemcom"
521509

@@ -584,6 +572,8 @@ async def reboot(self):
584572
}
585573

586574
response = await self.__api_request_async([action], False)
575+
ActionErrorHandler.throw_if(response)
576+
587577
data = self.__get_response_value(response)
588578

589579
return data

0 commit comments

Comments
 (0)