From 16fb9810a2d665c76cef05997476693f0eeb3fee Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 20 Aug 2019 19:39:14 +0200 Subject: [PATCH 01/45] Test for retry on connection problems --- tests/test_errorclient.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index ea5942a..9c25dce 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -1,12 +1,15 @@ +import json +import mock import sys import unittest +import requests + from httmock import urlmatch, HTTMock, response from pymod import ArgoMessagingService from pymod import AmsMessage from pymod import AmsTopic from pymod import AmsSubscription -from pymod import AmsServiceException, AmsException -import json +from pymod import AmsServiceException, AmsConnectionException, AmsException from .amsmocks import ErrorMocks from .amsmocks import TopicMocks @@ -116,7 +119,23 @@ def error_unauth(url, request): self.assertEqual(e.msg, 'While trying the [topic_get]: Unauthorized') self.assertEqual(e.status, 'UNAUTHORIZED') - + @mock.patch('pymod.ams.requests.get') + def testRetryConnection(self, mock_requests_get): + mock_response = mock.create_autospec(requests.Response) + mock_requests_get.side_effect = [requests.exceptions.ConnectionError, + requests.exceptions.ConnectionError, + requests.exceptions.ConnectionError, + requests.exceptions.ConnectionError] + retry = 3 + retrysleep = 0.1 + self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep) + self.assertRaises(AmsConnectionException, self.ams.list_topics) + self.assertEqual(mock_requests_get.call_count, retry + 1) + + # mock_response2 = mock.create_autospec(requests.Response) + # mock_response2.status_code = 408 + # mock_requests_get.return_value = mock_response2 + # r = self.ams.list_topics() if __name__ == '__main__': From 4af644b74b131daaf2a30f2811f6d20f106e5ed9 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 21 Aug 2019 11:13:07 +0200 Subject: [PATCH 02/45] Initial test for AMS Timeout --- tests/test_errorclient.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index 9c25dce..954463c 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -120,8 +120,7 @@ def error_unauth(url, request): self.assertEqual(e.status, 'UNAUTHORIZED') @mock.patch('pymod.ams.requests.get') - def testRetryConnection(self, mock_requests_get): - mock_response = mock.create_autospec(requests.Response) + def testRetryConnectionProblems(self, mock_requests_get): mock_requests_get.side_effect = [requests.exceptions.ConnectionError, requests.exceptions.ConnectionError, requests.exceptions.ConnectionError, @@ -132,10 +131,15 @@ def testRetryConnection(self, mock_requests_get): self.assertRaises(AmsConnectionException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) - # mock_response2 = mock.create_autospec(requests.Response) - # mock_response2.status_code = 408 - # mock_requests_get.return_value = mock_response2 - # r = self.ams.list_topics() + @mock.patch('pymod.ams.requests.get') + def testRetryConnectionAmsTimeout(self, mock_requests_get): + mock_response = mock.create_autospec(requests.Response) + mock_response.status_code = 408 + mock_response.content = '{"error": {"code": 408, \ + "message": "Ams Timeout", \ + "status": "TIMEOUT"}}' + mock_requests_get.return_value = mock_response + r = self.ams.list_topics() if __name__ == '__main__': From aa1faba54b6855e1d6389791fd52d8c90fe08b00 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 22 Aug 2019 14:53:46 +0200 Subject: [PATCH 03/45] Added general mock library for tox testing --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 712dc20..ecfcf33 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py27-requests0, py27-requests260, py34-requests2123, py36-requests0, p deps = coverage pytest httmock + mock requests0: requests requests2123: requests==2.12.3 requests260: requests==2.6.0 From e53102483e1e911ddc8d30d5eee411f02901dc28 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Fri, 30 Aug 2019 13:20:35 +0200 Subject: [PATCH 04/45] Retry also on socket.error exceptions raised on SIGPIPE errors --- pymod/ams.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pymod/ams.py b/pymod/ams.py index 1fb6a10..8e37b44 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -1,5 +1,6 @@ import json import requests +import socket import sys import datetime from .amsexceptions import AmsServiceException, AmsConnectionException, AmsMessageException, AmsException @@ -101,7 +102,7 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): 'message': content}} raise AmsServiceException(json=errormsg, request=route_name) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, socket.error) as e: raise AmsConnectionException(e, route_name) else: From 1677480a001930491d2c34e482dd914a3ab2a9bb Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 3 Sep 2019 19:09:49 +0200 Subject: [PATCH 05/45] Retry on AMS Timeout --- pymod/ams.py | 3 +++ tests/test_errorclient.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pymod/ams.py b/pymod/ams.py index 8e37b44..11653f0 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -87,6 +87,9 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): decoded = json.loads(content) if content else {} raise AmsServiceException(json=decoded, request=route_name) + elif status_code == 408: + raise requests.exceptions.Timeout + # JSON error returned by AMS elif status_code != 200 and status_code in self.errors_route[route_name][1]: decoded = json.loads(content) if content else {} diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index 954463c..c98d1b5 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -139,7 +139,11 @@ def testRetryConnectionAmsTimeout(self, mock_requests_get): "message": "Ams Timeout", \ "status": "TIMEOUT"}}' mock_requests_get.return_value = mock_response - r = self.ams.list_topics() + retry = 3 + retrysleep = 0.1 + self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep) + self.assertRaises(AmsConnectionException, self.ams.list_topics) + self.assertEqual(mock_requests_get.call_count, retry + 1) if __name__ == '__main__': From 2459506ca11a567b41e1f38df6e17ba0d5d8a933 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 5 Sep 2019 11:50:05 +0200 Subject: [PATCH 06/45] Method attributes renamed in Py3 --- tests/test_errorclient.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index c98d1b5..95e129d 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -127,7 +127,10 @@ def testRetryConnectionProblems(self, mock_requests_get): requests.exceptions.ConnectionError] retry = 3 retrysleep = 0.1 - self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep) + if sys.version_info < (3, ): + self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep) + else: + self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep) self.assertRaises(AmsConnectionException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) @@ -141,7 +144,10 @@ def testRetryConnectionAmsTimeout(self, mock_requests_get): mock_requests_get.return_value = mock_response retry = 3 retrysleep = 0.1 - self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep) + if sys.version_info < (3, ): + self.ams._retry_make_request.im_func.__defaults__ = (None, None, retry, retrysleep) + else: + self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep) self.assertRaises(AmsConnectionException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) From 7b3d56d73ffb92bd44c5493a23c43bf643b66fec Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 5 Sep 2019 18:19:46 +0200 Subject: [PATCH 07/45] Refactorings with introduced AmsTimeoutException --- pymod/__init__.py | 4 +++- pymod/ams.py | 36 ++++++++++++++++++++++++++++++++++-- pymod/amsexceptions.py | 9 +++++++++ tests/test_errorclient.py | 4 ++-- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/pymod/__init__.py b/pymod/__init__.py index 801120f..2a021ea 100644 --- a/pymod/__init__.py +++ b/pymod/__init__.py @@ -8,7 +8,9 @@ def emit(self, record): logging.getLogger(__name__).addHandler(NullHandler()) from .ams import ArgoMessagingService -from .amsexceptions import (AmsServiceException, AmsConnectionException, AmsMessageException, AmsException) +from .amsexceptions import (AmsServiceException, AmsConnectionException, + AmsTimeoutException, AmsMessageException, + AmsException) from .amsmsg import AmsMessage from .amstopic import AmsTopic from .amssubscription import AmsSubscription diff --git a/pymod/ams.py b/pymod/ams.py index 11653f0..f043528 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -2,8 +2,16 @@ import requests import socket import sys +<<<<<<< HEAD import datetime from .amsexceptions import AmsServiceException, AmsConnectionException, AmsMessageException, AmsException +======= +import time + +from .amsexceptions import (AmsServiceException, AmsConnectionException, + AmsMessageException, AmsException, + AmsTimeoutException) +>>>>>>> Refactorings with introduced AmsTimeoutException from .amsmsg import AmsMessage from .amstopic import AmsTopic from .amssubscription import AmsSubscription @@ -56,9 +64,32 @@ def __init__(self): "topic_publish": ["post", set([413, 401, 403])], "sub_pushconfig": ["post", set([400, 401, 403, 404])], "auth_x509": ["post", set([400, 401, 403, 404])], +<<<<<<< HEAD "sub_pull": ["post", set([400, 401, 403, 404])], "sub_timeToOffset": ["get", set([400, 401, 403, 404, 409])] } +======= + "sub_pull": ["post", set([400, 401, 403, 404])]} + + def _retry_make_request(self, url, body=None, route_name=None, retry=3, retrysleep=60, **reqkwargs): + i = 1 + timeout = reqkwargs.get('timeout', 0) + + while i <= retry + 1: + try: + return self._make_request(url, body, route_name, **reqkwargs) + except (AmsConnectionException, AmsTimeoutException) as e: + if i == retry + 1: + raise e + else: + time.sleep(retrysleep) + if timeout: + log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, retrysleep, timeout)) + else: + log.warning('Retry #{0} after {1} seconds'.format(i, retrysleep)) + finally: + i += 1 +>>>>>>> Refactorings with introduced AmsTimeoutException def _make_request(self, url, body=None, route_name=None, **reqkwargs): """Common method for PUT, GET, POST HTTP requests with appropriate @@ -88,7 +119,8 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): raise AmsServiceException(json=decoded, request=route_name) elif status_code == 408: - raise requests.exceptions.Timeout + decoded = json.loads(content) if content else {} + raise AmsTimeoutException(json=decoded, request=route_name) # JSON error returned by AMS elif status_code != 200 and status_code in self.errors_route[route_name][1]: @@ -105,7 +137,7 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): 'message': content}} raise AmsServiceException(json=errormsg, request=route_name) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, socket.error) as e: + except (requests.exceptions.ConnectionError, socket.error) as e: raise AmsConnectionException(e, route_name) else: diff --git a/pymod/amsexceptions.py b/pymod/amsexceptions.py index d0d92c3..8db1235 100644 --- a/pymod/amsexceptions.py +++ b/pymod/amsexceptions.py @@ -29,6 +29,15 @@ def __init__(self, json, request): super(AmsServiceException, self).__init__(errord) +class AmsTimeoutException(AmsServiceException): + """ + Exception for timeout returned by the Argo Messaging Service if message + was not acknownledged in desired time frame (ackDeadlineSeconds) + """ + def __init__(self, json, request): + super(AmsServiceException, self).__init__(json, request) + + class AmsConnectionException(AmsException): """ Exception for connection related problems catched from requests library diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index 95e129d..a6510c6 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -9,7 +9,7 @@ from pymod import AmsMessage from pymod import AmsTopic from pymod import AmsSubscription -from pymod import AmsServiceException, AmsConnectionException, AmsException +from pymod import AmsServiceException, AmsConnectionException, AmsTimeoutException, AmsException from .amsmocks import ErrorMocks from .amsmocks import TopicMocks @@ -148,7 +148,7 @@ def testRetryConnectionAmsTimeout(self, mock_requests_get): self.ams._retry_make_request.im_func.__defaults__ = (None, None, retry, retrysleep) else: self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep) - self.assertRaises(AmsConnectionException, self.ams.list_topics) + self.assertRaises(AmsTimeoutException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) From f53ffce0b0709dfdc7a737ce206b4e5f559f3f57 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 30 Sep 2019 18:28:27 +0200 Subject: [PATCH 08/45] Backoff sleep retry --- pymod/ams.py | 62 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index f043528..b53fc82 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -1,17 +1,16 @@ import json +import logging +import logging.handlers import requests import socket import sys -<<<<<<< HEAD import datetime from .amsexceptions import AmsServiceException, AmsConnectionException, AmsMessageException, AmsException -======= import time from .amsexceptions import (AmsServiceException, AmsConnectionException, AmsMessageException, AmsException, AmsTimeoutException) ->>>>>>> Refactorings with introduced AmsTimeoutException from .amsmsg import AmsMessage from .amstopic import AmsTopic from .amssubscription import AmsSubscription @@ -21,6 +20,7 @@ except: from ordereddict import OrderedDict +log = logging.getLogger(__name__) class AmsHttpRequests(object): """ @@ -64,32 +64,50 @@ def __init__(self): "topic_publish": ["post", set([413, 401, 403])], "sub_pushconfig": ["post", set([400, 401, 403, 404])], "auth_x509": ["post", set([400, 401, 403, 404])], -<<<<<<< HEAD "sub_pull": ["post", set([400, 401, 403, 404])], "sub_timeToOffset": ["get", set([400, 401, 403, 404, 409])] } -======= - "sub_pull": ["post", set([400, 401, 403, 404])]} - def _retry_make_request(self, url, body=None, route_name=None, retry=3, retrysleep=60, **reqkwargs): + def _gen_backoff_time(try_number, backoff_factor): + for i in range(1, try_number): + value = backoff_factor * (2 ** (i - 1)) + yield value + + def _retry_make_request(self, url, body=None, route_name=None, retry=3, + retrysleep=60, retrybackoff=None, **reqkwargs): i = 1 timeout = reqkwargs.get('timeout', 0) - while i <= retry + 1: - try: - return self._make_request(url, body, route_name, **reqkwargs) - except (AmsConnectionException, AmsTimeoutException) as e: - if i == retry + 1: - raise e - else: - time.sleep(retrysleep) + if retrybackoff: + for s in self._gen_backoff_time(retry, retrybackoff): + try: + return self._make_request(url, body, route_name, **reqkwargs) + except (AmsConnectionException, AmsTimeoutException) as e: + time.sleep(s) if timeout: - log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, retrysleep, timeout)) + log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, s, timeout)) else: log.warning('Retry #{0} after {1} seconds'.format(i, retrysleep)) - finally: - i += 1 ->>>>>>> Refactorings with introduced AmsTimeoutException + finally: + i += 1 + else: + raise e + + else: + while i <= retry + 1: + try: + return self._make_request(url, body, route_name, **reqkwargs) + except (AmsConnectionException, AmsTimeoutException) as e: + if i == retry + 1: + raise e + else: + time.sleep(retrysleep) + if timeout: + log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, retrysleep, timeout)) + else: + log.warning('Retry #{0} after {1} seconds'.format(i, retrysleep)) + finally: + i += 1 def _make_request(self, url, body=None, route_name=None, **reqkwargs): """Common method for PUT, GET, POST HTTP requests with appropriate @@ -155,7 +173,7 @@ def do_get(self, url, route_name, **reqkwargs): # try to send a GET request to the messaging service. # if a connection problem araises a Connection error exception is raised. try: - return self._make_request(url, route_name=route_name, **reqkwargs) + return self._retry_make_request(url, route_name=route_name, **reqkwargs) except AmsException as e: raise e @@ -172,7 +190,7 @@ def do_put(self, url, body, route_name, **reqkwargs): # try to send a PUT request to the messaging service. # if a connection problem araises a Connection error exception is raised. try: - return self._make_request(url, body=body, route_name=route_name, **reqkwargs) + return self._retry_make_request(url, body=body, route_name=route_name, **reqkwargs) except AmsException as e: raise e @@ -189,7 +207,7 @@ def do_post(self, url, body, route_name, **reqkwargs): # try to send a Post request to the messaging service. # if a connection problem araises a Connection error exception is raised. try: - return self._make_request(url, body=body, route_name=route_name, **reqkwargs) + return self._retry_make_request(url, body=body, route_name=route_name, **reqkwargs) except AmsException as e: raise e From d00b7a60cd1907717631c655613a57d9add3ee38 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 1 Oct 2019 11:18:49 +0200 Subject: [PATCH 09/45] Backoff retry log messages --- pymod/ams.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index b53fc82..74bc173 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -68,7 +68,7 @@ def __init__(self): "sub_timeToOffset": ["get", set([400, 401, 403, 404, 409])] } - def _gen_backoff_time(try_number, backoff_factor): + def _gen_backoff_time(self, try_number, backoff_factor): for i in range(1, try_number): value = backoff_factor * (2 ** (i - 1)) yield value @@ -79,15 +79,15 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=3, timeout = reqkwargs.get('timeout', 0) if retrybackoff: - for s in self._gen_backoff_time(retry, retrybackoff): + for sleep_secs in self._gen_backoff_time(retry + 1, retrybackoff): try: return self._make_request(url, body, route_name, **reqkwargs) except (AmsConnectionException, AmsTimeoutException) as e: - time.sleep(s) + time.sleep(sleep_secs) if timeout: - log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, s, timeout)) + log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, sleep_secs, timeout)) else: - log.warning('Retry #{0} after {1} seconds'.format(i, retrysleep)) + log.warning('Backoff retry #{0} after {1} seconds'.format(i, sleep_secs)) finally: i += 1 else: From 77c3fad190361fc77353da98b4e7748b01783953 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 1 Oct 2019 12:48:43 +0200 Subject: [PATCH 10/45] Tests for backoff retry --- pymod/ams.py | 4 ++-- tests/test_errorclient.py | 24 ++++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 74bc173..688278b 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -69,7 +69,7 @@ def __init__(self): } def _gen_backoff_time(self, try_number, backoff_factor): - for i in range(1, try_number): + for i in range(0, try_number): value = backoff_factor * (2 ** (i - 1)) yield value @@ -91,7 +91,7 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=3, finally: i += 1 else: - raise e + raise AmsConnectionException('Timeout', route_name) else: while i <= retry + 1: diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index a6510c6..1e4013b 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -128,9 +128,25 @@ def testRetryConnectionProblems(self, mock_requests_get): retry = 3 retrysleep = 0.1 if sys.version_info < (3, ): - self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep) + self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep, None) else: - self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep) + self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep, None) + self.assertRaises(AmsConnectionException, self.ams.list_topics) + self.assertEqual(mock_requests_get.call_count, retry + 1) + + @mock.patch('pymod.ams.requests.get') + def testBackoffRetryConnectionProblems(self, mock_requests_get): + mock_requests_get.side_effect = [requests.exceptions.ConnectionError, + requests.exceptions.ConnectionError, + requests.exceptions.ConnectionError, + requests.exceptions.ConnectionError] + retry = 3 + retrysleep = 0.1 + retrybackoff = 0.1 + if sys.version_info < (3, ): + self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep, retrybackoff) + else: + self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep, retrybackoff) self.assertRaises(AmsConnectionException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) @@ -145,9 +161,9 @@ def testRetryConnectionAmsTimeout(self, mock_requests_get): retry = 3 retrysleep = 0.1 if sys.version_info < (3, ): - self.ams._retry_make_request.im_func.__defaults__ = (None, None, retry, retrysleep) + self.ams._retry_make_request.im_func.__defaults__ = (None, None, retry, retrysleep, None) else: - self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep) + self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep, None) self.assertRaises(AmsTimeoutException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) From c37d7ba7d99c9206a7f969727c3ca95ec1029e6d Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 1 Oct 2019 15:56:20 +0200 Subject: [PATCH 11/45] Fix backoff Py34 test --- pymod/ams.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 688278b..3aa5595 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -91,7 +91,7 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=3, finally: i += 1 else: - raise AmsConnectionException('Timeout', route_name) + raise AmsConnectionException('Backoff retries exhausted', route_name) else: while i <= retry + 1: @@ -125,8 +125,10 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): content = r.content status_code = r.status_code - if content and sys.version_info < (3, 6, ): - content = content.decode() + if (content + and sys.version_info < (3, 6, ) + and isinstance(content, bytes)): + content = content.decode() if status_code == 200: decoded = json.loads(content) if content else {} From adfcd0b48e65b172703119535647e55888763bb1 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 4 Nov 2019 18:07:17 +0100 Subject: [PATCH 12/45] Add retry* args for topic_publish, sub_ack and sub_pull --- pymod/ams.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 3aa5595..1fc56e6 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -73,7 +73,7 @@ def _gen_backoff_time(self, try_number, backoff_factor): value = backoff_factor * (2 ** (i - 1)) yield value - def _retry_make_request(self, url, body=None, route_name=None, retry=3, + def _retry_make_request(self, url, body=None, route_name=None, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): i = 1 timeout = reqkwargs.get('timeout', 0) @@ -196,7 +196,8 @@ def do_put(self, url, body, route_name, **reqkwargs): except AmsException as e: raise e - def do_post(self, url, body, route_name, **reqkwargs): + def do_post(self, url, body, route_name, retry=0, retrysleep=60, + retrybackoff=None, **reqkwargs): """Method supports all the POST requests. Used for (topics, subscriptions, messages). @@ -209,7 +210,11 @@ def do_post(self, url, body, route_name, **reqkwargs): # try to send a Post request to the messaging service. # if a connection problem araises a Connection error exception is raised. try: - return self._retry_make_request(url, body=body, route_name=route_name, **reqkwargs) + return self._retry_make_request(url, body=body, + route_name=route_name, retry=retry, + retrysleep=retrysleep, + retrybackoff=retrybackoff, + **reqkwargs) except AmsException as e: raise e @@ -684,7 +689,7 @@ def get_topic(self, topic, retobj=False, **reqkwargs): else: return r - def publish(self, topic, msg, **reqkwargs): + def publish(self, topic, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): """Publish a message or list of messages to a selected topic. Args: @@ -782,7 +787,8 @@ def has_sub(self, sub, **reqkwargs): except AmsConnectionException as e: raise e - def pull_sub(self, sub, num=1, return_immediately=False, **reqkwargs): + def pull_sub(self, sub, retry=0, retrysleep=60, retrybackoff=None, num=1, + return_immediately=False, **reqkwargs): """This function consumes messages from a subscription in a project with a POST request. @@ -803,7 +809,9 @@ def pull_sub(self, sub, num=1, return_immediately=False, **reqkwargs): # Compose url url = route[1].format(self.endpoint, self.token, self.project, "", sub) method = getattr(self, 'do_{0}'.format(route[0])) - r = method(url, msg_body, "sub_pull", **reqkwargs) + r = method(url, msg_body, "sub_pull", retry=retry, + retrysleep=retrysleep, retrybackoff=retrybackoff, + **reqkwargs) msgs = r['receivedMessages'] self.set_pullopt('maxMessages', wasmax) @@ -811,7 +819,8 @@ def pull_sub(self, sub, num=1, return_immediately=False, **reqkwargs): return list(map(lambda m: (m['ackId'], AmsMessage(b64enc=False, **m['message'])), msgs)) - def ack_sub(self, sub, ids, **reqkwargs): + def ack_sub(self, sub, ids, retry=0, retrysleep=60, retrybackoff=None, + **reqkwargs): """Messages retrieved from a pull subscription can be acknowledged by sending message with an array of ackIDs. The service will retrieve the ackID corresponding to the highest message offset and will consider that message and all previous messages as acknowledged by the consumer. From 22889a1feacb60414cd862f50908c9c789207572 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 4 Nov 2019 18:40:09 +0100 Subject: [PATCH 13/45] Remove duplicated import --- pymod/ams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymod/ams.py b/pymod/ams.py index 1fc56e6..8e8bf29 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -5,7 +5,6 @@ import socket import sys import datetime -from .amsexceptions import AmsServiceException, AmsConnectionException, AmsMessageException, AmsException import time from .amsexceptions import (AmsServiceException, AmsConnectionException, @@ -22,6 +21,7 @@ log = logging.getLogger(__name__) + class AmsHttpRequests(object): """ Class encapsulates methods used by ArgoMessagingService. Each method represent From 3df7df1dff1c56b5deb0a1d8c86b223ecc63a68c Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 4 Nov 2019 21:52:41 +0100 Subject: [PATCH 14/45] Exception for errors coming from HAProxy AMS balancer --- pymod/__init__.py | 6 +++--- pymod/ams.py | 13 ++++--------- pymod/amsexceptions.py | 26 ++++++++++++++++++++++++++ tests/test_errorclient.py | 9 +++++---- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/pymod/__init__.py b/pymod/__init__.py index 2a021ea..37debdb 100644 --- a/pymod/__init__.py +++ b/pymod/__init__.py @@ -8,9 +8,9 @@ def emit(self, record): logging.getLogger(__name__).addHandler(NullHandler()) from .ams import ArgoMessagingService -from .amsexceptions import (AmsServiceException, AmsConnectionException, - AmsTimeoutException, AmsMessageException, - AmsException) +from .amsexceptions import (AmsServiceException, AmsBalancerException, + AmsConnectionException, AmsTimeoutException, + AmsMessageException, AmsException) from .amsmsg import AmsMessage from .amstopic import AmsTopic from .amssubscription import AmsSubscription diff --git a/pymod/ams.py b/pymod/ams.py index 8e8bf29..eda57ef 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -9,7 +9,7 @@ from .amsexceptions import (AmsServiceException, AmsConnectionException, AmsMessageException, AmsException, - AmsTimeoutException) + AmsTimeoutException, AmsBalancerException) from .amsmsg import AmsMessage from .amstopic import AmsTopic from .amssubscription import AmsSubscription @@ -147,15 +147,10 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): decoded = json.loads(content) if content else {} raise AmsServiceException(json=decoded, request=route_name) - # handle other erroneous behaviour and construct error message from - # JSON or plaintext content in response + # handle errors coming from HAProxy load balancer and construct + # error message from JSON or plaintext content in response elif status_code != 200 and status_code not in self.errors_route[route_name][1]: - try: - errormsg = json.loads(content) - except ValueError: - errormsg = {'error': {'code': status_code, - 'message': content}} - raise AmsServiceException(json=errormsg, request=route_name) + raise AmsBalancerException(content, status_code, request=route_name) except (requests.exceptions.ConnectionError, socket.error) as e: raise AmsConnectionException(e, route_name) diff --git a/pymod/amsexceptions.py b/pymod/amsexceptions.py index 8db1235..e7f76e1 100644 --- a/pymod/amsexceptions.py +++ b/pymod/amsexceptions.py @@ -29,6 +29,32 @@ def __init__(self, json, request): super(AmsServiceException, self).__init__(errord) +class AmsBalancerException(AmsException): + """ + Exception for HAProxy Argo Messaging Service errors + """ + def __init__(self, error, status, request): + errord = dict() + + try: + errormsg = json.loads(error) + except ValueError: + errormsg = {'error': {'code': status, 'message': error}} + + self.msg = "While trying the [{0}]: {1}".format(request, errormsg['error']['message']) + errord.update(error=self.msg) + + if errormsg['error'].get('code'): + self.code = errormsg['error']['code'] + errord.update(status_code=self.code) + + if errormsg['error'].get('status'): + self.status = errormsg['error']['status'] + errord.update(status=self.status) + + super(AmsException, self).__init__(errord) + + class AmsTimeoutException(AmsServiceException): """ Exception for timeout returned by the Argo Messaging Service if message diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index 1e4013b..885d76a 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -9,7 +9,8 @@ from pymod import AmsMessage from pymod import AmsTopic from pymod import AmsSubscription -from pymod import AmsServiceException, AmsConnectionException, AmsTimeoutException, AmsException +from pymod import (AmsServiceException, AmsConnectionException, + AmsTimeoutException, AmsBalancerException, AmsException) from .amsmocks import ErrorMocks from .amsmocks import TopicMocks @@ -65,7 +66,7 @@ def publish_mock(url, request): try: resp = self.ams.publish("topic1", msg) except Exception as e: - assert isinstance(e, AmsServiceException) + assert isinstance(e, AmsBalancerException) self.assertEqual(e.code, 504) # Tests for plaintext or JSON encoded backend error messages @@ -79,7 +80,7 @@ def error_plaintxt(url, request): with HTTMock(error_plaintxt): try: resp = self.ams.get_topic("topic1") - except AmsServiceException as e: + except AmsBalancerException as e: if sys.version_info < (3, 6 ): response_string = "Cannot get topic" else: @@ -98,7 +99,7 @@ def error_json(url, request): with HTTMock(error_json): try: resp = self.ams.get_topic("topic1") - except AmsServiceException as e: + except AmsBalancerException as e: self.assertEqual(e.code, 500) self.assertEqual(e.msg, "While trying the [topic_get]: Cannot get topic") self.assertEqual(e.status, "INTERNAL_SERVER_ERROR") From ab59c20b1fe97130e8d97dbba7508764a510c6ad Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 5 Nov 2019 00:19:07 +0100 Subject: [PATCH 15/45] Test for HAProxy timeout for subscription pull --- dist/argo_ams_library-0.4.2-py3.7.egg | Bin 31686 -> 0 bytes pymod/ams.py | 5 +-- tests/test_errorclient.py | 44 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) delete mode 100644 dist/argo_ams_library-0.4.2-py3.7.egg diff --git a/dist/argo_ams_library-0.4.2-py3.7.egg b/dist/argo_ams_library-0.4.2-py3.7.egg deleted file mode 100644 index 7ad6ca6eacf3c464ba7383f65f674601fd82c916..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31686 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

FrpUNe~E}DYKU=h8zS0c+q%=I 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

lDj6+FqW^rN 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 UmehZ^Iimmw{(5HP{Ku>R1A;KCdjJ3c diff --git a/pymod/ams.py b/pymod/ams.py index eda57ef..d832f7a 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -82,7 +82,8 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, for sleep_secs in self._gen_backoff_time(retry + 1, retrybackoff): try: return self._make_request(url, body, route_name, **reqkwargs) - except (AmsConnectionException, AmsTimeoutException) as e: + except (AmsBalancerException, AmsConnectionException, + AmsTimeoutException) as e: time.sleep(sleep_secs) if timeout: log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, sleep_secs, timeout)) @@ -97,7 +98,7 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, while i <= retry + 1: try: return self._make_request(url, body, route_name, **reqkwargs) - except (AmsConnectionException, AmsTimeoutException) as e: + except (AmsBalancerException, AmsConnectionException, AmsTimeoutException) as e: if i == retry + 1: raise e else: diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index 885d76a..a92e563 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -25,6 +25,15 @@ def setUp(self): self.topicmocks = TopicMocks() self.submocks = SubMocks() + # set defaults for testing of retries + retry = 0 + retrysleep = 0.1 + retrybackoff = None + if sys.version_info < (3, ): + self.ams._retry_make_request.im_func.func_defaults = (None, None, retry, retrysleep, retrybackoff) + else: + self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep, retrybackoff) + # Test create topic client request def testCreateTopics(self): # Execute ams client with mocked response @@ -168,6 +177,41 @@ def testRetryConnectionAmsTimeout(self, mock_requests_get): self.assertRaises(AmsTimeoutException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) + @mock.patch('pymod.ams.requests.post') + def testRetryAmsBalancerTimeout(self, mock_requests_post): + retry = 4 + retrysleep = 0.2 + errmsg = "

504 Gateway Time-out

\nThe server didn't respond in time.\n" + mock_response = mock.create_autospec(requests.Response) + mock_response.status_code = 504 + mock_response.content = errmsg + mock_requests_post.return_value = mock_response + try: + self.ams.pull_sub('subscription1', retry=retry, retrysleep=retrysleep) + except Exception as e: + assert isinstance(e, AmsBalancerException) + self.assertEqual(e.code, 504) + self.assertEqual(e.msg, 'While trying the [sub_pull]: ' + errmsg) + self.assertEqual(mock_requests_post.call_count, retry + 1) + + @mock.patch('pymod.ams.requests.get') + def testRetryConnectionAmsTimeout(self, mock_requests_get): + mock_response = mock.create_autospec(requests.Response) + mock_response.status_code = 408 + mock_response.content = '{"error": {"code": 408, \ + "message": "Ams Timeout", \ + "status": "TIMEOUT"}}' + mock_requests_get.return_value = mock_response + retry = 3 + retrysleep = 0.1 + if sys.version_info < (3, ): + self.ams._retry_make_request.im_func.__defaults__ = (None, None, retry, retrysleep, None) + else: + self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep, None) + self.assertRaises(AmsTimeoutException, self.ams.list_topics) + self.assertEqual(mock_requests_get.call_count, retry + 1) + + if __name__ == '__main__': unittest.main() From ec1a364bf56858bb3795fa2af9aabfcf69c94e46 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 5 Nov 2019 20:25:05 +0100 Subject: [PATCH 16/45] Explicit define of HTTP statuses and AMS methods as balancer exceptions --- pymod/ams.py | 24 +++++++++++++++++++----- tests/test_errorclient.py | 4 ++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index d832f7a..2e32ed9 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -54,7 +54,7 @@ def __init__(self): # HTTP error status codes returned by AMS according to: # http://argoeu.github.io/messaging/v1/api_errors/ - self.errors_route = {"topic_create": ["put", set([409, 401, 403])], + self.ams_errors_route = {"topic_create": ["put", set([409, 401, 403])], "topic_list": ["get", set([400, 401, 403, 404])], "sub_create": ["put", set([400, 409, 408, 401, 403])], "sub_ack": ["post", set([408, 400, 401, 403, 404])], @@ -67,6 +67,10 @@ def __init__(self): "sub_pull": ["post", set([400, 401, 403, 404])], "sub_timeToOffset": ["get", set([400, 401, 403, 404, 409])] } + # https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#1.3 + self.balancer_errors_route = {"sub_ack": ["post", set([500, 502, 503, 504])], + "sub_pull": ["post", set([500, 502, 503, 504])], + "topic_publish": ["post", set([500, 502, 503, 504])]} def _gen_backoff_time(self, try_number, backoff_factor): for i in range(0, try_number): @@ -144,15 +148,25 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): raise AmsTimeoutException(json=decoded, request=route_name) # JSON error returned by AMS - elif status_code != 200 and status_code in self.errors_route[route_name][1]: + elif status_code != 200 and status_code in self.ams_errors_route[route_name][1]: decoded = json.loads(content) if content else {} raise AmsServiceException(json=decoded, request=route_name) - # handle errors coming from HAProxy load balancer and construct - # error message from JSON or plaintext content in response - elif status_code != 200 and status_code not in self.errors_route[route_name][1]: + # handle errors coming from HAProxy load balancer + elif (status_code != 200 and route_name in + self.balancer_errors_route and status_code in + self.balancer_errors_route[route_name][1]): raise AmsBalancerException(content, status_code, request=route_name) + # handle any other erroneous behaviour by raising exception + else: + try: + errormsg = json.loads(content) + except ValueError: + errormsg = {'error': {'code': status_code, + 'message': content}} + raise AmsServiceException(json=errormsg, request=route_name) + except (requests.exceptions.ConnectionError, socket.error) as e: raise AmsConnectionException(e, route_name) diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index a92e563..489de15 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -89,7 +89,7 @@ def error_plaintxt(url, request): with HTTMock(error_plaintxt): try: resp = self.ams.get_topic("topic1") - except AmsBalancerException as e: + except AmsServiceException as e: if sys.version_info < (3, 6 ): response_string = "Cannot get topic" else: @@ -108,7 +108,7 @@ def error_json(url, request): with HTTMock(error_json): try: resp = self.ams.get_topic("topic1") - except AmsBalancerException as e: + except AmsServiceException as e: self.assertEqual(e.code, 500) self.assertEqual(e.msg, "While trying the [topic_get]: Cannot get topic") self.assertEqual(e.status, "INTERNAL_SERVER_ERROR") From 713ed6e01940f7b013f28f4c64f26fa7f18faa2f Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 5 Nov 2019 21:06:32 +0100 Subject: [PATCH 17/45] Refactor exception error message generation --- pymod/ams.py | 40 ++++++++++++++++++++++++++-------------- pymod/amsexceptions.py | 17 ++++++----------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 2e32ed9..1878531 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -72,6 +72,16 @@ def __init__(self): "sub_pull": ["post", set([500, 502, 503, 504])], "topic_publish": ["post", set([500, 502, 503, 504])]} + def _error_dict(self, response_content, status): + error_dict = dict() + + try: + error_dict = json.loads(response_content) if response_content else {} + except ValueError: + error_dict = {'error': {'code': status, 'message': response_content}} + + return error_dict + def _gen_backoff_time(self, try_number, backoff_factor): for i in range(0, try_number): value = backoff_factor * (2 ** (i - 1)) @@ -136,36 +146,38 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): content = content.decode() if status_code == 200: - decoded = json.loads(content) if content else {} + decoded = self._error_dict(content, status_code) # handle authnz related errors for all calls elif status_code == 401 or status_code == 403: - decoded = json.loads(content) if content else {} - raise AmsServiceException(json=decoded, request=route_name) + raise AmsServiceException(json=self._error_dict(content, + status_code), + request=route_name) elif status_code == 408: - decoded = json.loads(content) if content else {} - raise AmsTimeoutException(json=decoded, request=route_name) + raise AmsTimeoutException(json=self._error_dict(content, + status_code), + request=route_name) # JSON error returned by AMS elif status_code != 200 and status_code in self.ams_errors_route[route_name][1]: - decoded = json.loads(content) if content else {} - raise AmsServiceException(json=decoded, request=route_name) + raise AmsServiceException(json=self._error_dict(content, + status_code), + request=route_name) # handle errors coming from HAProxy load balancer elif (status_code != 200 and route_name in self.balancer_errors_route and status_code in self.balancer_errors_route[route_name][1]): - raise AmsBalancerException(content, status_code, request=route_name) + raise AmsBalancerException(json=self._error_dict(content, + status_code), + request=route_name) # handle any other erroneous behaviour by raising exception else: - try: - errormsg = json.loads(content) - except ValueError: - errormsg = {'error': {'code': status_code, - 'message': content}} - raise AmsServiceException(json=errormsg, request=route_name) + raise AmsServiceException(json=self._error_dict(content, + status_code), + request=route_name) except (requests.exceptions.ConnectionError, socket.error) as e: raise AmsConnectionException(e, route_name) diff --git a/pymod/amsexceptions.py b/pymod/amsexceptions.py index e7f76e1..a095788 100644 --- a/pymod/amsexceptions.py +++ b/pymod/amsexceptions.py @@ -33,23 +33,18 @@ class AmsBalancerException(AmsException): """ Exception for HAProxy Argo Messaging Service errors """ - def __init__(self, error, status, request): + def __init__(self, json, request): errord = dict() - try: - errormsg = json.loads(error) - except ValueError: - errormsg = {'error': {'code': status, 'message': error}} - - self.msg = "While trying the [{0}]: {1}".format(request, errormsg['error']['message']) + self.msg = "While trying the [{0}]: {1}".format(request, json['error']['message']) errord.update(error=self.msg) - if errormsg['error'].get('code'): - self.code = errormsg['error']['code'] + if json['error'].get('code'): + self.code = json['error']['code'] errord.update(status_code=self.code) - if errormsg['error'].get('status'): - self.status = errormsg['error']['status'] + if json['error'].get('status'): + self.status = json['error']['status'] errord.update(status=self.status) super(AmsException, self).__init__(errord) From ad86510b449ce77185890e4b2e2852748cc6489b Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 5 Nov 2019 21:40:45 +0100 Subject: [PATCH 18/45] Refactor by using general error msg construction --- pymod/ams.py | 33 +++++++++++++++++---------------- tests/test_authenticate.py | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 1878531..d395318 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -140,9 +140,8 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): content = r.content status_code = r.status_code - if (content - and sys.version_info < (3, 6, ) - and isinstance(content, bytes)): + if (content and sys.version_info < (3, 6, ) and isinstance(content, + bytes)): content = content.decode() if status_code == 200: @@ -160,7 +159,8 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): request=route_name) # JSON error returned by AMS - elif status_code != 200 and status_code in self.ams_errors_route[route_name][1]: + elif (status_code != 200 and status_code in + self.ams_errors_route[route_name][1]): raise AmsServiceException(json=self._error_dict(content, status_code), request=route_name) @@ -197,7 +197,8 @@ def do_get(self, url, route_name, **reqkwargs): # try to send a GET request to the messaging service. # if a connection problem araises a Connection error exception is raised. try: - return self._retry_make_request(url, route_name=route_name, **reqkwargs) + return self._retry_make_request(url, route_name=route_name, + **reqkwargs) except AmsException as e: raise e @@ -214,7 +215,8 @@ def do_put(self, url, body, route_name, **reqkwargs): # try to send a PUT request to the messaging service. # if a connection problem araises a Connection error exception is raised. try: - return self._retry_make_request(url, body=body, route_name=route_name, **reqkwargs) + return self._retry_make_request(url, body=body, + route_name=route_name, **reqkwargs) except AmsException as e: raise e @@ -258,18 +260,18 @@ def do_delete(self, url, route_name, **reqkwargs): # JSON error returned by AMS if r.status_code != 200 and r.status_code in self.errors[m]: - decoded = json.loads(r.content) if r.content else {} - raise AmsServiceException(json=decoded, request=route_name) + errormsg = self._error_dict(r.content, r.status_code) + raise AmsServiceException(json=errormsg, request=route_name) # handle other erroneous behaviour elif r.status_code != 200 and r.status_code not in self.errors[m]: - errormsg = {'error': {'code': r.status_code, - 'message': r.content}} + errormsg = self._error_dict(r.content, r.status_code) raise AmsServiceException(json=errormsg, request=route_name) else: return True - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: + except (requests.exceptions.ConnectionError, + requests.exceptions.Timeout) as e: raise AmsConnectionException(e, route_name) @@ -316,9 +318,8 @@ def assign_token(self, token, cert, key): # when no token was provided if e.msg == 'While trying the [auth_x509]: No certificate provided.': - refined_msg = "No certificate provided. No token provided" - errormsg = {'error': {'code': e.code, - 'message': refined_msg}} + refined_msg = "No certificate provided. No token provided." + errormsg = self._error_dict(refined_msg, e.code) raise AmsServiceException(json=errormsg, request="auth_x509") raise e @@ -335,7 +336,7 @@ def auth_via_cert(self, cert, key, **reqkwargs): python-requests library call. """ if cert == "" and key == "": - errord = {"error": {"code": 400, "message": "No certificate provided."}} + errord = self._error_dict("No certificate provided.", 400) raise AmsServiceException(json=errord, request="auth_x509") # create the certificate tuple needed by the requests library @@ -351,7 +352,7 @@ def auth_via_cert(self, cert, key, **reqkwargs): r = method(url, "auth_x509", **reqkwargs) # if the `token` field was not found in the response, raise an error if "token" not in r: - errord = {"error": {"code": 500, "message": "Token was not found in the response. Response: " + str(r)}} + errord = self._error_dict("Token was not found in the response. Response: " + str(r), 500) raise AmsServiceException(json=errord, request="auth_x509") return r["token"] except (AmsServiceException, AmsConnectionException) as e: diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 0458932..da33cb4 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -43,7 +43,7 @@ def test_auth_via_cert_empty_token_and_cert(self): ams = ArgoMessagingService(endpoint="localhost", project="TEST") except AmsServiceException as e: self.assertEqual(e.code, 400) - self.assertEqual(e.msg, 'While trying the [auth_x509]: No certificate provided. No token provided') + self.assertEqual(e.msg, 'While trying the [auth_x509]: No certificate provided. No token provided.') # tests the case of providing a token def test_assign_token(self): From a766432cbcb51e7def2af7e0e6613bade008d2dd Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 6 Nov 2019 10:49:26 +0100 Subject: [PATCH 19/45] Wrap 408 AMS, 408 HAProxy and 504 HAProxy in general AmsTimeoutException --- pymod/ams.py | 95 ++++++++++++++++++++++----------------- pymod/amsexceptions.py | 19 ++------ tests/test_errorclient.py | 36 +++++++-------- 3 files changed, 74 insertions(+), 76 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index d395318..4571fd3 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -24,9 +24,10 @@ class AmsHttpRequests(object): """ - Class encapsulates methods used by ArgoMessagingService. Each method represent - HTTP request made to AMS with the help of requests library. Proper service error - handling is implemented according to HTTP status codes returned by service. + Class encapsulates methods used by ArgoMessagingService. Each method + represent HTTP request made to AMS with the help of requests library. + Proper service error handling is implemented according to HTTP status + codes returned by service and the balancer. """ def __init__(self): # Create route list @@ -89,6 +90,10 @@ def _gen_backoff_time(self, try_number, backoff_factor): def _retry_make_request(self, url, body=None, route_name=None, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): + """ + Wrapper around _make_request() that decides whether should request + be retried or not. + """ i = 1 timeout = reqkwargs.get('timeout', 0) @@ -125,10 +130,10 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, i += 1 def _make_request(self, url, body=None, route_name=None, **reqkwargs): - """Common method for PUT, GET, POST HTTP requests with appropriate - service error handling. For known error HTTP statuses, returned JSON - will be used as exception error message, otherwise assume and build one - from response content string. + """ + Common method for PUT, GET, POST HTTP requests with appropriate + service error handling by differing between AMS and HAProxy + erroneous behaviour. Method is wrapped by _retry_make_request(). """ m = self.routes[route_name][0] decoded = None @@ -153,12 +158,13 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): status_code), request=route_name) - elif status_code == 408: + elif status_code == 408 or (status_code == 504 and route_name in + self.balancer_errors_route): raise AmsTimeoutException(json=self._error_dict(content, status_code), request=route_name) - # JSON error returned by AMS + # handle errors from AMS elif (status_code != 200 and status_code in self.ams_errors_route[route_name][1]): raise AmsServiceException(json=self._error_dict(content, @@ -186,13 +192,14 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): return decoded if decoded else {} def do_get(self, url, route_name, **reqkwargs): - """Method supports all the GET requests. Used for (topics, - subscriptions, messages). + """ + Method supports all the GET requests. Used for (topics, + subscriptions, messages). - Args: - url: str. The final messaging service endpoint - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + url: str. The final messaging service endpoint + route_name: str. The name of the route to follow selected from the route list + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ # try to send a GET request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -203,14 +210,15 @@ def do_get(self, url, route_name, **reqkwargs): raise e def do_put(self, url, body, route_name, **reqkwargs): - """Method supports all the PUT requests. Used for (topics, - subscriptions, messages). + """ + Method supports all the PUT requests. Used for (topics, + subscriptions, messages). - Args: - url: str. The final messaging service endpoint - body: dict. Body the post data to send based on the PUT request. The post data is always in json format. - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + url: str. The final messaging service endpoint + body: dict. Body the post data to send based on the PUT request. The post data is always in json format. + route_name: str. The name of the route to follow selected from the route list + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ # try to send a PUT request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -222,14 +230,15 @@ def do_put(self, url, body, route_name, **reqkwargs): def do_post(self, url, body, route_name, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """Method supports all the POST requests. Used for (topics, - subscriptions, messages). + """ + Method supports all the POST requests. Used for (topics, + subscriptions, messages). - Args: - url: str. The final messaging service endpoint - body: dict. Body the post data to send based on the PUT request. The post data is always in json format. - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + url: str. The final messaging service endpoint + body: dict. Body the post data to send based on the PUT request. The post data is always in json format. + route_name: str. The name of the route to follow selected from the route list + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ # try to send a Post request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -243,13 +252,14 @@ def do_post(self, url, body, route_name, retry=0, retrysleep=60, raise e def do_delete(self, url, route_name, **reqkwargs): - """Delete method that is used to make the appropriate request. - Used for (topics, subscriptions). + """ + Delete method that is used to make the appropriate request. + Used for (topics, subscriptions). - Args: - url: str. The final messaging service endpoint - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + url: str. The final messaging service endpoint + route_name: str. The name of the route to follow selected from the route list + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ # try to send a delete request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -296,12 +306,12 @@ def __init__(self, endpoint, token="", project="", cert="", key="", authn_port=8 def assign_token(self, token, cert, key): """ - Assign a token to the ams object + Assign a token to the ams object - Args: - token(str): a valid ams token - cert(str): a path to a valid certificate file - key(str): a path to the associated key file for the provided certificate + Args: + token(str): a valid ams token + cert(str): a path to a valid certificate file + key(str): a path to the associated key file for the provided certificate """ # check if a token has been provided @@ -523,7 +533,7 @@ def time_to_offset_sub(self, sub, timestamp, **reqkwargs): def modifyoffset_sub(self, sub, move_to, **reqkwargs): """ - Modify the position of the current offset. + Modify the position of the current offset. Args: sub (str): The subscription name. @@ -584,7 +594,8 @@ def modifyacl_sub(self, sub, users, **reqkwargs): raise e def pushconfig_sub(self, sub, push_endpoint=None, retry_policy_type='linear', retry_policy_period=300, **reqkwargs): - """Modify push configuration of given subscription + """ + Modify push configuration of given subscription Args: sub: shortname of subscription diff --git a/pymod/amsexceptions.py b/pymod/amsexceptions.py index a095788..95dc3ed 100644 --- a/pymod/amsexceptions.py +++ b/pymod/amsexceptions.py @@ -29,25 +29,12 @@ def __init__(self, json, request): super(AmsServiceException, self).__init__(errord) -class AmsBalancerException(AmsException): +class AmsBalancerException(AmsServiceException): """ Exception for HAProxy Argo Messaging Service errors """ def __init__(self, json, request): - errord = dict() - - self.msg = "While trying the [{0}]: {1}".format(request, json['error']['message']) - errord.update(error=self.msg) - - if json['error'].get('code'): - self.code = json['error']['code'] - errord.update(status_code=self.code) - - if json['error'].get('status'): - self.status = json['error']['status'] - errord.update(status=self.status) - - super(AmsException, self).__init__(errord) + super(AmsBalancerException, self).__init__(json, request) class AmsTimeoutException(AmsServiceException): @@ -56,7 +43,7 @@ class AmsTimeoutException(AmsServiceException): was not acknownledged in desired time frame (ackDeadlineSeconds) """ def __init__(self, json, request): - super(AmsServiceException, self).__init__(json, request) + super(AmsTimeoutException, self).__init__(json, request) class AmsConnectionException(AmsException): diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index 489de15..4545961 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -75,7 +75,7 @@ def publish_mock(url, request): try: resp = self.ams.publish("topic1", msg) except Exception as e: - assert isinstance(e, AmsBalancerException) + assert isinstance(e, AmsTimeoutException) self.assertEqual(e.code, 504) # Tests for plaintext or JSON encoded backend error messages @@ -160,25 +160,25 @@ def testBackoffRetryConnectionProblems(self, mock_requests_get): self.assertRaises(AmsConnectionException, self.ams.list_topics) self.assertEqual(mock_requests_get.call_count, retry + 1) - @mock.patch('pymod.ams.requests.get') - def testRetryConnectionAmsTimeout(self, mock_requests_get): + @mock.patch('pymod.ams.requests.post') + def testRetryAmsBalancerTimeout408(self, mock_requests_post): + retry = 4 + retrysleep = 0.2 + errmsg = "

408 Request Time-out

\nYour browser didn't send a complete request in time.\n" mock_response = mock.create_autospec(requests.Response) mock_response.status_code = 408 - mock_response.content = '{"error": {"code": 408, \ - "message": "Ams Timeout", \ - "status": "TIMEOUT"}}' - mock_requests_get.return_value = mock_response - retry = 3 - retrysleep = 0.1 - if sys.version_info < (3, ): - self.ams._retry_make_request.im_func.__defaults__ = (None, None, retry, retrysleep, None) - else: - self.ams._retry_make_request.__func__.__defaults__ = (None, None, retry, retrysleep, None) - self.assertRaises(AmsTimeoutException, self.ams.list_topics) - self.assertEqual(mock_requests_get.call_count, retry + 1) + mock_response.content = errmsg + mock_requests_post.return_value = mock_response + try: + self.ams.pull_sub('subscription1', retry=retry, retrysleep=retrysleep) + except Exception as e: + assert isinstance(e, AmsTimeoutException) + self.assertEqual(e.code, 408) + self.assertEqual(e.msg, 'While trying the [sub_pull]: ' + errmsg) + self.assertEqual(mock_requests_post.call_count, retry + 1) @mock.patch('pymod.ams.requests.post') - def testRetryAmsBalancerTimeout(self, mock_requests_post): + def testRetryAmsBalancerTimeout504(self, mock_requests_post): retry = 4 retrysleep = 0.2 errmsg = "

504 Gateway Time-out

\nThe server didn't respond in time.\n" @@ -189,13 +189,13 @@ def testRetryAmsBalancerTimeout(self, mock_requests_post): try: self.ams.pull_sub('subscription1', retry=retry, retrysleep=retrysleep) except Exception as e: - assert isinstance(e, AmsBalancerException) + assert isinstance(e, AmsTimeoutException) self.assertEqual(e.code, 504) self.assertEqual(e.msg, 'While trying the [sub_pull]: ' + errmsg) self.assertEqual(mock_requests_post.call_count, retry + 1) @mock.patch('pymod.ams.requests.get') - def testRetryConnectionAmsTimeout(self, mock_requests_get): + def testRetryAckDeadlineAmsTimeout(self, mock_requests_get): mock_response = mock.create_autospec(requests.Response) mock_response.status_code = 408 mock_response.content = '{"error": {"code": 408, \ From 6d79474262d4f5bf230865c820f7e585ff0c5dd9 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 6 Nov 2019 12:08:50 +0100 Subject: [PATCH 20/45] Tests for HAProxy 502 and 503 --- tests/test_errorclient.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index 4545961..a363800 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -164,7 +164,7 @@ def testBackoffRetryConnectionProblems(self, mock_requests_get): def testRetryAmsBalancerTimeout408(self, mock_requests_post): retry = 4 retrysleep = 0.2 - errmsg = "

408 Request Time-out

\nYour browser didn't send a complete request in time.\n" + errmsg = "

408 Request Time-out

\nYour browser didn't send a complete request in time.\n\n" mock_response = mock.create_autospec(requests.Response) mock_response.status_code = 408 mock_response.content = errmsg @@ -177,11 +177,45 @@ def testRetryAmsBalancerTimeout408(self, mock_requests_post): self.assertEqual(e.msg, 'While trying the [sub_pull]: ' + errmsg) self.assertEqual(mock_requests_post.call_count, retry + 1) + @mock.patch('pymod.ams.requests.post') + def testRetryAmsBalancer502(self, mock_requests_post): + retry = 4 + retrysleep = 0.2 + errmsg = "

502 Bad Gateway

\nThe server returned an invalid or incomplete response.\n\n" + mock_response = mock.create_autospec(requests.Response) + mock_response.status_code = 502 + mock_response.content = errmsg + mock_requests_post.return_value = mock_response + try: + self.ams.pull_sub('subscription1', retry=retry, retrysleep=retrysleep) + except Exception as e: + assert isinstance(e, AmsBalancerException) + self.assertEqual(e.code, 502) + self.assertEqual(e.msg, 'While trying the [sub_pull]: ' + errmsg) + self.assertEqual(mock_requests_post.call_count, retry + 1) + + @mock.patch('pymod.ams.requests.post') + def testRetryAmsBalancer503(self, mock_requests_post): + retry = 4 + retrysleep = 0.2 + errmsg = "

503 Service Unavailable

\nNo server is available to handle this request.\n\n" + mock_response = mock.create_autospec(requests.Response) + mock_response.status_code = 503 + mock_response.content = errmsg + mock_requests_post.return_value = mock_response + try: + self.ams.pull_sub('subscription1', retry=retry, retrysleep=retrysleep) + except Exception as e: + assert isinstance(e, AmsBalancerException) + self.assertEqual(e.code, 503) + self.assertEqual(e.msg, 'While trying the [sub_pull]: ' + errmsg) + self.assertEqual(mock_requests_post.call_count, retry + 1) + @mock.patch('pymod.ams.requests.post') def testRetryAmsBalancerTimeout504(self, mock_requests_post): retry = 4 retrysleep = 0.2 - errmsg = "

504 Gateway Time-out

\nThe server didn't respond in time.\n" + errmsg = "

504 Gateway Time-out

\nThe server didn't respond in time.\n\n" mock_response = mock.create_autospec(requests.Response) mock_response.status_code = 504 mock_response.content = errmsg From 9f646b0b1211e1e38b2c88cbb550d1c31edd776d Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 6 Nov 2019 16:32:51 +0100 Subject: [PATCH 21/45] Raise saved exception for backoff --- pymod/ams.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 4571fd3..b5f4c62 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -97,12 +97,14 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, i = 1 timeout = reqkwargs.get('timeout', 0) + saved_exp = None if retrybackoff: for sleep_secs in self._gen_backoff_time(retry + 1, retrybackoff): try: return self._make_request(url, body, route_name, **reqkwargs) except (AmsBalancerException, AmsConnectionException, AmsTimeoutException) as e: + saved_exp = e time.sleep(sleep_secs) if timeout: log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, sleep_secs, timeout)) @@ -111,7 +113,7 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, finally: i += 1 else: - raise AmsConnectionException('Backoff retries exhausted', route_name) + raise saved_exp else: while i <= retry + 1: @@ -821,8 +823,8 @@ def has_sub(self, sub, **reqkwargs): except AmsConnectionException as e: raise e - def pull_sub(self, sub, retry=0, retrysleep=60, retrybackoff=None, num=1, - return_immediately=False, **reqkwargs): + def pull_sub(self, sub, num=1, return_immediately=False, retry=0, + retrysleep=60, retrybackoff=None, **reqkwargs): """This function consumes messages from a subscription in a project with a POST request. From 57d75dc58f80a337c146685c4f33c334eaf359c9 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 6 Nov 2019 17:54:53 +0100 Subject: [PATCH 22/45] Comments in the code wrt new retry* options --- pymod/ams.py | 231 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 139 insertions(+), 92 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index b5f4c62..40e29bc 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -92,7 +92,26 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): """ Wrapper around _make_request() that decides whether should request - be retried or not. + be retried or not. If enabled, request will be retried in the + following occassions: + * timeouts from AMS (HTTP 408) or load balancer (HTTP 408 and 504) + * load balancer HTTP 502, 503 + * connection related problems in the lower network layers + Default behaviour is no retry attempts. + + Args: + url: str. The final messaging service endpoint + body: dict. Payload of the request + route_name: str. The name of the route to follow selected from the route list + retry: int. Number of request retries before giving up. Default + is 0 meaning no further request retry will be made + after first unsuccesfull request. + retrysleep: int. Static number of seconds to sleep before next + request attempt + retrybackoff: int. Backoff factor to apply between each request + attempts + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ i = 1 timeout = reqkwargs.get('timeout', 0) @@ -134,8 +153,8 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, def _make_request(self, url, body=None, route_name=None, **reqkwargs): """ Common method for PUT, GET, POST HTTP requests with appropriate - service error handling by differing between AMS and HAProxy - erroneous behaviour. Method is wrapped by _retry_make_request(). + service error handling by differing between AMS and load balancer + erroneous behaviour. """ m = self.routes[route_name][0] decoded = None @@ -173,7 +192,7 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): status_code), request=route_name) - # handle errors coming from HAProxy load balancer + # handle errors coming from load balancer elif (status_code != 200 and route_name in self.balancer_errors_route and status_code in self.balancer_errors_route[route_name][1]): @@ -629,10 +648,11 @@ def pushconfig_sub(self, sub, push_endpoint=None, retry_policy_type='linear', re return p def iter_subs(self, topic=None, **reqkwargs): - """Iterate over AmsSubscription objects + """ + Iterate over AmsSubscription objects - Args: - topic: Iterate over subscriptions only associated to this topic name + Args: + topic: Iterate over subscriptions only associated to this topic name """ self.list_subs(**reqkwargs) @@ -648,7 +668,9 @@ def iter_subs(self, topic=None, **reqkwargs): yield s def iter_topics(self, **reqkwargs): - """Iterate over AmsTopic objects""" + """ + Iterate over AmsTopic objects + """ self.list_topics(**reqkwargs) @@ -661,11 +683,12 @@ def iter_topics(self, **reqkwargs): yield t def list_topics(self, **reqkwargs): - """List the topics of a selected project + """ + List the topics of a selected project - Args: - reqkwargs: keyword argument that will be passed to underlying - python-requests library call + Args: + reqkwargs: keyword argument that will be passed to underlying + python-requests library call """ route = self.routes["topic_list"] # Compose url @@ -684,10 +707,11 @@ def list_topics(self, **reqkwargs): return [] def has_topic(self, topic, **reqkwargs): - """Inspect if topic already exists or not + """ + Inspect if topic already exists or not - Args: - topic: str. Topic name + Args: + topic: str. Topic name """ try: self.get_topic(topic, **reqkwargs) @@ -703,12 +727,13 @@ def has_topic(self, topic, **reqkwargs): raise e def get_topic(self, topic, retobj=False, **reqkwargs): - """Get the details of a selected topic. + """ + Get the details of a selected topic. - Args: - topic: str. Topic name. - retobj: Controls whether method should return AmsTopic object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + topic: str. Topic name. + retobj: Controls whether method should return AmsTopic object + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ route = self.routes["topic_get"] # Compose url @@ -726,18 +751,22 @@ def get_topic(self, topic, retobj=False, **reqkwargs): return r def publish(self, topic, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """Publish a message or list of messages to a selected topic. - - Args: - topic (str): Topic name. - msg (list): A list with one or more messages to send. - Each message is represented as AmsMessage object or python - dictionary with at least data or one attribute key defined. - Kwargs: - reqkwargs: keyword argument that will be passed to underlying + """ + Publish a message or list of messages to a selected topic. If + enabled (retry > 0), multiple topic publishes will be tried in case + of problems/glitches with the AMS service. retry* options are + eventually passed to _retry_make_request() + + Args: + topic (str): Topic name. + msg (list): A list with one or more messages to send. + Each message is represented as AmsMessage object or python + dictionary with at least data or one attribute key defined. + Kwargs: + reqkwargs: keyword argument that will be passed to underlying python-requests library call. - Return: - dict: Dictionary with messageIds of published messages + Return: + dict: Dictionary with messageIds of published messages """ if not isinstance(msg, list): msg = [msg] @@ -756,10 +785,11 @@ def publish(self, topic, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkw return method(url, msg_body, "topic_publish", **reqkwargs) def list_subs(self, **reqkwargs): - """Lists all subscriptions in a project with a GET request. + """ + Lists all subscriptions in a project with a GET request. - Args: - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ route = self.routes["sub_list"] # Compose url @@ -780,12 +810,13 @@ def list_subs(self, **reqkwargs): return [] def get_sub(self, sub, retobj=False, **reqkwargs): - """Get the details of a subscription. + """ + Get the details of a subscription. - Args: - sub: str. The subscription name. - retobj: Controls whether method should return AmsSubscription object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + sub: str. The subscription name. + retobj: Controls whether method should return AmsSubscription object + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ route = self.routes["sub_get"] # Compose url @@ -805,10 +836,11 @@ def get_sub(self, sub, retobj=False, **reqkwargs): return r def has_sub(self, sub, **reqkwargs): - """Inspect if subscription already exists or not + """ + Inspect if subscription already exists or not - Args: - sub: str. The subscription name. + Args: + sub: str. The subscription name. """ try: self.get_sub(sub, **reqkwargs) @@ -825,13 +857,16 @@ def has_sub(self, sub, **reqkwargs): def pull_sub(self, sub, num=1, return_immediately=False, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """This function consumes messages from a subscription in a project - with a POST request. + """ + This function consumes messages from a subscription in a project + with a POST request. If enabled (retry > 0), multiple subscription + pulls will be tried in case of problems/glitches with the AMS service. + retry* options are eventually passed to _retry_make_request() - Args: - sub: str. The subscription name. - num: int. The number of messages to pull. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + sub: str. The subscription name. + num: int. The number of messages to pull. + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ wasmax = self.get_pullopt('maxMessages') @@ -857,14 +892,19 @@ def pull_sub(self, sub, num=1, return_immediately=False, retry=0, def ack_sub(self, sub, ids, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """Messages retrieved from a pull subscription can be acknowledged by sending message with an array of ackIDs. - The service will retrieve the ackID corresponding to the highest message offset and will consider that message - and all previous messages as acknowledged by the consumer. + """ + Messages retrieved from a pull subscription can be acknowledged by + sending message with an array of ackIDs. The service will retrieve + the ackID corresponding to the highest message offset and will + consider that message and all previous messages as acknowledged by + the consumer. If enabled (retry > 0), multiple acknowledgement + will be tried in case of problems/glitches with the AMS service. + retry* options are eventually passed to _retry_make_request() - Args: - sub: str. The subscription name. - ids: list(str). A list of ids of the messages to acknowledge. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + sub: str. The subscription name. + ids: list(str). A list of ids of the messages to acknowledge. + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ msg_body = json.dumps({"ackIds": ids}) @@ -878,44 +918,47 @@ def ack_sub(self, sub, ids, retry=0, retrysleep=60, retrybackoff=None, return True def set_pullopt(self, key, value): - """Function for setting pull options + """ + Function for setting pull options - Args: - key: str. The name of the pull option (ex. maxMessages, returnImmediately).Messaging specific + Args: + key: str. The name of the pull option (ex. maxMessages, returnImmediately). Messaging specific names are allowed. - value: str or int. The name of the pull option (ex. maxMessages, returnImmediately).Messaging specific names - are allowed. + value: str or int. The name of the pull option (ex. maxMessages, returnImmediately). Messaging specific names + are allowed. """ self.pullopts.update({key: str(value)}) def get_pullopt(self, key): - """Function for getting pull options + """ + Function for getting pull options - Args: - key: str. The name of the pull option (ex. maxMessages, returnImmediately).Messaging specific + Args: + key: str. The name of the pull option (ex. maxMessages, returnImmediately).Messaging specific names are allowed. - Returns: - str. The value of the pull option + Returns: + str. The value of the pull option """ return self.pullopts[key] def create_sub(self, sub, topic, ackdeadline=10, push_endpoint=None, retry_policy_type='linear', retry_policy_period=300, retobj=False, **reqkwargs): - """This function creates a new subscription in a project with a PUT request - - Args: - sub: str. The subscription name. - topic: str. The topic name. - ackdeadline: int. It is a custom "ack" deadline (in seconds) in the subscription. If your code doesn't - acknowledge the message in this time, the message is sent again. If you don't specify - the deadline, the default is 10 seconds. - push_endpoint: URL of remote endpoint that should receive messages in push subscription mode - retry_policy_type: - retry_policy_period: - retobj: Controls whether method should return AmsSubscription object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + """ + This function creates a new subscription in a project with a PUT request + + Args: + sub: str. The subscription name. + topic: str. The topic name. + ackdeadline: int. It is a custom "ack" deadline (in seconds) in the subscription. If your code doesn't + acknowledge the message in this time, the message is sent again. If you don't specify + the deadline, the default is 10 seconds. + push_endpoint: URL of remote endpoint that should receive messages in push subscription mode + retry_policy_type: + retry_policy_period: + retobj: Controls whether method should return AmsSubscription object + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ topic = self.get_topic(topic, retobj=True, **reqkwargs) @@ -944,11 +987,12 @@ def create_sub(self, sub, topic, ackdeadline=10, push_endpoint=None, return r def delete_sub(self, sub, **reqkwargs): - """This function deletes a selected subscription in a project + """ + This function deletes a selected subscription in a project - Args: - sub: str. The subscription name. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + sub: str. The subscription name. + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ route = self.routes["sub_delete"] # Compose url @@ -964,7 +1008,8 @@ def delete_sub(self, sub, **reqkwargs): return r def topic(self, topic, **reqkwargs): - """Function create a topic in a project. It's wrapper around few + """ + Function create a topic in a project. It's wrapper around few methods defined in client class. Method will ensure that AmsTopic object is returned either by fetching existing one or creating a new one in case it doesn't exist. @@ -987,12 +1032,13 @@ def topic(self, topic, **reqkwargs): raise e def create_topic(self, topic, retobj=False, **reqkwargs): - """This function creates a topic in a project + """ + This function creates a topic in a project - Args: - topic: str. The topic name. - retobj: Controls whether method should return AmsTopic object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + topic: str. The topic name. + retobj: Controls whether method should return AmsTopic object + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ route = self.routes["topic_create"] # Compose url @@ -1010,11 +1056,12 @@ def create_topic(self, topic, retobj=False, **reqkwargs): return r def delete_topic(self, topic, **reqkwargs): - """ This function deletes a topic in a project + """ + This function deletes a topic in a project - Args: - topic: str. The topic name. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + topic: str. The topic name. + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ route = self.routes["topic_delete"] # Compose url From eccae7b95c9bc17e147c16e71cbc6a06e0c6165a Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 7 Nov 2019 15:57:25 +0100 Subject: [PATCH 23/45] Comment two retry modes --- pymod/ams.py | 62 ++++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 40e29bc..c20a7c2 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -92,26 +92,32 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): """ Wrapper around _make_request() that decides whether should request - be retried or not. If enabled, request will be retried in the - following occassions: + be retried or not. Two request retry modes are available: + 1) static sleep - fixed amount of seconds to sleep between + request attempts + 2) backoff - each next sleep before request attempt is + exponentially longer + + If enabled, request will be retried in the following occassions: * timeouts from AMS (HTTP 408) or load balancer (HTTP 408 and 504) * load balancer HTTP 502, 503 * connection related problems in the lower network layers - Default behaviour is no retry attempts. - Args: - url: str. The final messaging service endpoint - body: dict. Payload of the request - route_name: str. The name of the route to follow selected from the route list - retry: int. Number of request retries before giving up. Default - is 0 meaning no further request retry will be made - after first unsuccesfull request. - retrysleep: int. Static number of seconds to sleep before next - request attempt - retrybackoff: int. Backoff factor to apply between each request - attempts - reqkwargs: keyword argument that will be passed to underlying - python-requests library call. + Default behaviour is no retry attempts. + + Args: + url: str. The final messaging service endpoint + body: dict. Payload of the request + route_name: str. The name of the route to follow selected from the route list + retry: int. Number of request retries before giving up. Default + is 0 meaning no further request retry will be made + after first unsuccesfull request. + retrysleep: int. Static number of seconds to sleep before next + request attempt + retrybackoff: int. Backoff factor to apply between each request + attempts + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ i = 1 timeout = reqkwargs.get('timeout', 0) @@ -214,13 +220,13 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): def do_get(self, url, route_name, **reqkwargs): """ - Method supports all the GET requests. Used for (topics, - subscriptions, messages). + Method supports all the GET requests. Used for (topics, + subscriptions, messages). - Args: - url: str. The final messaging service endpoint - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + url: str. The final messaging service endpoint + route_name: str. The name of the route to follow selected from the route list + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ # try to send a GET request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -274,13 +280,13 @@ def do_post(self, url, body, route_name, retry=0, retrysleep=60, def do_delete(self, url, route_name, **reqkwargs): """ - Delete method that is used to make the appropriate request. - Used for (topics, subscriptions). + Delete method that is used to make the appropriate request. + Used for (topics, subscriptions). - Args: - url: str. The final messaging service endpoint - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Args: + url: str. The final messaging service endpoint + route_name: str. The name of the route to follow selected from the route list + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ # try to send a delete request to the messaging service. # if a connection problem araises a Connection error exception is raised. From f986ad22b7916eab01ea00be1935eb3f279cddb6 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 7 Nov 2019 16:01:09 +0100 Subject: [PATCH 24/45] Add retry arguments for higher level methods --- pymod/amssubscription.py | 5 +++-- pymod/amstopic.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pymod/amssubscription.py b/pymod/amssubscription.py index 9a716a1..19cf643 100644 --- a/pymod/amssubscription.py +++ b/pymod/amssubscription.py @@ -47,7 +47,8 @@ def pushconfig(self, push_endpoint=None, retry_policy_type='linear', retry_polic retry_policy_period=retry_policy_period, **reqkwargs) - def pull(self, num=1, return_immediately=False, **reqkwargs): + def pull(self, num=1, retry=0, retrysleep=60, retrybackoff=None, + return_immediately=False, **reqkwargs): """Pull messages from subscription Kwargs: @@ -119,7 +120,7 @@ def acl(self, users=None, **reqkwargs): else: return self.init.modifyacl_sub(self.name, users, **reqkwargs) - def ack(self, ids, **reqkwargs): + def ack(self, ids, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): """Acknowledge receive of messages Kwargs: diff --git a/pymod/amstopic.py b/pymod/amstopic.py index 86f734f..6361112 100644 --- a/pymod/amstopic.py +++ b/pymod/amstopic.py @@ -75,7 +75,7 @@ def iter_subs(self): for s in self.init.iter_subs(topic=self.name): yield s - def publish(self, msg, **reqkwargs): + def publish(self, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): """Publish message to topic Args: @@ -89,4 +89,6 @@ def publish(self, msg, **reqkwargs): dict: Dictionary with messageIds of published messages """ - return self.init.publish(self.name, msg, **reqkwargs) + return self.init.publish(self.name, msg, retry=retry, + retrysleep=retrysleep, + retrybackoff=retrybackoff, **reqkwargs) From 862b5479e76a90cf32075444a41870712989a61b Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 7 Nov 2019 17:23:56 +0100 Subject: [PATCH 25/45] Fix backoff loop with correct number of attempts --- pymod/ams.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index c20a7c2..b0dbe49 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -124,21 +124,25 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, saved_exp = None if retrybackoff: - for sleep_secs in self._gen_backoff_time(retry + 1, retrybackoff): - try: - return self._make_request(url, body, route_name, **reqkwargs) - except (AmsBalancerException, AmsConnectionException, - AmsTimeoutException) as e: - saved_exp = e - time.sleep(sleep_secs) - if timeout: - log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, sleep_secs, timeout)) - else: - log.warning('Backoff retry #{0} after {1} seconds'.format(i, sleep_secs)) - finally: - i += 1 - else: - raise saved_exp + try: + return self._make_request(url, body, route_name, **reqkwargs) + except (AmsBalancerException, AmsConnectionException, + AmsTimeoutException) as e: + for sleep_secs in self._gen_backoff_time(retry, retrybackoff): + try: + return self._make_request(url, body, route_name, **reqkwargs) + except (AmsBalancerException, AmsConnectionException, + AmsTimeoutException) as e: + saved_exp = e + time.sleep(sleep_secs) + if timeout: + log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, sleep_secs, timeout)) + else: + log.warning('Backoff retry #{0} after {1} seconds'.format(i, sleep_secs)) + finally: + i += 1 + else: + raise saved_exp else: while i <= retry + 1: From b8f2c131686f3743811cb39742d969b9cb83bd90 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 14 Nov 2019 12:54:31 +0100 Subject: [PATCH 26/45] Code comments for AmsTimeout load balancer exception --- pymod/amsexceptions.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/pymod/amsexceptions.py b/pymod/amsexceptions.py index 95dc3ed..c82642b 100644 --- a/pymod/amsexceptions.py +++ b/pymod/amsexceptions.py @@ -1,17 +1,15 @@ import json class AmsException(Exception): - """ - Base exception class for all Argo Messaging service related errors - """ + """Base exception class for all Argo Messaging service related errors""" + def __init__(self, *args, **kwargs): super(AmsException, self).__init__(*args, **kwargs) class AmsServiceException(AmsException): - """ - Exception for Argo Messaging Service API errors - """ + """Exception for Argo Messaging Service API errors""" + def __init__(self, json, request): errord = dict() @@ -30,35 +28,35 @@ def __init__(self, json, request): class AmsBalancerException(AmsServiceException): - """ - Exception for HAProxy Argo Messaging Service errors - """ + """Exception for load balancer Argo Messaging Service errors""" + def __init__(self, json, request): super(AmsBalancerException, self).__init__(json, request) class AmsTimeoutException(AmsServiceException): - """ - Exception for timeout returned by the Argo Messaging Service if message - was not acknownledged in desired time frame (ackDeadlineSeconds) + """Exception for timeouts errors + + Timeouts can be generated by the Argo Messaging Service if message was + not acknownledged in desired time frame (ackDeadlineSeconds). Also, 408 + timeouts can come from load balancer for partial requests that were not + completed in required time frame. """ def __init__(self, json, request): super(AmsTimeoutException, self).__init__(json, request) class AmsConnectionException(AmsException): - """ - Exception for connection related problems catched from requests library - """ + """Exception for connection related problems catched from requests library""" + def __init__(self, exp, request): self.msg = "While trying the [{0}]: {1}".format(request, repr(exp)) super(AmsConnectionException, self).__init__(self.msg) class AmsMessageException(AmsException): - """ - Exception that indicate problems with constructing message - """ + """Exception that indicate problems with constructing message""" + def __init__(self, msg): self.msg = msg super(AmsMessageException, self).__init__(self.msg) From 1b81c212b27c9e3ad93e51c5a3466bb7c0a5e042 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 14 Nov 2019 13:10:54 +0100 Subject: [PATCH 27/45] Refactored code comments --- pymod/ams.py | 255 ++++++++++++++++++++------------------- pymod/amssubscription.py | 17 ++- pymod/amstopic.py | 17 ++- 3 files changed, 158 insertions(+), 131 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index b0dbe49..ccd2273 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -23,11 +23,11 @@ class AmsHttpRequests(object): - """ - Class encapsulates methods used by ArgoMessagingService. Each method - represent HTTP request made to AMS with the help of requests library. - Proper service error handling is implemented according to HTTP status - codes returned by service and the balancer. + """Class encapsulates methods used by ArgoMessagingService. + + Each method represent HTTP request made to AMS with the help of requests + library. service error handling is implemented according to HTTP + status codes returned by service and the balancer. """ def __init__(self): # Create route list @@ -90,9 +90,10 @@ def _gen_backoff_time(self, try_number, backoff_factor): def _retry_make_request(self, url, body=None, route_name=None, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """ - Wrapper around _make_request() that decides whether should request - be retried or not. Two request retry modes are available: + """Wrapper around _make_request() that decides whether should request + be retried or not. + + Two request retry modes are available: 1) static sleep - fixed amount of seconds to sleep between request attempts 2) backoff - each next sleep before request attempt is @@ -103,7 +104,8 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, * load balancer HTTP 502, 503 * connection related problems in the lower network layers - Default behaviour is no retry attempts. + Default behaviour is no retry attempts. If both, retry and + retrybackoff are enabled, retrybackoff will take precedence. Args: url: str. The final messaging service endpoint @@ -161,8 +163,7 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, i += 1 def _make_request(self, url, body=None, route_name=None, **reqkwargs): - """ - Common method for PUT, GET, POST HTTP requests with appropriate + """Common method for PUT, GET, POST HTTP requests with appropriate service error handling by differing between AMS and load balancer erroneous behaviour. """ @@ -223,14 +224,15 @@ def _make_request(self, url, body=None, route_name=None, **reqkwargs): return decoded if decoded else {} def do_get(self, url, route_name, **reqkwargs): - """ - Method supports all the GET requests. Used for (topics, - subscriptions, messages). + """Method supports all the GET requests. + + Used for (topics, subscriptions, messages). Args: url: str. The final messaging service endpoint route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ # try to send a GET request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -241,15 +243,18 @@ def do_get(self, url, route_name, **reqkwargs): raise e def do_put(self, url, body, route_name, **reqkwargs): - """ - Method supports all the PUT requests. Used for (topics, - subscriptions, messages). + """Method supports all the PUT requests. + + Used for (topics, subscriptions, messages). Args: url: str. The final messaging service endpoint - body: dict. Body the post data to send based on the PUT request. The post data is always in json format. - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + body: dict. Body the post data to send based on the PUT request. + The post data is always in json format. + route_name: str. The name of the route to follow selected from + the route list + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ # try to send a PUT request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -261,15 +266,18 @@ def do_put(self, url, body, route_name, **reqkwargs): def do_post(self, url, body, route_name, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """ - Method supports all the POST requests. Used for (topics, - subscriptions, messages). + """Method supports all the POST requests. + + Used for (topics, subscriptions, messages). Args: url: str. The final messaging service endpoint - body: dict. Body the post data to send based on the PUT request. The post data is always in json format. - route_name: str. The name of the route to follow selected from the route list - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + body: dict. Body the post data to send based on the PUT request. + The post data is always in json format. + route_name: str. The name of the route to follow selected from + the route list + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ # try to send a Post request to the messaging service. # if a connection problem araises a Connection error exception is raised. @@ -283,8 +291,8 @@ def do_post(self, url, body, route_name, retry=0, retrysleep=60, raise e def do_delete(self, url, route_name, **reqkwargs): - """ - Delete method that is used to make the appropriate request. + """Delete method that is used to make the appropriate request. + Used for (topics, subscriptions). Args: @@ -317,10 +325,10 @@ def do_delete(self, url, route_name, **reqkwargs): class ArgoMessagingService(AmsHttpRequests): - """ + """Class is entry point for client code. + Class abstract Argo Messaging Service by covering all available HTTP API - calls that are wrapped in series of methods. Class is entry point for - client code. + calls that are wrapped in series of methods. """ def __init__(self, endpoint, token="", project="", cert="", key="", authn_port=8443): super(ArgoMessagingService, self).__init__() @@ -336,8 +344,7 @@ def __init__(self, endpoint, token="", project="", cert="", key="", authn_port=8 self.subs = OrderedDict() def assign_token(self, token, cert, key): - """ - Assign a token to the ams object + """Assign a token to the ams object Args: token(str): a valid ams token @@ -365,8 +372,7 @@ def assign_token(self, token, cert, key): raise e def auth_via_cert(self, cert, key, **reqkwargs): - """ - Retrieve an ams token based on the provided certificate + """Retrieve an ams token based on the provided certificate Args: cert(str): a path to a valid certificate file @@ -415,8 +421,7 @@ def _delete_topic_obj(self, t): del self.topics[t['name']] def getacl_topic(self, topic, **reqkwargs): - """ - Get access control lists for topic + """Get access control lists for topic Args: topic (str): The topic name. @@ -443,8 +448,7 @@ def getacl_topic(self, topic, **reqkwargs): return [] def modifyacl_topic(self, topic, users, **reqkwargs): - """ - Modify access control lists for topic + """Modify access control lists for topic Args: topic (str): The topic name. @@ -477,8 +481,7 @@ def modifyacl_topic(self, topic, users, **reqkwargs): raise e def getacl_sub(self, sub, **reqkwargs): - """ - Get access control lists for subscription + """Get access control lists for subscription Args: sub (str): The subscription name. @@ -505,8 +508,7 @@ def getacl_sub(self, sub, **reqkwargs): return [] def getoffsets_sub(self, sub, offset='all', **reqkwargs): - """ - Retrieve the current positions of min,max and current offsets. + """Retrieve the current positions of min,max and current offsets. Args: sub (str): The subscription name. @@ -531,8 +533,7 @@ def getoffsets_sub(self, sub, offset='all', **reqkwargs): raise AmsServiceException(json=errormsg, request="sub_offsets") def time_to_offset_sub(self, sub, timestamp, **reqkwargs): - """ - Retrieve the closest(greater than) available offset to the given timestamp. + """Retrieve the closest(greater than) available offset to the given timestamp. Args: sub (str): The subscription name. @@ -563,8 +564,7 @@ def time_to_offset_sub(self, sub, timestamp, **reqkwargs): raise e def modifyoffset_sub(self, sub, move_to, **reqkwargs): - """ - Modify the position of the current offset. + """Modify the position of the current offset. Args: sub (str): The subscription name. @@ -592,8 +592,7 @@ def modifyoffset_sub(self, sub, move_to, **reqkwargs): raise e def modifyacl_sub(self, sub, users, **reqkwargs): - """ - Modify access control lists for subscription + """Modify access control lists for subscription Args: sub (str): The subscription name. @@ -625,8 +624,7 @@ def modifyacl_sub(self, sub, users, **reqkwargs): raise e def pushconfig_sub(self, sub, push_endpoint=None, retry_policy_type='linear', retry_policy_period=300, **reqkwargs): - """ - Modify push configuration of given subscription + """Modify push configuration of given subscription Args: sub: shortname of subscription @@ -658,8 +656,7 @@ def pushconfig_sub(self, sub, push_endpoint=None, retry_policy_type='linear', re return p def iter_subs(self, topic=None, **reqkwargs): - """ - Iterate over AmsSubscription objects + """Iterate over AmsSubscription objects Args: topic: Iterate over subscriptions only associated to this topic name @@ -678,9 +675,7 @@ def iter_subs(self, topic=None, **reqkwargs): yield s def iter_topics(self, **reqkwargs): - """ - Iterate over AmsTopic objects - """ + """Iterate over AmsTopic objects""" self.list_topics(**reqkwargs) @@ -693,8 +688,7 @@ def iter_topics(self, **reqkwargs): yield t def list_topics(self, **reqkwargs): - """ - List the topics of a selected project + """List the topics of a selected project Args: reqkwargs: keyword argument that will be passed to underlying @@ -717,8 +711,7 @@ def list_topics(self, **reqkwargs): return [] def has_topic(self, topic, **reqkwargs): - """ - Inspect if topic already exists or not + """Inspect if topic already exists or not Args: topic: str. Topic name @@ -737,13 +730,13 @@ def has_topic(self, topic, **reqkwargs): raise e def get_topic(self, topic, retobj=False, **reqkwargs): - """ - Get the details of a selected topic. + """Get the details of a selected topic. Args: topic: str. Topic name. retobj: Controls whether method should return AmsTopic object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ route = self.routes["topic_get"] # Compose url @@ -761,10 +754,10 @@ def get_topic(self, topic, retobj=False, **reqkwargs): return r def publish(self, topic, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """ - Publish a message or list of messages to a selected topic. If - enabled (retry > 0), multiple topic publishes will be tried in case - of problems/glitches with the AMS service. retry* options are + """Publish a message or list of messages to a selected topic. + + If enabled (retry > 0), multiple topic publishes will be tried in + case of problems/glitches with the AMS service. retry* options are eventually passed to _retry_make_request() Args: @@ -773,6 +766,13 @@ def publish(self, topic, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkw Each message is represented as AmsMessage object or python dictionary with at least data or one attribute key defined. Kwargs: + retry: int. Number of request retries before giving up. Default + is 0 meaning no further request retry will be made + after first unsuccesfull request. + retrysleep: int. Static number of seconds to sleep before next + request attempt + retrybackoff: int. Backoff factor to apply between each request + attempts reqkwargs: keyword argument that will be passed to underlying python-requests library call. Return: @@ -795,11 +795,11 @@ def publish(self, topic, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkw return method(url, msg_body, "topic_publish", **reqkwargs) def list_subs(self, **reqkwargs): - """ - Lists all subscriptions in a project with a GET request. + """Lists all subscriptions in a project with a GET request. Args: - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ route = self.routes["sub_list"] # Compose url @@ -820,13 +820,13 @@ def list_subs(self, **reqkwargs): return [] def get_sub(self, sub, retobj=False, **reqkwargs): - """ - Get the details of a subscription. + """Get the details of a subscription. Args: sub: str. The subscription name. retobj: Controls whether method should return AmsSubscription object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ route = self.routes["sub_get"] # Compose url @@ -846,8 +846,7 @@ def get_sub(self, sub, retobj=False, **reqkwargs): return r def has_sub(self, sub, **reqkwargs): - """ - Inspect if subscription already exists or not + """Inspect if subscription already exists or not Args: sub: str. The subscription name. @@ -867,16 +866,18 @@ def has_sub(self, sub, **reqkwargs): def pull_sub(self, sub, num=1, return_immediately=False, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """ - This function consumes messages from a subscription in a project - with a POST request. If enabled (retry > 0), multiple subscription - pulls will be tried in case of problems/glitches with the AMS service. - retry* options are eventually passed to _retry_make_request() + """This function consumes messages from a subscription in a project + with a POST request. - Args: - sub: str. The subscription name. - num: int. The number of messages to pull. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + If enabled (retry > 0), multiple subscription pulls will be tried in + case of problems/glitches with the AMS service. retry* options are + eventually passed to _retry_make_request() + + Args: + sub: str. The subscription name. + num: int. The number of messages to pull. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ wasmax = self.get_pullopt('maxMessages') @@ -902,19 +903,20 @@ def pull_sub(self, sub, num=1, return_immediately=False, retry=0, def ack_sub(self, sub, ids, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): - """ - Messages retrieved from a pull subscription can be acknowledged by - sending message with an array of ackIDs. The service will retrieve - the ackID corresponding to the highest message offset and will - consider that message and all previous messages as acknowledged by - the consumer. If enabled (retry > 0), multiple acknowledgement - will be tried in case of problems/glitches with the AMS service. - retry* options are eventually passed to _retry_make_request() + """Acknownledgment of received messages - Args: - sub: str. The subscription name. - ids: list(str). A list of ids of the messages to acknowledge. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + Messages retrieved from a pull subscription can be acknowledged by + sending message with an array of ackIDs. The service will retrieve + the ackID corresponding to the highest message offset and will + consider that message and all previous messages as acknowledged by + the consumer. If enabled (retry > 0), multiple acknowledgement + will be tried in case of problems/glitches with the AMS service. + retry* options are eventually passed to _retry_make_request() + + Args: + sub: str. The subscription name. + ids: list(str). A list of ids of the messages to acknowledge. + reqkwargs: keyword argument that will be passed to underlying python-requests library call. """ msg_body = json.dumps({"ackIds": ids}) @@ -928,25 +930,23 @@ def ack_sub(self, sub, ids, retry=0, retrysleep=60, retrybackoff=None, return True def set_pullopt(self, key, value): - """ - Function for setting pull options + """Function for setting pull options Args: key: str. The name of the pull option (ex. maxMessages, returnImmediately). Messaging specific - names are allowed. - value: str or int. The name of the pull option (ex. maxMessages, returnImmediately). Messaging specific names - are allowed. + names are allowed. + value: str or int. The name of the pull option (ex. maxMessages, + returnImmediately). Messaging specific names are allowed. """ self.pullopts.update({key: str(value)}) def get_pullopt(self, key): - """ - Function for getting pull options + """Function for getting pull options Args: - key: str. The name of the pull option (ex. maxMessages, returnImmediately).Messaging specific - names are allowed. + key: str. The name of the pull option (ex. maxMessages, + returnImmediately). Messaging specific names are allowed. Returns: str. The value of the pull option @@ -955,20 +955,23 @@ def get_pullopt(self, key): def create_sub(self, sub, topic, ackdeadline=10, push_endpoint=None, retry_policy_type='linear', retry_policy_period=300, retobj=False, **reqkwargs): - """ - This function creates a new subscription in a project with a PUT request + """This function creates a new subscription in a project with a PUT request Args: sub: str. The subscription name. topic: str. The topic name. - ackdeadline: int. It is a custom "ack" deadline (in seconds) in the subscription. If your code doesn't - acknowledge the message in this time, the message is sent again. If you don't specify + ackdeadline: int. It is a custom "ack" deadline (in seconds) in + the subscription. If your code doesn't + acknowledge the message in this time, the + message is sent again. If you don't specify the deadline, the default is 10 seconds. - push_endpoint: URL of remote endpoint that should receive messages in push subscription mode + push_endpoint: URL of remote endpoint that should receive + messages in push subscription mode retry_policy_type: retry_policy_period: retobj: Controls whether method should return AmsSubscription object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ topic = self.get_topic(topic, retobj=True, **reqkwargs) @@ -997,12 +1000,12 @@ def create_sub(self, sub, topic, ackdeadline=10, push_endpoint=None, return r def delete_sub(self, sub, **reqkwargs): - """ - This function deletes a selected subscription in a project + """This function deletes a selected subscription in a project Args: sub: str. The subscription name. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ route = self.routes["sub_delete"] # Compose url @@ -1018,11 +1021,11 @@ def delete_sub(self, sub, **reqkwargs): return r def topic(self, topic, **reqkwargs): - """ - Function create a topic in a project. It's wrapper around few - methods defined in client class. Method will ensure that AmsTopic - object is returned either by fetching existing one or creating - a new one in case it doesn't exist. + """Function create a topic in a project. + + It's wrapper around few methods defined in client class. Method will + ensure that AmsTopic object is returned either by fetching existing + one or creating a new one in case it doesn't exist. Args: topic (str): The topic name @@ -1042,13 +1045,13 @@ def topic(self, topic, **reqkwargs): raise e def create_topic(self, topic, retobj=False, **reqkwargs): - """ - This function creates a topic in a project + """This function creates a topic in a project Args: topic: str. The topic name. retobj: Controls whether method should return AmsTopic object - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ route = self.routes["topic_create"] # Compose url @@ -1066,12 +1069,12 @@ def create_topic(self, topic, retobj=False, **reqkwargs): return r def delete_topic(self, topic, **reqkwargs): - """ - This function deletes a topic in a project + """This function deletes a topic in a project Args: topic: str. The topic name. - reqkwargs: keyword argument that will be passed to underlying python-requests library call. + reqkwargs: keyword argument that will be passed to underlying + python-requests library call. """ route = self.routes["topic_delete"] # Compose url diff --git a/pymod/amssubscription.py b/pymod/amssubscription.py index 19cf643..9ccd60c 100644 --- a/pymod/amssubscription.py +++ b/pymod/amssubscription.py @@ -29,7 +29,8 @@ def delete(self): return self.init.delete_sub(self.name) - def pushconfig(self, push_endpoint=None, retry_policy_type='linear', retry_policy_period=300, **reqkwargs): + def pushconfig(self, push_endpoint=None, retry_policy_type='linear', + retry_policy_period=300, **reqkwargs): """Configure Push mode parameters of subscription. When push_endpoint is defined, subscription will automatically start to send messages to it. @@ -53,6 +54,13 @@ def pull(self, num=1, retry=0, retrysleep=60, retrybackoff=None, Kwargs: num (int): Number of messages to pull + retry: int. Number of request retries before giving up. Default + is 0 meaning no further request retry will be made + after first unsuccesfull request. + retrysleep: int. Static number of seconds to sleep before next + request attempt + retrybackoff: int. Backoff factor to apply between each request + attempts return_immediately (boolean): If True and if stream of messages is empty, subscriber call will not block and wait for messages @@ -125,6 +133,13 @@ def ack(self, ids, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): Kwargs: ids (list): A list of ackIds of the messages to acknowledge. + retry: int. Number of request retries before giving up. Default + is 0 meaning no further request retry will be made + after first unsuccesfull request. + retrysleep: int. Static number of seconds to sleep before next + request attempt + retrybackoff: int. Backoff factor to apply between each request + attempts """ return self.init.ack_sub(self.name, ids, **reqkwargs) diff --git a/pymod/amstopic.py b/pymod/amstopic.py index 6361112..3231fce 100644 --- a/pymod/amstopic.py +++ b/pymod/amstopic.py @@ -27,10 +27,11 @@ def delete(self): return self.init.delete_topic(self.name) def subscription(self, sub, ackdeadline=10, **reqkwargs): - """Create a subscription for the topic. It's wrapper around few - methods defined in client class. Method will ensure that AmsSubscription - object is returned either by fetching existing one or creating - a new one in case it doesn't exist. + """Create a subscription for the topic. + + It's wrapper around few methods defined in client class. Method will + ensure that AmsSubscription object is returned either by fetching + existing one or creating a new one in case it doesn't exist. Args: sub (str): Name of the subscription @@ -82,6 +83,14 @@ def publish(self, msg, retry=0, retrysleep=60, retrybackoff=None, **reqkwargs): msg (list, dict): One or list of dictionaries representing AMS Message Kwargs: + retry: int. Number of request retries before giving up. Default + is 0 meaning no further request retry will be made + after first unsuccesfull request. + retrysleep: int. Static number of seconds to sleep before next + request attempt + retrybackoff: int. Backoff factor to apply between each request + attempts + reqkwargs: Keyword argument that will be passed to underlying python-requests library call. From e54084a13bdbb7a7911910dea6048bb2ed563e42 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 25 Nov 2019 12:02:19 +0100 Subject: [PATCH 28/45] Fix check with missing attribute --- pymod/ams.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index ccd2273..94cff75 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -56,18 +56,29 @@ def __init__(self): # HTTP error status codes returned by AMS according to: # http://argoeu.github.io/messaging/v1/api_errors/ self.ams_errors_route = {"topic_create": ["put", set([409, 401, 403])], - "topic_list": ["get", set([400, 401, 403, 404])], - "sub_create": ["put", set([400, 409, 408, 401, 403])], - "sub_ack": ["post", set([408, 400, 401, 403, 404])], - "topic_get": ["get", set([404, 401, 403])], - "topic_modifyacl": ["post", set([400, 401, 403, 404])], - "sub_get": ["get", set([404, 401, 403])], - "topic_publish": ["post", set([413, 401, 403])], - "sub_pushconfig": ["post", set([400, 401, 403, 404])], - "auth_x509": ["post", set([400, 401, 403, 404])], - "sub_pull": ["post", set([400, 401, 403, 404])], - "sub_timeToOffset": ["get", set([400, 401, 403, 404, 409])] - } + "topic_list": ["get", set([400, 401, 403, + 404])], + "topic_delete": ["delete", set([401, 403, + 404])], + "sub_create": ["put", set([400, 409, 408, 401, + 403])], + "sub_ack": ["post", set([408, 400, 401, 403, + 404])], + "topic_get": ["get", set([404, 401, 403])], + "topic_modifyacl": ["post", set([400, 401, + 403, 404])], + "sub_get": ["get", set([404, 401, 403])], + "topic_publish": ["post", set([413, 401, + 403])], + "sub_pushconfig": ["post", set([400, 401, 403, + 404])], + "auth_x509": ["post", set([400, 401, 403, + 404])], + "sub_pull": ["post", set([400, 401, 403, + 404])], + "sub_timeToOffset": ["get", set([400, 401, + 403, 404, + 409])]} # https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#1.3 self.balancer_errors_route = {"sub_ack": ["post", set([500, 502, 503, 504])], "sub_pull": ["post", set([500, 502, 503, 504])], @@ -302,20 +313,15 @@ def do_delete(self, url, route_name, **reqkwargs): """ # try to send a delete request to the messaging service. # if a connection problem araises a Connection error exception is raised. - m = self.routes[route_name][0] try: # the delete request based on requests. r = requests.delete(url, **reqkwargs) # JSON error returned by AMS - if r.status_code != 200 and r.status_code in self.errors[m]: + if r.status_code != 200: errormsg = self._error_dict(r.content, r.status_code) raise AmsServiceException(json=errormsg, request=route_name) - # handle other erroneous behaviour - elif r.status_code != 200 and r.status_code not in self.errors[m]: - errormsg = self._error_dict(r.content, r.status_code) - raise AmsServiceException(json=errormsg, request=route_name) else: return True From b4de9018c6356e76162ca2eec48232cb1a4ec209 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 25 Nov 2019 12:03:52 +0100 Subject: [PATCH 29/45] Test for topic delete --- tests/amsmocks.py | 8 ++++++++ tests/test_errorclient.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/tests/amsmocks.py b/tests/amsmocks.py index dc55527..3435725 100644 --- a/tests/amsmocks.py +++ b/tests/amsmocks.py @@ -117,12 +117,20 @@ class ErrorMocks(object): create_subscription_urlmatch = dict(netloc="localhost", path="/v1/projects/TEST/subscriptions/subscription1", method="PUT") + delete_topic_urlmatch = dict(netloc="localhost", + path="/v1/projects/TEST/topics/topic1", + method='DELETE') # Mock ALREADY_EXIST error response for PUT topic request @urlmatch(**create_topic_urlmatch) def create_topic_alreadyexist_mock(self, url, request): return response(409, '{"error":{"code": 409,"message":"Topic already exists","status":"ALREADY_EXIST"}}', None, None, 5, request) + # Mock NOT_FOUND error response for DELETE topic request + @urlmatch(**delete_topic_urlmatch) + def delete_topic_notfound_mock(self, url, request): + return response(404, '{"error":{"code": 404,"message":"Topic does not exist","status":"NOT_FOUND"}}', None, None, 5, request) + # Mock ALREADY_EXIST error response for PUT subscription request @urlmatch(**create_subscription_urlmatch) def create_subscription_alreadyexist_mock(self, url, request): diff --git a/tests/test_errorclient.py b/tests/test_errorclient.py index a363800..70559f9 100644 --- a/tests/test_errorclient.py +++ b/tests/test_errorclient.py @@ -45,6 +45,17 @@ def testCreateTopics(self): self.assertEqual(e.status, 'ALREADY_EXIST') self.assertEqual(e.code, 409) + # Test delete topic client request + def testDeleteTopics(self): + # Execute ams client with mocked response + with HTTMock(self.errormocks.delete_topic_notfound_mock): + try: + resp = self.ams.delete_topic("topic1") + except Exception as e: + assert isinstance(e, AmsServiceException) + self.assertEqual(e.status, 'NOT_FOUND') + self.assertEqual(e.code, 404) + def testCreateSubscription(self): # Execute ams client with mocked response with HTTMock(self.topicmocks.get_topic_mock, From 51085331cfa903b56fc9bba19238ffa748c5cbd5 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 25 Nov 2019 12:35:15 +0100 Subject: [PATCH 30/45] Convert bytes to stringi for Py34 --- pymod/ams.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pymod/ams.py b/pymod/ams.py index 94cff75..a158d7b 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -88,6 +88,9 @@ def _error_dict(self, response_content, status): error_dict = dict() try: + if (response_content and sys.version_info < (3, 6, ) and + isinstance(response_content, bytes)): + response_content = response_content.decode() error_dict = json.loads(response_content) if response_content else {} except ValueError: error_dict = {'error': {'code': status, 'message': response_content}} From 77fc3dce5be958e22163b8561e68833ec419d21b Mon Sep 17 00:00:00 2001 From: Kostas Koumantaros Date: Tue, 3 Dec 2019 17:27:08 +0200 Subject: [PATCH 31/45] Update argo-ams-library.spec --- argo-ams-library.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argo-ams-library.spec b/argo-ams-library.spec index cfb2ae3..75b9e5e 100644 --- a/argo-ams-library.spec +++ b/argo-ams-library.spec @@ -6,7 +6,7 @@ Name: argo-ams-library Summary: %{sum} -Version: 0.4.3 +Version: 0.5.0 Release: 1%{?dist} Group: Development/Libraries From 321fbb1eb2ce857f4f08f56b9791c9f5475bc350 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 4 Dec 2019 17:33:32 +0100 Subject: [PATCH 32/45] Added Changelog entry --- argo-ams-library.spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/argo-ams-library.spec b/argo-ams-library.spec index 75b9e5e..d6e2b34 100644 --- a/argo-ams-library.spec +++ b/argo-ams-library.spec @@ -91,6 +91,8 @@ rm -rf %{buildroot} %changelog +* Wed Dec 4 2019 Daniel Vrcic - 0.5.0-1%{?dist} +- ARGO-1481 Connection retry logic in ams-library * Fri Nov 8 2019 Daniel Vrcic , agelostsal - 0.4.3-1%{?dist} - ARGO-1990 Fix runtime dependencies - ARGO-1862 Make argo-ams-library Python 3 ready From e80c01a2dc1a35cd971f1c06b6c74a88012fccf2 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Tue, 5 Nov 2019 10:58:52 +0100 Subject: [PATCH 33/45] update gitignore --- .gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.gitignore b/.gitignore index 0d20b64..65d20f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,14 @@ +*.gitignore +*.ropeproject *.pyc +__pycache__ +*.values +MANIFEST +*.gz +*.rpm +*.pyc +*.egg* +*egg-info* +.tox* +coverage.xml +.coverage From 80c527d0da823a7fb9931e4eb4b3b510b6ed8f26 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Fri, 8 Nov 2019 13:29:06 +0100 Subject: [PATCH 34/45] spec bump for release --- argo-ams-library.spec | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/argo-ams-library.spec b/argo-ams-library.spec index dabf4ce..cfb2ae3 100644 --- a/argo-ams-library.spec +++ b/argo-ams-library.spec @@ -6,7 +6,7 @@ Name: argo-ams-library Summary: %{sum} -Version: 0.4.2 +Version: 0.4.3 Release: 1%{?dist} Group: Development/Libraries @@ -91,6 +91,10 @@ rm -rf %{buildroot} %changelog +* Fri Nov 8 2019 Daniel Vrcic , agelostsal - 0.4.3-1%{?dist} +- ARGO-1990 Fix runtime dependencies +- ARGO-1862 Make argo-ams-library Python 3 ready +- ARGO-1841 Update the ams library to include the new timeToOffset functionality * Thu Jul 26 2018 agelostsal - 0.4.2-1%{?dist} - ARGO-1479 Subscription create methods don't delegate **reqkwargs where needed - Error handling bug during list_topic route From 35e04b31f2ee78b35808efb6c7de97c0ec667d94 Mon Sep 17 00:00:00 2001 From: Kostas Koumantaros Date: Tue, 3 Dec 2019 17:27:08 +0200 Subject: [PATCH 35/45] Update argo-ams-library.spec --- argo-ams-library.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argo-ams-library.spec b/argo-ams-library.spec index cfb2ae3..75b9e5e 100644 --- a/argo-ams-library.spec +++ b/argo-ams-library.spec @@ -6,7 +6,7 @@ Name: argo-ams-library Summary: %{sum} -Version: 0.4.3 +Version: 0.5.0 Release: 1%{?dist} Group: Development/Libraries From 7d9e5703d189afb07d217942d241351f208a825f Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 4 Dec 2019 17:33:32 +0100 Subject: [PATCH 36/45] Added Changelog entry --- argo-ams-library.spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/argo-ams-library.spec b/argo-ams-library.spec index 75b9e5e..d6e2b34 100644 --- a/argo-ams-library.spec +++ b/argo-ams-library.spec @@ -91,6 +91,8 @@ rm -rf %{buildroot} %changelog +* Wed Dec 4 2019 Daniel Vrcic - 0.5.0-1%{?dist} +- ARGO-1481 Connection retry logic in ams-library * Fri Nov 8 2019 Daniel Vrcic , agelostsal - 0.4.3-1%{?dist} - ARGO-1990 Fix runtime dependencies - ARGO-1862 Make argo-ams-library Python 3 ready From 4aa3630b8d07e47b9f7c497fddaeb7e8a440ff17 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 5 Dec 2019 10:47:51 +0100 Subject: [PATCH 37/45] Print catched exception on retry --- pymod/ams.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index a158d7b..6d57262 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -152,9 +152,9 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, saved_exp = e time.sleep(sleep_secs) if timeout: - log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, sleep_secs, timeout)) + log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds - {3}'.format(i, sleep_secs, timeout, e)) else: - log.warning('Backoff retry #{0} after {1} seconds'.format(i, sleep_secs)) + log.warning('Backoff retry #{0} after {1} seconds - {3}'.format(i, sleep_secs, e)) finally: i += 1 else: @@ -170,9 +170,9 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, else: time.sleep(retrysleep) if timeout: - log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds'.format(i, retrysleep, timeout)) + log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds - {3}'.format(i, retrysleep, timeout, e)) else: - log.warning('Retry #{0} after {1} seconds'.format(i, retrysleep)) + log.warning('Retry #{0} after {1} seconds - {2}'.format(i, retrysleep, e)) finally: i += 1 From 45885c593757c6c7b0e7e74aadb683c213480020 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Thu, 5 Dec 2019 11:19:45 +0100 Subject: [PATCH 38/45] Fix index of format argument --- pymod/ams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymod/ams.py b/pymod/ams.py index 6d57262..069c32f 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -154,7 +154,7 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, if timeout: log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds - {3}'.format(i, sleep_secs, timeout, e)) else: - log.warning('Backoff retry #{0} after {1} seconds - {3}'.format(i, sleep_secs, e)) + log.warning('Backoff retry #{0} after {1} seconds - {2}'.format(i, sleep_secs, e)) finally: i += 1 else: From 07ba83353fdc1b736ada56a9af45edc4dfcf7e68 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 16 Dec 2019 10:30:55 +0100 Subject: [PATCH 39/45] Print AMS hostname on retry log messages --- pymod/ams.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pymod/ams.py b/pymod/ams.py index 069c32f..3952ef0 100644 --- a/pymod/ams.py +++ b/pymod/ams.py @@ -29,7 +29,8 @@ class AmsHttpRequests(object): library. service error handling is implemented according to HTTP status codes returned by service and the balancer. """ - def __init__(self): + def __init__(self, endpoint): + self.endpoint = endpoint # Create route list self.routes = {"topic_list": ["get", "https://{0}/v1/projects/{2}/topics?key={1}"], "topic_get": ["get", "https://{0}/v1/projects/{2}/topics/{3}?key={1}"], @@ -152,9 +153,9 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, saved_exp = e time.sleep(sleep_secs) if timeout: - log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds - {3}'.format(i, sleep_secs, timeout, e)) + log.warning('Backoff retry #{0} after {1} seconds, connection timeout set to {2} seconds - {3}: {4}'.format(i, sleep_secs, timeout, self.endpoint, e)) else: - log.warning('Backoff retry #{0} after {1} seconds - {2}'.format(i, sleep_secs, e)) + log.warning('Backoff retry #{0} after {1} seconds - {2}: {3}'.format(i, sleep_secs, self.endpoint, e)) finally: i += 1 else: @@ -170,9 +171,9 @@ def _retry_make_request(self, url, body=None, route_name=None, retry=0, else: time.sleep(retrysleep) if timeout: - log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds - {3}'.format(i, retrysleep, timeout, e)) + log.warning('Retry #{0} after {1} seconds, connection timeout set to {2} seconds - {3}: {4}'.format(i, retrysleep, timeout, self.endpoint, e)) else: - log.warning('Retry #{0} after {1} seconds - {2}'.format(i, retrysleep, e)) + log.warning('Retry #{0} after {1} seconds - {2}: {3}'.format(i, retrysleep, self.endpoint, e)) finally: i += 1 @@ -340,7 +341,7 @@ class ArgoMessagingService(AmsHttpRequests): calls that are wrapped in series of methods. """ def __init__(self, endpoint, token="", project="", cert="", key="", authn_port=8443): - super(ArgoMessagingService, self).__init__() + super(ArgoMessagingService, self).__init__(endpoint) self.authn_port = authn_port self.token = "" self.endpoint = endpoint From 22d38e5af8d4c122f41261c25304a44378a3d460 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 16 Dec 2019 12:57:41 +0100 Subject: [PATCH 40/45] Test helper tool coverage version lock to 4.5.4 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ecfcf33..6a5f1f6 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = py27-requests0, py27-requests260, py34-requests2123, py36-requests0, py36-requests2125 [testenv] -deps = coverage +deps = coverage==4.5.4 pytest httmock mock From 41125bd1049b052beea2bead4f15dc94063aff2d Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 16 Dec 2019 13:16:54 +0100 Subject: [PATCH 41/45] Removed empty line --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6a5f1f6..0f6de1a 100644 --- a/tox.ini +++ b/tox.ini @@ -11,4 +11,3 @@ deps = coverage==4.5.4 requests260: requests==2.6.0 requests2125: requests==2.12.5 commands = coverage run -m pytest - From 68c46427e0f68b16cfc6448367d5c4871fe06d2a Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 16 Dec 2019 15:31:35 +0100 Subject: [PATCH 42/45] Added LICENSE file --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d15ab95 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 SRCE, GRNET, CNRS + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From dee5f9666ff2fc167459825dd4832a5e9869b978 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 16 Dec 2019 16:48:13 +0100 Subject: [PATCH 43/45] Slight update of README.md --- README.md | 72 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 3f6fa25..5ec07ea 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,24 @@ You may find more information about [the ARGO Messaging Service documentation](h ## Library installation -You may find and download the ARGO Messaging Library from ARGO Repository. +You may find and download the ARGO Messaging Library from ARGO Repository as well as from PyPI. If you want the devel instance so as to test all new features - - http://rpm-repo.argo.grnet.gr/ARGO/devel/centos6/argo-ams-library-*.*.* - - ``` - The current first realese is - http://rpm-repo.argo.grnet.gr/ARGO/devel/centos6/argo-ams-library-0.1.0-20170301161111.55fe753.el6.noarch.rpm - ``` +http://rpm-repo.argo.grnet.gr/ARGO/devel/centos6/ +http://rpm-repo.argo.grnet.gr/ARGO/devel/centos7/ + If you want the stable instance you may download it from here + http://rpm-repo.argo.grnet.gr/ARGO/prod/centos6/ +http://rpm-repo.argo.grnet.gr/ARGO/prod/centos7/ + +PyPI package is available here: + +https://pypi.org/project/argo-ams-library/ ## Authentication -The ams library uses a valid ams token to execute requests against the ams cluster. +The AMS library uses a valid AMS token to execute requests against the AMS cluster. This token can be provided with 2 ways: - Obtain a valid ams token and then use it when initializing the ams object. @@ -46,36 +48,36 @@ The library will use the provided certificate to access the corresponding ams to ## Examples In the folder examples, you may find examples of using the library: + +- for publishing messages (`examples/publish.py`) +- for consuming messages in pull mode (`examples/consume-pull.py`) + +### Publish messages + +This example explains how to publish messages in a topic with the use of the library. Topics are resources that can hold messages. Publishers (users/systems) can create topics on demand and name them (Usually with names that make sense and express the class of messages delivered in the topic). A topic name must be scoped to a project. - - for publishing messages (examples/publish.py) - - for consuming messages in pull mode (examples/consume-pull.py) - - ### Publish messages - - This example explains how to publish messages in a topic with the use of the library. Topics are resources that can hold messages. Publishers (users/systems) can create topics on demand and name them (Usually with names that make sense and express the class of messages delivered in the topic). A topic name must be scoped to a project. - - You may find more information about [Topics in the ARGO Messaging Service documentation](http://argoeu.github.io/messaging/v1/api_topics/) +You may find more information about [Topics in the ARGO Messaging Service documentation](http://argoeu.github.io/messaging/v1/api_topics/) - ``` - publish.py --host=[the FQDN of AMS Service] - --token=[the user token] - --project=[the name of your project registered in AMS Service] - --topic=[the topic to publish your messages] - ``` +``` +publish.py --host=[the FQDN of AMS Service] +--token=[the user token] +--project=[the name of your project registered in AMS Service] +--topic=[the topic to publish your messages] +``` - ### Consume messages in pull mode +### Consume messages in pull mode - This example explains how to consume messages from a predefined subscription with the use of the library. A subscription is a named resource representing the stream of messages from a single, specific topic, to be delivered to the subscribing application. A subscription name must be scoped to a project. In pull delivery, your subscriber application initiates requests to the Pub/Sub server to retrieve messages. When you create a subscription, the system establishes a sync point. That is, your subscriber is guaranteed to receive any message published after this point. Messages published before the sync point may not be delivered. +This example explains how to consume messages from a predefined subscription with the use of the library. A subscription is a named resource representing the stream of messages from a single, specific topic, to be delivered to the subscribing application. A subscription name must be scoped to a project. In pull delivery, your subscriber application initiates requests to the Pub/Sub server to retrieve messages. When you create a subscription, the system establishes a sync point. That is, your subscriber is guaranteed to receive any message published after this point. Messages published before the sync point may not be delivered. - You may find more information about [Subscriptions in the ARGO Messaging Service documentation](http://argoeu.github.io/messaging/v1/api_subs/) +You may find more information about [Subscriptions in the ARGO Messaging Service documentation](http://argoeu.github.io/messaging/v1/api_subs/) - ``` - consume-pull.py --host=[the FQDN of AMS Service] - --token=[the user token] - --project=[the name of your project registered in AMS Service] - --topic=[the topic from where the messages are delivered ] - --subscription=[the subscription name to pull the messages] - --nummsgs=[the num of messages to consume] - - ``` +``` +consume-pull.py --host=[the FQDN of AMS Service] +--token=[the user token] +--project=[the name of your project registered in AMS Service] +--topic=[the topic from where the messages are delivered ] +--subscription=[the subscription name to pull the messages] +--nummsgs=[the num of messages to consume] + +``` From 050b1c8501dc10a56f489079887b566686ddb4b8 Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Mon, 16 Dec 2019 17:02:56 +0100 Subject: [PATCH 44/45] Package long_description loaded from README.md --- setup.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 166733f..bbe5b20 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,13 @@ from setuptools import setup +from os import path import glob NAME='argo-ams-library' +this_directory = path.abspath(path.dirname(__file__)) +with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + def get_ver(): try: with open(NAME+'.spec') as f: @@ -14,19 +19,20 @@ def get_ver(): raise SystemExit(1) setup( - name = NAME, - version = get_ver(), - author = 'SRCE, GRNET', - author_email = 'dvrcic@srce.hr, agelos.tsal@gmail.com, kaggis@gmail.com, themiszamani@gmail.com', - license = 'ASL 2.0', - description = 'A simple python library for interacting with the ARGO Messaging Service', - long_description = 'A simple python library for interacting with the ARGO Messaging Service', - tests_require = [ + name=NAME, + version=get_ver(), + author='SRCE, GRNET', + author_email='dvrcic@srce.hr, agelos.tsal@gmail.com, kaggis@gmail.com, themiszamani@gmail.com', + license='ASL 2.0', + description='A simple python library for interacting with the ARGO Messaging Service', + long_description=long_description, + long_description_content_type='text/markdown', + tests_require=[ 'setuptools_scm', 'httmock', 'pytest' ], - classifiers = [ + classifiers=[ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX", @@ -37,8 +43,8 @@ def get_ver(): "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules" ], - url = 'https://github.com/ARGOeu/argo-ams-library', - package_dir = {'argo_ams_library': 'pymod/'}, - packages = ['argo_ams_library'], + url='https://github.com/ARGOeu/argo-ams-library', + package_dir={'argo_ams_library': 'pymod/'}, + packages=['argo_ams_library'], install_requires=['requests'] - ) +) From 6da36f6be881f341803bedf21130379e428e75de Mon Sep 17 00:00:00 2001 From: Daniel Vrcic Date: Wed, 18 Dec 2019 11:46:27 +0100 Subject: [PATCH 45/45] Remove encoding parameter as build is initiated with Py2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bbe5b20..91c5ae1 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ NAME='argo-ams-library' this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: +with open(path.join(this_directory, 'README.md')) as f: long_description = f.read() def get_ver():