1
1
# -*- coding: utf-8 -*-
2
2
"""Main class for pull data from Devo API (Client)."""
3
- import hmac
3
+ import calendar
4
4
import hashlib
5
+ import hmac
6
+ import json
5
7
import logging
6
8
import os
7
9
import re
8
10
import time
9
- import json
11
+ from datetime import datetime , timedelta
12
+
13
+ import pytz
10
14
import requests
15
+ from requests import JSONDecodeError
16
+
11
17
from devo .common import default_from , default_to
12
18
from .processors import processors , proc_json , \
13
19
json_compact_simple_names , proc_json_compact_simple_to_jobj
14
- import calendar
15
- from datetime import datetime , timedelta
16
- import pytz
17
-
18
20
19
21
CLIENT_DEFAULT_APP_NAME = 'python-sdk-app'
20
22
CLIENT_DEFAULT_USER = 'python-sdk-user'
37
39
"no_endpoint" : "Endpoint 'address' not found" ,
38
40
"to_but_no_from" : "If you use end dates for the query 'to' it is "
39
41
"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" ,
41
44
"wrong_processor" : "processor must be lambda/function or one of the defaults API processors." ,
42
45
"default_keepalive_only" : "Mode '%s' always uses default KeepAlive Token" ,
43
46
"keepalive_not_supported" : "Mode '%s' does not support KeepAlive Token" ,
44
47
"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" ,
47
51
"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"
49
56
}
50
57
51
58
DEFAULT_KEEPALIVE_TOKEN = '\n '
54
61
55
62
56
63
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 )
123
141
124
142
125
143
class ClientConfig :
@@ -169,12 +187,12 @@ def set_processor(self, processor=None):
169
187
try :
170
188
self .processor = processors ()[self .proc ]()
171
189
except KeyError :
172
- raise_exception (f"Processor { self .proc } not found" )
190
+ raise DevoClientException (f"Processor { self .proc } not found" )
173
191
elif isinstance (processor , (type (lambda x : 0 ))):
174
192
self .proc = "CUSTOM"
175
193
self .processor = processor
176
194
else :
177
- raise_exception (ERROR_MSGS ["wrong_processor" ])
195
+ raise DevoClientException (ERROR_MSGS ["wrong_processor" ])
178
196
return True
179
197
180
198
def set_user (self , user = CLIENT_DEFAULT_USER ):
@@ -269,7 +287,7 @@ def __init__(self, address=None, auth=None, config=None,
269
287
270
288
self .auth = auth
271
289
if not address :
272
- raise raise_exception (ERROR_MSGS ['no_endpoint' ])
290
+ raise DevoClientException (ERROR_MSGS ['no_endpoint' ])
273
291
274
292
self .address = self .__get_address_parts (address )
275
293
@@ -418,7 +436,7 @@ def query(self, query=None, query_id=None, dates=None,
418
436
toDate = self ._toDate_parser (fromDate , default_to (dates ['to' ]))
419
437
420
438
if toDate > default_to ("now()" ):
421
- raise raise_exception (ERROR_MSGS ["future_queries_not_supported" ])
439
+ raise DevoClientException (ERROR_MSGS ["future_queries_not_supported" ])
422
440
423
441
self .config .stream = False
424
442
@@ -470,8 +488,8 @@ def _return_string_stream(self, payload):
470
488
first = next (response )
471
489
except StopIteration :
472
490
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
475
493
476
494
if self ._is_correct_response (first ):
477
495
if self .config .proc == SIMPLECOMPACT_TO_OBJ :
@@ -548,7 +566,7 @@ def _make_request(self, payload):
548
566
try :
549
567
response = self .__request (payload )
550
568
if response .status_code != 200 :
551
- raise DevoClientException (response )
569
+ raise DevoClientRequestException (response )
552
570
553
571
if self .config .stream :
554
572
if (self .config .response in ["msgpack" , "xls" ]):
@@ -560,15 +578,12 @@ def _make_request(self, payload):
560
578
except requests .exceptions .ConnectionError as error :
561
579
tries += 1
562
580
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 )))
565
583
except DevoClientException as error :
566
- if isinstance (error , DevoClientException ):
567
- raise_exception (error .args [0 ])
568
- else :
569
- raise_exception (error )
584
+ raise
570
585
except Exception as error :
571
- return raise_exception ( error )
586
+ raise DevoClientException ( ERROR_MSGS [ "other_errors" ]) from error
572
587
573
588
def __request (self , payload ):
574
589
"""
@@ -758,19 +773,18 @@ def _call_jobs(self, address):
758
773
verify = self .verify ,
759
774
timeout = self .timeout )
760
775
except ConnectionError as error :
761
- raise_exception ({ "status" : 404 , "msg" : error })
776
+ raise DevoClientException ( ERROR_MSGS [ "connection_error" ]) from error
762
777
763
778
if response :
764
779
if response .status_code != 200 or \
765
780
"error" in response .text [0 :15 ].lower ():
766
- raise_exception (response .text )
767
- return None
781
+ raise DevoClientRequestException (response )
768
782
try :
769
783
return json .loads (response .text )
770
784
except json .decoder .JSONDecodeError :
771
785
return response .text
772
786
tries += 1
773
- time .sleep (self .retry_delay * (2 ** (tries - 1 )))
787
+ time .sleep (self .retry_delay * (2 ** (tries - 1 )))
774
788
return {}
775
789
776
790
@staticmethod
@@ -893,7 +907,7 @@ def _error_handler(self, content):
893
907
error = match .group (0 )
894
908
code = int (match .group (1 ))
895
909
message = match .group (2 ).strip ()
896
- raise DevoClientException (
910
+ raise DevoClientDataResponseException (
897
911
ERROR_MSGS ["data_query_error" ]
898
912
% message , code = code , cause = error )
899
913
else :
0 commit comments