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 99f0396..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. @@ -194,17 +197,21 @@ def post(self, message, msg_id=None, reply=False, attributes=None, attachments=N # Make requests module think that Text is a "file". This is the only way to force requests to send data as # multipart/form-data even if there are no attachments. Elog understands only multipart/form-data - files_to_attach.append(('Text', ('', message))) + files_to_attach.append(('Text', ('', message.encode('iso-8859-1')))) # Base attributes are common to all messages self._add_base_msg_attributes(attributes_to_edit) # Keys in attributes cannot have certain characters like whitespaces or dashes for the http request attributes_to_edit = _replace_special_characters_in_attribute_keys(attributes_to_edit) + + # All string values in the attributes must be encoded in latin1 + attributes_to_edit = _encode_values(attributes_to_edit) 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) @@ -213,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 @@ -226,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 @@ -234,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 """ @@ -244,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) + @@ -258,7 +282,7 @@ def read(self, msg_id): attributes = dict() attachments = list() - returned_msg = resp_message.decode('utf-8', 'ignore').splitlines() + returned_msg = resp_message.decode('iso-8859-1', 'ignore').splitlines() delimiter_idx = returned_msg.index('========================================') message = '\n'.join(returned_msg[delimiter_idx + 1:]) @@ -280,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: """ @@ -297,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) + @@ -312,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: @@ -343,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 ' @@ -360,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 ' @@ -391,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