11# -*- coding: utf-8 -*-
22"""Main class for pull data from Devo API (Client)."""
3- import hmac
3+ import calendar
44import hashlib
5+ import hmac
6+ import json
57import logging
68import os
79import re
810import time
9- import json
11+ from datetime import datetime , timedelta
12+
13+ import pytz
1014import requests
15+ from requests import JSONDecodeError
16+
1117from devo .common import default_from , default_to
1218from .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
1921CLIENT_DEFAULT_APP_NAME = 'python-sdk-app'
2022CLIENT_DEFAULT_USER = 'python-sdk-user'
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
5158DEFAULT_KEEPALIVE_TOKEN = '\n '
5461
5562
5663class 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
125143class 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