Skip to content

Commit a5213d9

Browse files
JuanFranDevofelipe-conde-devoalejandro-dotor
authored
Fix/sender exception (#177)
Closes DevoSenderException is throwing an unexpected exception when transforming to string #175 DevoClientException refactoring DevoSenderException refactoring Snyk integration for checking dependencies and static code security pipdeptree dependency open from 2.5.0 to >=2.5.0 Fixed message shown when configuration file (JSON or YAML) is not correct --------- Co-authored-by: Felipe Conde Benavides <felipe.conde@devo.com> Co-authored-by: Alejandro Dotor de Pradas <alejandro.dotordepradas@devo.com>
1 parent 37c551a commit a5213d9

File tree

11 files changed

+272
-225
lines changed

11 files changed

+272
-225
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7-
## [5.0.7] - Unreleased
7+
## [5.1.0] - 2023-03-07
88
### Fixed
9+
* Fixed [exception handling issue](https://github.com/DevoInc/python-sdk/issues/175)
10+
in `devo/sender/data.py`
911
* Fixed message shown when configuration file (JSON or YAML) is not correct
1012
### Added
13+
* Client side exception management refactoring for sending and querying data
1114
* Snyk integration for checking dependencies and static code security
15+
* `pipdeptree` dependency open from ==2.5.0 to >=2.5.0
1216

1317
## [5.0.6] - 2023-02-21
1418
### Chorus

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ reporting a new bug.
2626
To be able to make a fork and the corresponding MR you have to accept Devo's CLA
2727
The process to modify one package or script is the next:
2828

29-
1. Create your fork from release-next
29+
1. Create your fork from `master` git branch.
3030
2. Add to the `CHANGELOG.md`, in
3131
[`Unreleased`](#How_can_I_minimize_the_effort_required?) the tasks
3232
that you are going to take or are carrying out to be able to review at a quick

devo/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
__description__ = 'Devo Python Library.'
22
__url__ = 'http://www.devo.com'
3-
__version__ = "5.0.7"
3+
__version__ = "5.1.0"
44
__author__ = 'Devo'
55
__author_email__ = 'support@devo.com'
66
__license__ = 'MIT'

devo/api/client.py

Lines changed: 109 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
# -*- coding: utf-8 -*-
22
"""Main class for pull data from Devo API (Client)."""
3-
import hmac
3+
import calendar
44
import hashlib
5+
import hmac
6+
import json
57
import logging
68
import os
79
import re
810
import time
9-
import json
11+
from datetime import datetime, timedelta
12+
13+
import pytz
1014
import requests
15+
from requests import JSONDecodeError
16+
1117
from devo.common import default_from, default_to
1218
from .processors import processors, proc_json, \
1319
json_compact_simple_names, proc_json_compact_simple_to_jobj
14-
import calendar
15-
from datetime import datetime, timedelta
16-
import pytz
17-
1820

1921
CLIENT_DEFAULT_APP_NAME = 'python-sdk-app'
2022
CLIENT_DEFAULT_USER = 'python-sdk-user'
@@ -37,15 +39,20 @@
3739
"no_endpoint": "Endpoint 'address' not found",
3840
"to_but_no_from": "If you use end dates for the query 'to' it is "
3941
"necessary to use start date 'from'",
40-
"binary_format_requires_output": "Binary format like `msgpack` and `xls` requires output parameter",
42+
"binary_format_requires_output": "Binary format like `msgpack` and `xls` requires output"
43+
" parameter",
4144
"wrong_processor": "processor must be lambda/function or one of the defaults API processors.",
4245
"default_keepalive_only": "Mode '%s' always uses default KeepAlive Token",
4346
"keepalive_not_supported": "Mode '%s' does not support KeepAlive Token",
4447
"stream_mode_not_supported": "Mode '%s' does not support stream mode",
45-
"future_queries_not_supported": "Modes 'xls' and 'msgpack' does not support future queries because KeepAlive"
46-
" tokens are not available for those resonses type",
48+
"future_queries_not_supported": "Modes 'xls' and 'msgpack' does not support future queries"
49+
" because KeepAlive tokens are not available for those "
50+
"resonses type",
4751
"missing_api_key": "You need a API Key and API secret to make this",
48-
"data_query_error": "Error while receiving query data: %s "
52+
"data_query_error": "Error while receiving query data: %s ",
53+
"connection_error": "Failed to establish a new connection",
54+
"other_errors": "Error while invoking query",
55+
"error_no_detail": "Error code %d while invoking query"
4956
}
5057

5158
DEFAULT_KEEPALIVE_TOKEN = '\n'
@@ -54,72 +61,83 @@
5461

5562

5663
class DevoClientException(Exception):
57-
""" Default Devo Client Exception """
58-
59-
def __init__(self, message, status=None, code=None, cause=None):
60-
if isinstance(message, dict):
61-
self.status = message.get('status', status)
62-
self.cause = message.get('cause', cause)
63-
self.message = message.get('msg',
64-
message if isinstance(message, str)
65-
else json.dumps(message))
66-
self.cid = message.get('cid', None)
67-
self.code = message.get('code', code)
68-
self.timestamp = message.get('timestamp',
69-
time.time_ns() // 1000000)
70-
else:
71-
self.message = message
72-
self.status = status
73-
self.cause = cause
74-
self.cid = None
75-
self.code = code
76-
self.timestamp = time.time_ns() // 1000000
77-
super().__init__(message)
78-
79-
def __str__(self):
80-
return self.message + ((": " + self.cause) if self.cause else '')
81-
82-
83-
def raise_exception(error_data, status=None):
84-
if isinstance(error_data, requests.models.Response):
85-
raise DevoClientException(
86-
_format_error(error_data.json(), status=error_data.status_code))
87-
88-
elif isinstance(error_data, str):
89-
if not status:
90-
raise DevoClientException(
91-
_format_error({"object": error_data}, status=None))
92-
raise DevoClientException(
93-
_format_error({"object": error_data}, status=status))
94-
elif isinstance(error_data, BaseException):
95-
raise DevoClientException(_format_error(error_data, status=None))\
96-
from error_data
97-
else:
98-
raise DevoClientException(_format_error(error_data, status=None))
99-
100-
101-
def _format_error(error, status):
102-
if isinstance(error, dict):
103-
response = {
104-
"msg": error.get("msg", "Error Launching Query"),
105-
"cause": error.get("object") or error.get("context") or error
106-
}
107-
# 'object' may be a list
108-
if isinstance(response["cause"], list):
109-
response["cause"] = ": ".join(response["cause"])
110-
if status:
111-
response['status'] = status
112-
elif 'status' in error:
113-
response['status'] = error['status']
114-
for item in ['code', 'cid', 'timestamp']:
115-
if item in error:
116-
response[item] = error[item]
117-
return response
118-
else:
119-
return {
120-
"msg": str(error),
121-
"cause": str(error)
122-
}
64+
""" Default Devo Client Exception for functionalities
65+
related to querying data to the platform"""
66+
67+
def __init__(self, message: str):
68+
"""
69+
Creates an exception related to query data functionality
70+
71+
:param message: Message describing the exception. It will be
72+
also used as `args` attribute in `Exception`class
73+
"""
74+
self.message = message
75+
"""Message describing exception"""
76+
super().__init__(self.message)
77+
78+
79+
class DevoClientRequestException(DevoClientException):
80+
""" Devo Client Exception that is raised whenever a query data request
81+
is performed and processed but an error is found on server side"""
82+
83+
def __init__(self, response: requests.models.Response):
84+
"""
85+
Creates an exception related bad request of data queries
86+
87+
:param response: A `requests.models.Response` model standing
88+
for the `request` library response for the query data request.
89+
It will be also used as `args` attribute in `Exception`class
90+
"""
91+
self.status = response.status_code
92+
try:
93+
error_response = response.json()
94+
self.message = error_response.get("msg",
95+
error_response.get("error", "Error Launching Query"))
96+
"""Message describing exception"""
97+
if 'code' in error_response:
98+
self.code = error_response['code']
99+
"""Error code `int` as returned by server"""
100+
if 'error' in error_response:
101+
self.cause = error_response.get("error")
102+
"""Cause of error or detailed description as returned by server"""
103+
if 'code' in error_response['error']:
104+
self.code = error_response['error']['code']
105+
if 'message' in error_response['error']:
106+
self.message = error_response['error']['message']
107+
elif 'object' in error_response:
108+
self.message = ": ".join(error_response["object"])
109+
else:
110+
self.cause = error_response
111+
if 'cid' in error_response:
112+
self.cid = error_response['cid']
113+
"""Unique request identifier as assigned by server"""
114+
self.timestamp = error_response.get('timestamp', time.time_ns() // 1000000)
115+
"""Timestamp of the error if returned by server, autogenerated if not"""
116+
except JSONDecodeError as exc:
117+
self.message = ERROR_MSGS["error_no_detail"] % self.status
118+
super().__init__(self.message)
119+
120+
121+
class DevoClientDataResponseException(DevoClientException):
122+
""" Devo Client Exception that is raised after a successful streamed request
123+
whenever an error is found during the processing of an event"""
124+
125+
def __init__(self, message: str, code: int, cause: str):
126+
"""
127+
Creates an exception related to wrong processing of an event of a successful request
128+
129+
:param message: Message describing the exception. It will be
130+
also used as `args` attribute in `Exception`class
131+
:param code: Error code `int` as returned by server
132+
:param cause: Cause of error or detailed description as returned by server
133+
"""
134+
self.message = message
135+
"""Message describing exception"""
136+
self.code = code
137+
"""Error code `int` as returned by server"""
138+
self.cause = cause
139+
"""Cause of error or detailed description as returned by server"""
140+
super().__init__(self.message)
123141

124142

125143
class ClientConfig:
@@ -169,12 +187,12 @@ def set_processor(self, processor=None):
169187
try:
170188
self.processor = processors()[self.proc]()
171189
except KeyError:
172-
raise_exception(f"Processor {self.proc} not found")
190+
raise DevoClientException(f"Processor {self.proc} not found")
173191
elif isinstance(processor, (type(lambda x: 0))):
174192
self.proc = "CUSTOM"
175193
self.processor = processor
176194
else:
177-
raise_exception(ERROR_MSGS["wrong_processor"])
195+
raise DevoClientException(ERROR_MSGS["wrong_processor"])
178196
return True
179197

180198
def set_user(self, user=CLIENT_DEFAULT_USER):
@@ -269,7 +287,7 @@ def __init__(self, address=None, auth=None, config=None,
269287

270288
self.auth = auth
271289
if not address:
272-
raise raise_exception(ERROR_MSGS['no_endpoint'])
290+
raise DevoClientException(ERROR_MSGS['no_endpoint'])
273291

274292
self.address = self.__get_address_parts(address)
275293

@@ -418,7 +436,7 @@ def query(self, query=None, query_id=None, dates=None,
418436
toDate = self._toDate_parser(fromDate, default_to(dates['to']))
419437

420438
if toDate > default_to("now()"):
421-
raise raise_exception(ERROR_MSGS["future_queries_not_supported"])
439+
raise DevoClientException(ERROR_MSGS["future_queries_not_supported"])
422440

423441
self.config.stream = False
424442

@@ -470,8 +488,8 @@ def _return_string_stream(self, payload):
470488
first = next(response)
471489
except StopIteration:
472490
return None # The query did not return any result
473-
except TypeError:
474-
raise_exception(response)
491+
except TypeError as error:
492+
raise DevoClientException(ERROR_MSGS["other_errors"]) from error
475493

476494
if self._is_correct_response(first):
477495
if self.config.proc == SIMPLECOMPACT_TO_OBJ:
@@ -548,7 +566,7 @@ def _make_request(self, payload):
548566
try:
549567
response = self.__request(payload)
550568
if response.status_code != 200:
551-
raise DevoClientException(response)
569+
raise DevoClientRequestException(response)
552570

553571
if self.config.stream:
554572
if (self.config.response in ["msgpack", "xls"]):
@@ -560,15 +578,12 @@ def _make_request(self, payload):
560578
except requests.exceptions.ConnectionError as error:
561579
tries += 1
562580
if tries > self.retries:
563-
return raise_exception(error)
564-
time.sleep(self.retry_delay * (2 ** (tries-1)))
581+
raise DevoClientException(ERROR_MSGS["connection_error"]) from error
582+
time.sleep(self.retry_delay * (2 ** (tries - 1)))
565583
except DevoClientException as error:
566-
if isinstance(error, DevoClientException):
567-
raise_exception(error.args[0])
568-
else:
569-
raise_exception(error)
584+
raise
570585
except Exception as error:
571-
return raise_exception(error)
586+
raise DevoClientException(ERROR_MSGS["other_errors"]) from error
572587

573588
def __request(self, payload):
574589
"""
@@ -758,19 +773,18 @@ def _call_jobs(self, address):
758773
verify=self.verify,
759774
timeout=self.timeout)
760775
except ConnectionError as error:
761-
raise_exception({"status": 404, "msg": error})
776+
raise DevoClientException(ERROR_MSGS["connection_error"]) from error
762777

763778
if response:
764779
if response.status_code != 200 or \
765780
"error" in response.text[0:15].lower():
766-
raise_exception(response.text)
767-
return None
781+
raise DevoClientRequestException(response)
768782
try:
769783
return json.loads(response.text)
770784
except json.decoder.JSONDecodeError:
771785
return response.text
772786
tries += 1
773-
time.sleep(self.retry_delay * (2 ** (tries-1)))
787+
time.sleep(self.retry_delay * (2 ** (tries - 1)))
774788
return {}
775789

776790
@staticmethod
@@ -893,7 +907,7 @@ def _error_handler(self, content):
893907
error = match.group(0)
894908
code = int(match.group(1))
895909
message = match.group(2).strip()
896-
raise DevoClientException(
910+
raise DevoClientDataResponseException(
897911
ERROR_MSGS["data_query_error"]
898912
% message, code=code, cause=error)
899913
else:

0 commit comments

Comments
 (0)