Skip to content

Commit d6db528

Browse files
authored
Sender error improvements (#158)
* feat(PAR-6556): not working yet * feat(PAR-6556): Improve sender feedback * fix: Improve some messaging
1 parent 1202d52 commit d6db528

File tree

8 files changed

+218
-115
lines changed

8 files changed

+218
-115
lines changed

Diff for: CHANGELOG.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ 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.0] - 2022-11-29
7+
## [5.0.0] - 2022-12-02
88
### Added
9-
* `DevoClientException` refactored for better details extraction
10-
* In query error detection and feedback through detailed `DevoClientException`
9+
* `DevoClientException` and `DevoSenderException` refactored for better details extraction
10+
* In query error detection and feedback through detailed `DevoClientException`
11+
* New test selection in `run_tests.py` tool (included and excluded parameter)
1112
### Removed
1213
* Parameter `key` removed from `devo.sender.lookup.Lookup.send_data_line`. The `key` parameter pointed to the value in the `fields` list that was the key of the lookup item. When the value appeared several times in `fields`, the one expected to be the key cannot be identified. This parameter was set as deprecated since version 3.4.0 (2020-08-06)
1314
### Changed

Diff for: devo/api/client.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,15 @@
3737
"no_endpoint": "Endpoint 'address' not found",
3838
"to_but_no_from": "If you use end dates for the query 'to' it is "
3939
"necessary to use start date 'from'",
40-
"binary_format_requires_output": "Binary format like `msgpack` and `xls` "
41-
"requires output parameter"
40+
"binary_format_requires_output": "Binary format like `msgpack` and `xls` requires output parameter",
41+
"wrong_processor": "processor must be lambda/function or one of the defaults API processors.",
42+
"default_keepalive_only": "Mode '%s' always uses default KeepAlive Token",
43+
"keepalive_not_supported": "Mode '%s' does not support KeepAlive Token",
44+
"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",
47+
"missing_api_key": "You need a API Key and API secret to make this",
48+
"data_query_error": "Error while receiving query data: %s "
4249
}
4350

4451
DEFAULT_KEEPALIVE_TOKEN = '\n'
@@ -164,8 +171,7 @@ def set_processor(self, processor=None):
164171
self.proc = "CUSTOM"
165172
self.processor = processor
166173
else:
167-
raise_exception("processor must be lambda/function or one of"
168-
"the defaults API processors.")
174+
raise_exception(ERROR_MSGS["wrong_processor"])
169175
return True
170176

171177
def set_user(self, user=CLIENT_DEFAULT_USER):
@@ -203,8 +209,7 @@ def set_keepalive_token(self, keepAliveToken=DEFAULT_KEEPALIVE_TOKEN):
203209
if keepAliveToken not in [NO_KEEPALIVE_TOKEN,
204210
DEFAULT_KEEPALIVE_TOKEN]:
205211
logging.warning(
206-
f"Mode '{self.response}' always uses default KeepAlive"
207-
f" Token")
212+
ERROR_MSGS["default_keepalive_only"] % self.response)
208213
# In the cases 'csv', 'tsv' you can use any value passed in
209214
# 'keepAliveToken'.
210215
elif self.response in ['csv', 'tsv']:
@@ -213,7 +218,7 @@ def set_keepalive_token(self, keepAliveToken=DEFAULT_KEEPALIVE_TOKEN):
213218
if keepAliveToken not in [NO_KEEPALIVE_TOKEN,
214219
DEFAULT_KEEPALIVE_TOKEN]:
215220
logging.warning(
216-
f"Mode '{self.response}' does not support KeepAlive Token")
221+
ERROR_MSGS["keepalive_not_supported"] % self.response)
217222
self.keepAliveToken = NO_KEEPALIVE_TOKEN
218223
return True
219224

@@ -399,8 +404,7 @@ def query(self, query=None, query_id=None, dates=None,
399404
if not dates['to']:
400405
dates['to'] = "now()"
401406
if self.config.stream:
402-
logging.warning(f"Mode '{self.config.response}' does not"
403-
f" support stream mode")
407+
logging.warning(ERROR_MSGS["stream_mode_not_supported"] % self.config.response)
404408
# If is a future query and response type is 'xls' or 'msgpack'
405409
# return warning because is not available.
406410
if self._future_queries_available(self.config.response):
@@ -411,10 +415,7 @@ def query(self, query=None, query_id=None, dates=None,
411415
toDate = self._toDate_parser(fromDate, default_to(dates['to']))
412416

413417
if toDate > default_to("now()"):
414-
raise raise_exception(
415-
"Modes 'xls' and 'msgpack' does not support future "
416-
"queries because KeepAlive tokens are not available "
417-
"for those resonses type")
418+
raise raise_exception(ERROR_MSGS["future_queries_not_supported"])
418419

419420
self.config.stream = False
420421

@@ -676,8 +677,7 @@ def _get_sign(self, data, tstamp):
676677
"""
677678
if not self.auth.get("key", False) \
678679
or not self.auth.get("secret", False):
679-
raise DevoClientException(("You need a API Key and "
680-
"API secret to make this"))
680+
raise DevoClientException(ERROR_MSGS["missing_api_key"])
681681
sign = hmac.new(self.auth.get("secret").encode("utf-8"),
682682
(self.auth.get("key") + data + tstamp).encode("utf-8"),
683683
hashlib.sha256)
@@ -891,7 +891,7 @@ def _error_handler(self, content):
891891
code = int(match.group(1))
892892
message = match.group(2).strip()
893893
raise DevoClientException(
894-
"Error while receiving query data: %s "
894+
ERROR_MSGS["data_query_error"]
895895
% message, code=code, cause=error)
896896
else:
897897
return content

Diff for: devo/sender/data.py

+65-40
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
import time
1010
import zlib
11+
from enum import Enum
1112
from pathlib import Path
1213
from _socket import SHUT_RDWR
1314

@@ -21,16 +22,25 @@
2122
PYPY = hasattr(sys, 'pypy_version_info')
2223

2324

24-
ERROR_MSGS = {
25-
"no_query": "Error: Not query provided.",
26-
"no_auth": "Client dont have key&secret or auth token/jwt",
27-
"no_endpoint": "Endpoint 'url' not found"
28-
}
25+
class ERROR_MSGS(str, Enum):
26+
WRONG_FILE_TYPE = "'%s' is not a valid type to be opened as a file"
27+
ADDRESS_TUPLE = "Devo-SenderConfigSSL| address must be a tuple (\"hostname\", int(port))'",
28+
WRONG_SSL_CONFIG = "Devo-SenderConfigSSL|Can't create SSL config: %s",
29+
CONFIG_FILE_NOT_FOUND = "Error in the configuration, %s is not a file or the path does not exist",
30+
CANT_READ_CONFIG_FILE = "Error in the configuration %s can't be read\noriginal error: %s",
31+
CONFIG_FILE_PROBLEM = "Error in the configuration, %s problem related to: %s"
2932

3033

3134
class DevoSenderException(Exception):
3235
""" Default Devo Sender Exception """
3336

37+
def __init__(self, message):
38+
self.message = message
39+
super().__init__(message)
40+
41+
def __str__(self):
42+
return self.message
43+
3444

3545
class SenderConfigSSL:
3646
"""
@@ -55,13 +65,12 @@ class SenderConfigSSL:
5565
Sender
5666
5767
"""
68+
5869
def __init__(self, address=None, key=None, cert=None, chain=None,
5970
pkcs=None, sec_level=None, check_hostname=True,
6071
verify_mode=None, verify_config=False):
6172
if not isinstance(address, tuple):
62-
raise DevoSenderException(
63-
"Devo-SenderConfigSSL| address must be a tuple "
64-
"'(\"hostname\", int(port))'")
73+
raise DevoSenderException(ERROR_MSGS.ADDRESS_TUPLE)
6574
try:
6675
self.address = address
6776
self.key = key
@@ -75,8 +84,7 @@ def __init__(self, address=None, key=None, cert=None, chain=None,
7584
self.verify_mode = verify_mode
7685
except Exception as error:
7786
raise DevoSenderException(
78-
"Devo-SenderConfigSSL|Can't create SSL config: "
79-
"%s" % str(error))
87+
ERROR_MSGS.WRONG_SSL_CONFIG % str(error)) from error
8088

8189
if self.verify_config:
8290
self.check_config_files_path()
@@ -94,23 +102,20 @@ def check_config_files_path(self):
94102
certificates = [self.key, self.chain, self.cert]
95103
for file in certificates:
96104
try:
97-
if not Path(file).is_file():
105+
if not (file.is_file() if isinstance(file, Path) else Path(
106+
file).is_file()):
98107
raise DevoSenderException(
99-
"Error in the configuration, "
100-
+ file +
101-
" is not a file or the path does not exist")
108+
ERROR_MSGS.CONFIG_FILE_NOT_FOUND % file)
102109
except IOError as message:
103110
if message.errno == errno.EACCES:
104111
raise DevoSenderException(
105-
"Error in the configuration "
106-
+ file + " can't be read" +
107-
"\noriginal error: " +
108-
str(message))
112+
ERROR_MSGS.CANT_READ_CONFIG_FILE % (
113+
file, str(message))) \
114+
from message
109115
else:
110116
raise DevoSenderException(
111-
"Error in the configuration, "
112-
+ file + " problem related to: " + str(message))
113-
117+
ERROR_MSGS.CONFIG_FILE_PROBLEM % (file, str(message))) \
118+
from message
114119
return True
115120

116121
def check_config_certificate_key(self):
@@ -121,8 +126,8 @@ def check_config_certificate_key(self):
121126
:return: Boolean true or raises an exception
122127
"""
123128

124-
with open(self.cert, "rb") as certificate_file, \
125-
open(self.key, "rb") as key_file:
129+
with open_file(self.cert, mode="rb") as certificate_file, \
130+
open_file(self.key, mode="rb") as key_file:
126131

127132
certificate_raw = certificate_file.read()
128133
key_raw = key_file.read()
@@ -139,7 +144,7 @@ def check_config_certificate_key(self):
139144
raise DevoSenderException(
140145
"Error in the configuration, the key: " + self.key +
141146
" is not compatible with the cert: " + self.cert +
142-
"\noriginal error: " + str(message))
147+
"\noriginal error: " + str(message)) from message
143148
return True
144149

145150
def check_config_certificate_chain(self):
@@ -149,8 +154,8 @@ def check_config_certificate_chain(self):
149154
150155
:return: Boolean true or raises an exception
151156
"""
152-
with open(self.cert, "rb") as certificate_file, \
153-
open(self.chain, "rb") as chain_file:
157+
with open_file(self.cert, mode="rb") as certificate_file, \
158+
open_file(self.chain, mode="rb") as chain_file:
154159

155160
certificate_raw = certificate_file.read()
156161
chain_raw = chain_file.read()
@@ -169,7 +174,7 @@ def check_config_certificate_chain(self):
169174
raise DevoSenderException(
170175
"Error in config, the chain: " + self.chain +
171176
" is not compatible with the certificate: " + self.cert +
172-
"\noriginal error: " + str(message))
177+
"\noriginal error: " + str(message)) from message
173178
return True
174179

175180
def check_config_certificate_address(self):
@@ -190,18 +195,18 @@ def check_config_certificate_address(self):
190195
raise DevoSenderException(
191196
"Possible error in config, a timeout could be related " +
192197
"to an incorrect address/port: " + str(self.address) +
193-
"\noriginal error: " + str(message))
198+
"\noriginal error: " + str(message)) from message
194199
except ConnectionRefusedError as message:
195200
raise DevoSenderException(
196201
"Error in config, incorrect address/port: "
197202
+ str(self.address) +
198-
"\noriginal error: " + str(message))
203+
"\noriginal error: " + str(message)) from message
199204
sock.setblocking(True)
200205
connection.do_handshake()
201206
server_chain = connection.get_peer_cert_chain()
202207
connection.close()
203208

204-
with open(self.chain, "rb") as chain_file:
209+
with open_file(self.chain, mode="rb") as chain_file:
205210
chain = chain_file.read()
206211
chain_certs = []
207212
for _ca in pem.parse(chain):
@@ -226,15 +231,15 @@ def check_config_certificate_address(self):
226231
def get_common_names(cert_chain, components_type):
227232
result = set()
228233
for temp_cert in cert_chain:
229-
for key, value in getattr(temp_cert, components_type)()\
234+
for key, value in getattr(temp_cert, components_type)() \
230235
.get_components():
231236
if key.decode("utf-8") == "CN":
232237
result.add(value)
233238
return result
234239

235240
@staticmethod
236241
def fake_get_peer_cert_chain(chain):
237-
with open(chain, "rb") as chain_file:
242+
with open_file(chain, mode="rb") as chain_file:
238243
chain_certs = []
239244
for _ca in pem.parse(chain_file.read()):
240245
chain_certs.append(
@@ -266,11 +271,12 @@ def __init__(self, address=None):
266271
except Exception as error:
267272
raise DevoSenderException(
268273
"DevoSenderConfigTCP|Can't create TCP config: "
269-
"%s" % str(error))
274+
"%s" % str(error)) from error
270275

271276

272277
class SenderBuffer:
273278
"""Micro class for buffer values"""
279+
274280
def __init__(self):
275281
self.length = 19500
276282
self.compression_level = -1
@@ -289,6 +295,7 @@ class Sender(logging.Handler):
289295
:param debug: For more info in console/logger output
290296
:param logger: logger. Default sys.console
291297
"""
298+
292299
def __init__(self, config=None, con_type=None,
293300
timeout=30, debug=False, logger=None):
294301
if config is None:
@@ -312,7 +319,7 @@ def __init__(self, config=None, con_type=None,
312319
self.logger = logger if logger else \
313320
get_log(handler=get_stream_handler(
314321
msg_format='%(asctime)s|%(levelname)s|Devo-Sender|%(message)s')
315-
)
322+
)
316323

317324
self._sender_config = config
318325

@@ -347,7 +354,8 @@ def __connect_tcp_socket(self):
347354
except socket.error as error:
348355
self.close()
349356
raise DevoSenderException(
350-
"TCP conn establishment socket error: %s" % str(error))
357+
"TCP conn establishment socket error: %s" % str(
358+
error)) from error
351359

352360
self.timestart = int(round(time.time() * 1000))
353361

@@ -373,7 +381,7 @@ def __connect_ssl(self):
373381
self.close()
374382
raise DevoSenderException(
375383
"PFX Certificate read failed: %s" %
376-
str(error))
384+
str(error)) from error
377385
try:
378386
try:
379387
if self._sender_config.key is not None \
@@ -418,7 +426,7 @@ def __connect_ssl(self):
418426
self.close()
419427
raise DevoSenderException(
420428
"SSL conn establishment socket error: %s" %
421-
str(error))
429+
str(error)) from error
422430

423431
def info(self, msg):
424432
"""
@@ -531,7 +539,7 @@ def __encode_multiline(record):
531539
record = Sender.__encode_record(record)
532540
return b'%d %s' % (len(record), record)
533541
except Exception as error:
534-
raise DevoSenderException(error)
542+
raise DevoSenderException(error) from error
535543

536544
@staticmethod
537545
def __encode_record(record):
@@ -590,14 +598,14 @@ def send_raw(self, record, multiline=False, zip=False):
590598
except socket.error:
591599
self.close()
592600
raise DevoSenderException(
593-
"Socket error: %s" % str(socket.error))
601+
"Socket error: %s" % str(socket.error)) from error
594602
finally:
595603
if self.debug:
596604
self.logger.debug('sent|%d|size|%d|msg|%s' %
597605
(sent, len(record), record))
598606
raise Exception("Socket cant connect: unknown error")
599607
except Exception as error:
600-
raise DevoSenderException(error)
608+
raise DevoSenderException(error) from error
601609

602610
@staticmethod
603611
def compose_mem(tag, **kwargs):
@@ -810,3 +818,20 @@ def emit(self, record):
810818
severity=severity)
811819
except Exception:
812820
self.handleError(record)
821+
822+
823+
def open_file(file, mode='r', encoding='utf-8'):
824+
"""
825+
Helper class to open file whenever is provided as `Path` or `str` type
826+
:param file File to open
827+
:param mode Opening mode
828+
:param encoding Encoding of content
829+
"""
830+
if isinstance(file, Path):
831+
return file.open(mode=mode,
832+
encoding=encoding if not mode.endswith('b') else None)
833+
elif isinstance(file, str):
834+
return open(file, mode=mode,
835+
encoding=encoding if not mode.endswith('b') else None)
836+
else:
837+
raise DevoSenderException(ERROR_MSGS.WRONG_FILE_TYPE % str(type(file)))

0 commit comments

Comments
 (0)