Skip to content

Commit

Permalink
#145: Move date-related functions to a separate class
Browse files Browse the repository at this point in the history
- In the future, these could be used by other backends so I've moved these 3 functions into their own utility class. This change helps clean up common.icat.helpers a bit which is another reason this change has been made
  • Loading branch information
MRichards99 committed Oct 8, 2020
1 parent d7741f7 commit 2a88aae
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 62 deletions.
74 changes: 74 additions & 0 deletions common/date_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from datetime import datetime
from dateutil.parser import parse

from common.exceptions import BadRequestError
from common.constants import Constants


class DateHandler:
"""
Utility class to deal with dates. Currently, this class converts dates between
strings and `datetime.datetime` objects as well as detecting whether a string is
likely to be a date.
"""

@staticmethod
def is_str_a_date(potential_date):
"""
This function identifies if a string contains a date. This function doesn't
detect which format the date is, just if there's a date or not.
:param potential_date: String data that could contain a date of any format
:type potential_date: :class:`str`
:return: Boolean to signify whether `potential_date` is a date or not
"""

try:
# Disabled fuzzy to avoid picking up dates in things like descriptions etc.
parse(potential_date, fuzzy=False)
return True
except ValueError:
return False

def str_to_datetime_object(data):
"""
Convert a string to a `datetime.datetime` object. This is commonly used when
storing user input in ICAT (using the Python ICAT backend).
Python 3.7+ has support for `datetime.fromisoformat()` which would be a more
elegant solution to this conversion operation since dates are converted into ISO
format within this file, however, the production instance of this API is
typically built on Python 3.6, and it doesn't seem of enough value to mandate
3.7 for a single line of code
:param data: Single data value from the request body
:type data: Data type of the data as per user's request body
:return: Date converted into a :class:`datetime` object
:raises BadRequestError: If the date is entered in the incorrect format, as per
`Constants.ACCEPTED_DATE_FORMAT`
"""

try:
data = datetime.strptime(data, Constants.ACCEPTED_DATE_FORMAT)
except ValueError:
raise BadRequestError(
"Bad request made, the date entered is not in the correct format. Use"
f" the {Constants.ACCEPTED_DATE_FORMAT} format to submit dates to the"
" API"
)

return data

@staticmethod
def datetime_object_to_str(date_obj):
"""
Convert a datetime object to a string so it can be outputted in JSON
There's currently no reason to make this function static, but it could be useful
in the future if a use case required this functionality.
:param date_obj: Datetime object from data from an ICAT entity
:type date_obj: :class:`datetime.datetime`
:return: Datetime (of type string) in the agreed format
"""
return date_obj.replace(tzinfo=None).strftime(Constants.ACCEPTED_DATE_FORMAT)
69 changes: 7 additions & 62 deletions common/icat/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from functools import wraps
import logging
from datetime import datetime, timedelta
from dateutil.parser import parse


from icat.entity import Entity, EntityList
from icat.entities import getTypeMap
Expand All @@ -22,6 +22,7 @@
PythonICATError,
)
from common.filter_order_handler import FilterOrderHandler
from common.date_handler import DateHandler
from common.constants import Constants
from common.icat.filters import (
PythonICATLimitFilter,
Expand Down Expand Up @@ -251,19 +252,6 @@ def check_attribute_name_for_distinct(self, attribute_list, key, value):
if value == Constants.PYTHON_ICAT_DISTNCT_CONDITION:
attribute_list.append(key)

def datetime_object_to_str(self, date_obj):
"""
Convert a datetime object to a string so it can be outputted in JSON
There's currently no reason to make this function static, but it could be useful
in the future if a use case required this functionality.
:param date_obj: Datetime object from data from an ICAT entity
:type date_obj: :class:`datetime.datetime`
:return: Datetime (of type string) in the agreed format
"""
return date_obj.replace(tzinfo=None).strftime(Constants.ACCEPTED_DATE_FORMAT)

def entity_to_dict(self, entity, includes, distinct_fields=None):
"""
This expands on Python ICAT's implementation of `icat.entity.Entity.as_dict()`
Expand Down Expand Up @@ -336,7 +324,7 @@ def entity_to_dict(self, entity, includes, distinct_fields=None):
# Convert datetime objects to strings ready to be outputted as JSON
if isinstance(entity_data, datetime):
# Remove timezone data which isn't utilised in ICAT
entity_data = self.datetime_object_to_str(entity_data)
entity_data = DateHandler.datetime_object_to_str(entity_data)

d[key] = entity_data
return d
Expand Down Expand Up @@ -500,36 +488,6 @@ def get_python_icat_entity_name(client, database_table_name, camel_case_output=F
return python_icat_entity_name


def str_to_datetime_object(data):
"""
Where data is stored as dates in ICAT (which this function determines), convert
strings (i.e. user data from PATCH/POST requests) into datetime objects so they can
be stored in ICAT
Python 3.7+ has support for `datetime.fromisoformat()` which would be a more elegant
solution to this conversion operation since dates are converted into ISO format
within this file, however, the production instance of this API is typically built on
Python 3.6, and it doesn't seem of enough value to mandate 3.7 for a single line of
code
:param data: Single data value from the request body
:type data: Data type of the data as per user's request body
:return: Date converted into a :class:`datetime` object
:raises BadRequestError: If the date is entered in the incorrect format, as per
`Constants.ACCEPTED_DATE_FORMAT`
"""

try:
data = datetime.strptime(data, Constants.ACCEPTED_DATE_FORMAT)
except ValueError:
raise BadRequestError(
"Bad request made, the date entered is not in the correct format. Use the"
f" {Constants.ACCEPTED_DATE_FORMAT} format to submit dates to the API"
)

return data


def update_attributes(old_entity, new_entity):
"""
Updates the attribute(s) of a given object which is a record of an entity from
Expand All @@ -548,7 +506,7 @@ def update_attributes(old_entity, new_entity):
try:
original_data_attribute = getattr(old_entity, key)
if isinstance(original_data_attribute, datetime):
new_entity[key] = str_to_datetime_object(new_entity[key])
new_entity[key] = DateHandler.str_to_datetime_object(new_entity[key])
except AttributeError:
raise BadRequestError(
f"Bad request made, cannot find attribute '{key}' within the"
Expand Down Expand Up @@ -862,8 +820,8 @@ def create_entities(client, table_name, data):
entity_info = new_entity.getAttrInfo(client, attribute_name)
if entity_info.relType.lower() == "attribute":
if isinstance(value, str):
if is_str_a_date(value):
value = str_to_datetime_object(value)
if DateHandler.is_str_a_date(value):
value = DateHandler.str_to_datetime_object(value)

setattr(new_entity, attribute_name, value)
else:
Expand All @@ -886,20 +844,7 @@ def create_entities(client, table_name, data):
raise PythonICATError(e)
except (ICATObjectExistsError, ICATParameterError) as e:
raise BadRequestError(e)

created_data.append(get_entity_by_id(client, table_name, new_entity.id, True))

return created_data


def is_str_a_date(potential_date):
"""
TODO - Add docstring
"""

try:
# Disabled fuzzy to avoid picking up dates in things like descriptions etc.
parse(potential_date, fuzzy=False)
return True
except ValueError:
return False

0 comments on commit 2a88aae

Please sign in to comment.