Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow request post and get to timeout #37

Merged
merged 8 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
104 changes: 86 additions & 18 deletions elog/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import builtins
import re
import sys
from elog.logbook_exceptions import *
from datetime import datetime

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -226,14 +239,15 @@ 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
attributes: dictionary of all attributes returned by the logbook
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
"""

Expand All @@ -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) +
Expand All @@ -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:])
Expand All @@ -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:
"""

Expand All @@ -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) +
Expand All @@ -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:
Expand Down Expand Up @@ -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 '
Expand All @@ -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 '
Expand All @@ -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 <td class="errormsg"> 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:
"""

Expand All @@ -404,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.
Expand All @@ -416,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))

Expand Down Expand Up @@ -513,6 +565,22 @@ def _remove_reserved_attributes(attributes):
attributes.pop('Attachment', None)
attributes.pop('Text', None) # Remove this one because it will be send attachment like

def _encode_values(attributes):
"""
prepares a dictionary of the attributes with latin1 encoded string values.

:param attributes: dictionary of attributes to ve encoded
:return: dictionary with encoded string attributes
"""

encoded_attributes = {}
for key, value in attributes.items():
if isinstance(value, str):
encoded_attributes[key] = value.encode('iso-8859-1')
else:
encoded_attributes[key] = value
return encoded_attributes


def _replace_special_characters_in_attribute_keys(attributes):
"""
Expand Down
2 changes: 2 additions & 0 deletions elog/logbook_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
README = open(os.path.join(here, 'README.md')).read()

setup(name='py_elog',
version='1.3.13',
version='1.3.14',
description="Python library to access Elog.",
long_description=README,
long_description_content_type="text/markdown",
Expand Down
Loading