From e141e33d1b6ad2f17ab2065f1293871f068963cf Mon Sep 17 00:00:00 2001 From: Antonio Bulgheroni Date: Sat, 1 Oct 2022 09:23:25 +0200 Subject: [PATCH] Add a timeout parameter to all methods invoking either a get or post request. Update documentation. Add a test case to demonstrate the functionality. --- README.md | 11 ++++-- elog/logbook.py | 80 ++++++++++++++++++++++++++++++-------- elog/logbook_exceptions.py | 2 + tests/test_logbook.py | 13 +++++-- 4 files changed, 83 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 1d6e209..7f4678f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ message, attributes, attachments = logbook.read(23) ```python # Create new message with some text, attributes (dict of attributes + kwargs) and attachments -new_msg_id = logbook.post('This is message text', attributes=dict_of_attributes, attachments=list_of_attachments, attribute_as_param='value') +new_msg_id = logbook.post('This is message text', attributes=dict_of_attributes, attachments=list_of_attachments, + attribute_as_param='value') ``` What attributes are required is determined by the configuration of the elog server (keywork `Required Attributes`). @@ -76,14 +77,16 @@ new_msg_id = logbook.post('This is message text', author='me', type='Routine') ```python # Reply to message with ID=23 -new_msg_id = logbook.post('This is a reply', msg_id=23, reply=True, attributes=dict_of_attributes, attachments=list_of_attachments, attribute_as_param='value') +new_msg_id = logbook.post('This is a reply', msg_id=23, reply=True, attributes=dict_of_attributes, + attachments=list_of_attachments, attribute_as_param='value') ``` ## Edit Message ```python # Edit message with ID=23. Changed message text, some attributes (dict of edited attributes + kwargs) and new attachments -edited_msg_id = logbook.post('This is new message text', msg_id=23, attributes=dict_of_changed_attributes, attachments=list_of_new_attachments, attribute_as_param='new value') +edited_msg_id = logbook.post('This is new message text', msg_id=23, attributes=dict_of_changed_attributes, + attachments=list_of_new_attachments, attribute_as_param='new value') ``` ## Search Messages @@ -92,7 +95,7 @@ edited_msg_id = logbook.post('This is new message text', msg_id=23, attributes=d # Search for text in messages or specify attributes for search, returns list of message ids logbook.search('Hello World') logbook.search('Hello World', n_results=20, scope='attribname') -logbook.search({'attribname' : 'Hello World', ... }) +logbook.search({'attribname': 'Hello World', ...}) ``` ## Delete Message (and all its replies) diff --git a/elog/logbook.py b/elog/logbook.py index 3f7a193..45d387a 100644 --- a/elog/logbook.py +++ b/elog/logbook.py @@ -3,6 +3,7 @@ import os import builtins import re +import sys from elog.logbook_exceptions import * from datetime import datetime @@ -105,8 +106,8 @@ def __init__(self, hostname, logbook='', port=None, user=None, password=None, su self._user = user self._password = _handle_pswd(password, encrypt_pwd) - def post(self, message, msg_id=None, reply=False, attributes=None, attachments=None, - suppress_email_notification=False, encoding=None, **kwargs): + def post(self, message, msg_id=None, reply=False, attributes=None, attachments=None, + suppress_email_notification=False, encoding=None, timeout=None, **kwargs): """ Posts message to the logbook. If msg_id is not specified new message will be created, otherwise existing message will be edited, or a reply (if reply=True) to it will be created. This method returns the msg_id @@ -123,9 +124,11 @@ def post(self, message, msg_id=None, reply=False, attributes=None, attachments=N - paths to the files All items will be appended as attachment to the elog entry. In case of unknown attachment an exception LogbookInvalidAttachment will be raised. + :param suppress_email_notification: If set to True or 1, E-Mail notification will be suppressed, defaults to False. :param encoding: Defines encoding of the message. Can be: 'plain' -> plain text, 'html'->html-text, 'ELCode' --> elog formatting syntax - :param suppress_email_notification: If set to True or 1, E-Mail notification will be suppressed, defaults to False. + :param timeout: Define the timeout to be used by the post request. Its value is directly passed to the requests + post. Use None to disable the request timeout. :param kwargs: Anything in the kwargs will be interpreted as attribute. e.g.: logbook.post('Test text', Author='Rok Vintar), "Author" will be sent as an attribute. If named same as one of the attributes defined in "attributes", kwargs will have priority. @@ -207,7 +210,7 @@ def post(self, message, msg_id=None, reply=False, attributes=None, attachments=N try: response = requests.post(self._url, data=attributes_to_edit, files=files_to_attach, allow_redirects=False, - verify=False) + verify=False, timeout=timeout) # Validate response. Any problems will raise an Exception. resp_message, resp_headers, resp_msg_id = _validate_response(response) @@ -217,6 +220,12 @@ def post(self, message, msg_id=None, reply=False, attributes=None, attachments=N if hasattr(file_like_object, 'close'): file_like_object.close() + except requests.Timeout as e: + # Catch here a timeout o the post request. + # Raise the logbook excetion and let the user handle it + raise LogbookServerTimeout('{0} method cannot be completed because of a network timeout:\n'+ + '{1}'.format(sys._getframe().f_code.co_name, e)) + except requests.RequestException as e: # Check if message on server. self._check_if_message_on_server(msg_id) # raises exceptions if no message or no response from server @@ -230,7 +239,7 @@ def post(self, message, msg_id=None, reply=False, attributes=None, attachments=N raise LogbookInvalidMessageID('Invalid message ID: ' + str(resp_msg_id) + ' returned') return resp_msg_id - def read(self, msg_id): + def read(self, msg_id, timeout=None): """ Reads message from the logbook server and returns tuple of (message, attributes, attachments) where: message: string with message body @@ -238,6 +247,7 @@ def read(self, msg_id): attachments: list of urls to attachments on the logbook server :param msg_id: ID of the message to be read + :param timeout: The timeout value to be passed to the get request. :return: message, attributes, attachments """ @@ -248,11 +258,21 @@ def read(self, msg_id): try: self._check_if_message_on_server(msg_id) # raises exceptions if no message or no response from server response = requests.get(self._url + str(msg_id) + '?cmd=download', headers=request_headers, - allow_redirects=False, verify=False) + allow_redirects=False, verify=False, timeout=timeout) # Validate response. If problems Exception will be thrown. resp_message, resp_headers, resp_msg_id = _validate_response(response) + + except requests.Timeout as e: + + # Catch here a timeout o the post request. + + # Raise the logbook excetion and let the user handle it + + raise LogbookServerTimeout('{0} method cannot be completed because of a network timeout:\n' + + '{1}'.format(sys._getframe().f_code.co_name, e)) + except requests.RequestException as e: # If here: message is on server but cannot be downloaded (should never happen) raise LogbookServerProblem('Cannot access logbook server to read the message with ID: ' + str(msg_id) + @@ -284,12 +304,13 @@ def read(self, msg_id): return message, attributes, attachments - def delete(self, msg_id): + def delete(self, msg_id, timeout=None): """ Deletes message thread (!!!message + all replies!!!) from logbook. It also deletes all of attachments of corresponding messages from the server. :param msg_id: message to be deleted + :param timeout: timeout value to be passed to the get request :return: """ @@ -301,10 +322,16 @@ def delete(self, msg_id): self._check_if_message_on_server(msg_id) # check if something to delete response = requests.get(self._url + str(msg_id) + '?cmd=Delete&confirm=Yes', headers=request_headers, - allow_redirects=False, verify=False) + allow_redirects=False, verify=False, timeout=timeout) _validate_response(response) # raises exception if any other error identified + except requests.Timeout as e: + # Catch here a timeout o the post request. + # Raise the logbook excetion and let the user handle it + raise LogbookServerTimeout('{0} method cannot be completed because of a network timeout:\n'+ + '{1}'.format(sys._getframe().f_code.co_name, e)) + except requests.RequestException as e: # If here: message is on server but cannot be downloaded (should never happen) raise LogbookServerProblem('Cannot access logbook server to delete the message with ID: ' + str(msg_id) + @@ -316,10 +343,12 @@ def delete(self, msg_id): if response.status_code == 200: raise LogbookServerProblem('Cannot process delete command (only logbooks in English supported).') - def search(self, search_term, n_results=20, scope="subtext"): + def search(self, search_term, n_results=20, scope="subtext", timeout=None): """ Searches the logbook and returns the message ids. + :param timeout: timeout value to be passed to the get request + """ request_headers = dict() if self._user or self._password: @@ -347,12 +376,18 @@ def search(self, search_term, n_results=20, scope="subtext"): try: response = requests.get(self._url, params=params, headers=request_headers, - allow_redirects=False, verify=False) + allow_redirects=False, verify=False, timeout=timeout) # Validate response. If problems Exception will be thrown. _validate_response(response) resp_message = response + except requests.Timeout as e: + # Catch here a timeout o the post request. + # Raise the logbook excetion and let the user handle it + raise LogbookServerTimeout('{0} method cannot be completed because of a network timeout:\n'+ + '{1}'.format(sys._getframe().f_code.co_name, e)) + except requests.RequestException as e: # If here: message is on server but cannot be downloaded (should never happen) raise LogbookServerProblem('Cannot access logbook server to read message ids ' @@ -364,26 +399,32 @@ def search(self, search_term, n_results=20, scope="subtext"): message_ids = [int(m.split("/")[-1]) for m in message_ids] return message_ids - def get_last_message_id(self): - ids = self.get_message_ids() + def get_last_message_id(self, timeout=None): + ids = self.get_message_ids(timeout) if len(ids) > 0: return ids[0] else: return None - def get_message_ids(self): + def get_message_ids(self, timeout=None): request_headers = dict() if self._user or self._password: request_headers['Cookie'] = self._make_user_and_pswd_cookie() try: response = requests.get(self._url + 'page', headers=request_headers, - allow_redirects=False, verify=False) + allow_redirects=False, verify=False, timeout=timeout) # Validate response. If problems Exception will be thrown. _validate_response(response) resp_message = response + except requests.Timeout as e: + # Catch here a timeout o the post request. + # Raise the logbook excetion and let the user handle it + raise LogbookServerTimeout('{0} method cannot be completed because of a network timeout:\n'+ + '{1}'.format(sys._getframe().f_code.co_name, e)) + except requests.RequestException as e: # If here: message is on server but cannot be downloaded (should never happen) raise LogbookServerProblem('Cannot access logbook server to read message ids ' @@ -395,11 +436,12 @@ def get_message_ids(self): message_ids = [int(m.split("/")[-1]) for m in message_ids] return message_ids - def _check_if_message_on_server(self, msg_id): + def _check_if_message_on_server(self, msg_id, timeout=None): """Try to load page for specific message. If there is a htm tag like then there is no such message. :param msg_id: ID of message to be checked + :params timeout: The value of timeout to be passed to the get request :return: """ @@ -408,7 +450,7 @@ def _check_if_message_on_server(self, msg_id): request_headers['Cookie'] = self._make_user_and_pswd_cookie() try: response = requests.get(self._url + str(msg_id), headers=request_headers, allow_redirects=False, - verify=False) + verify=False, timeout=timeout) # If there is no message code 200 will be returned (OK) and _validate_response will not recognise it # but there will be some error in the html code. @@ -420,6 +462,12 @@ def _check_if_message_on_server(self, msg_id): flags=re.DOTALL): raise LogbookInvalidMessageID('Message with ID: ' + str(msg_id) + ' does not exist on logbook.') + except requests.Timeout as e: + # Catch here a timeout o the post request. + # Raise the logbook excetion and let the user handle it + raise LogbookServerTimeout('{0} method cannot be completed because of a network timeout:\n'+ + '{1}'.format(sys._getframe().f_code.co_name, e)) + except requests.RequestException as e: raise LogbookServerProblem('No response from the logbook server.\nDetails: ' + '{0}'.format(e)) diff --git a/elog/logbook_exceptions.py b/elog/logbook_exceptions.py index 64bea05..180f4d3 100644 --- a/elog/logbook_exceptions.py +++ b/elog/logbook_exceptions.py @@ -7,6 +7,8 @@ class LogbookAuthenticationError(LogbookError): """ Raise when problem with username and password.""" pass +class LogbookServerTimeout(LogbookError): + """ Raise when the request to the logbook server timeouts. """ class LogbookServerProblem(LogbookError): """ Raise when problem accessing logbook server.""" diff --git a/tests/test_logbook.py b/tests/test_logbook.py index af0d1a6..e0a087b 100644 --- a/tests/test_logbook.py +++ b/tests/test_logbook.py @@ -1,6 +1,7 @@ import unittest # import logging import elog +from elog.logbook_exceptions import * @@ -19,10 +20,16 @@ def test_get_message_ids(self): def test_get_last_message_id(self): logbook = elog.open(self.elog_hostname) - msg_id = logbook.post(self.message, attributes={'Author':'AB', 'Type':'Routine'}) + msg_id = logbook.post(self.message, attributes={'Author': 'AB', 'Type': 'Routine'}) message_id = logbook.get_last_message_id() self.assertEqual(msg_id, message_id, "Created message does not show up as last edited message") - + + def test_get_last_message_id_with_short_timeout(self): + + logbook = elog.open(self.elog_hostname) + self.assertRaises(LogbookServerTimeout, logbook.post, + self.message, attributes={'Author': 'AB', 'Type': 'Routine'}, timeout=0.01) + def test_edit(self): logbook = elog.open(self.elog_hostname) logbook.post('hehehehehe', msg_id=logbook.get_last_message_id(), attributes={"Subject": 'py_elog test [mod]'}) @@ -44,7 +51,7 @@ def test_post_special_characters(self): attributes = { 'Author' : 'Me', 'Type' : 'Other', 'Category' : 'General', 'Subject' : 'This is a test of UTF-8 characters like èéöä'} message = 'Just to be clear this is a general test using UTF-8 characters like èéöä.' - msg_id = logbook.post(message, reply=False, attributes=attributes,encoding='HTML') + msg_id = logbook.post(message, reply=False, attributes=attributes, encoding='HTML') read_msg, read_attr, read_att = logbook.read(msg_id) mess_ok = message == read_msg