Skip to content

Commit

Permalink
Helper to handle different timestamp formats (#86)
Browse files Browse the repository at this point in the history
* add helper to handle differents timestamps formats - close #85
* enforce tests
  • Loading branch information
Guts committed Aug 22, 2019
1 parent caf8eea commit e0b2285
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 17 deletions.
2 changes: 1 addition & 1 deletion isogeo_pysdk/api/routes_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def search(
loop = asyncio.get_event_loop()
except RuntimeError as e:
logger.warning(
"Async get loop failed. Maybe because it's already executed in a separated thread. Original error: ".format(
"Async get loop failed. Maybe because it's already executed in a separated thread. Original error: {}".format(
e
)
)
Expand Down
40 changes: 24 additions & 16 deletions isogeo_pysdk/models/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
# ##################################

# for slugified title
_slugify_strip_re = re.compile(r"[^\w\s-]")
_slugify_hyphenate_re = re.compile(r"[-\s]+")
_regex_slugify_strip = re.compile(r"[^\w\s-]")
_regex_slugify_hyphenate = re.compile(r"[-\s]+")


# #############################################################################
Expand Down Expand Up @@ -145,7 +145,8 @@ class Metadata(object):
"""

attr_types = {
# -- ATTRIBUTES --------------------------------------------------------------------
ATTR_TYPES = {
"_abilities": list,
"_created": str,
"_creator": dict,
Expand Down Expand Up @@ -193,7 +194,7 @@ class Metadata(object):
"validityComment": str,
}

attr_crea = {
ATTR_CREA = {
"abstract": str,
"collectionContext": str,
"collectionMethod": str,
Expand All @@ -220,22 +221,28 @@ class Metadata(object):
"validityComment": str,
}

attr_map = {
ATTR_MAP = {
"coordinateSystem": "coordinate-system",
# "creation": "created",
"featureAttributes": "feature-attributes",
# "modification": "modified",
}

# -- CLASS METHODS -----------------------------------------------------------------
@classmethod
def clean_attributes(cls, raw_object: dict):
"""Renames attributes wich are incompatible with Python (hyphens...).
"""Renames attributes which are incompatible with Python (hyphens...).
See related issue: https://github.com/isogeo/isogeo-api-py-minsdk/issues/82
:param dict raw_object: metadata dictionary returned by a request.json()
:returns: the metadata with correct attributes
:rtype: Metadata
"""
for k, v in cls.attr_map.items():
for k, v in cls.ATTR_MAP.items():
raw_object[k] = raw_object.pop(v, [])
return cls(**raw_object)

# -- CLASS INSTANCIATION -----------------------------------------------------------

def __init__(
self,
_abilities: list = None,
Expand Down Expand Up @@ -1242,13 +1249,14 @@ def validityComment(self, validityComment: str):
self._validityComment = validityComment

# -- METHODS -----------------------------------------------------------------------

def title_or_name(self, slugged: bool = False) -> str:
"""Gets the title of this Metadata or the name if there is no title.
It can return a slugified value.
:param bool slugged: slugify title. Defaults to `False`.
:return: the title or the name of this Metadata.
:returns: the title or the name of this Metadata.
:rtype: str
"""
if self._title:
Expand All @@ -1263,16 +1271,16 @@ def title_or_name(self, slugged: bool = False) -> str:
.encode("ascii", "ignore")
.decode("ascii")
)
title_or_name = _slugify_strip_re.sub("", title_or_name).strip().lower()
title_or_name = _slugify_hyphenate_re.sub("-", title_or_name)
title_or_name = _regex_slugify_strip.sub("", title_or_name).strip().lower()
title_or_name = _regex_slugify_hyphenate.sub("-", title_or_name)

return title_or_name

def to_dict(self) -> dict:
"""Returns the model properties as a dict"""
result = {}

for attr, _ in self.attr_types.items():
for attr, _ in self.ATTR_TYPES.items():
value = getattr(self, attr)
if isinstance(value, list):
result[attr] = list(
Expand Down Expand Up @@ -1301,12 +1309,12 @@ def to_dict_creation(self) -> dict:
"""Returns the model properties as a dict structured for creation purpose (POST)"""
result = {}

for attr, _ in self.attr_crea.items():
for attr, _ in self.ATTR_CREA.items():
# get attribute value
value = getattr(self, attr)
# switch attribute name for creation purpose
if attr in self.attr_map:
attr = self.attr_map.get(attr)
if attr in self.ATTR_MAP:
attr = self.ATTR_MAP.get(attr)
if isinstance(value, list):
result[attr] = list(
map(lambda x: x.to_dict() if hasattr(x, "to_dict") else x, value)
Expand Down
70 changes: 70 additions & 0 deletions isogeo_pysdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import re
import uuid
from configparser import ConfigParser
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse

Expand All @@ -37,6 +38,28 @@
checker = IsogeoChecker()
logger = logging.getLogger(__name__)

# timestamps format helpers
_regex_milliseconds = re.compile(r"\.(.*)\+")

"""
For timestamps about metadata (_created, _modified):
- `2019-05-17T13:01:08.559123+00:00`: sometimes there are 6 digits in milliseconds, as accepted in `standard lib (%f) <https://docs.python.org/fr/3/library/datetime.html#strftime-strptime-behavior>`_
- `2019-08-21T16:07:42.9419347+00:00`: sometimes there are more than 6 digits in milliseconds
"""
_dtm_metadata = "%Y-%m-%dT%H:%M:%S.%f+00:00"

"""
For timestamps about data (created, modified, published): `2018-06-04T00:00:00+00:00` without milliseconds.
"""
_dtm_data = "%Y-%m-%dT%H:%M:%S+00:00" # 2018-06-04T00:00:00+00:00

"""
For simple date. Used to add new events.
"""
_dtm_simple = "%Y-%m-%d" # 2018-06-04


# ##############################################################################
# ########## Classes ###############
# ##################################
Expand Down Expand Up @@ -793,6 +816,53 @@ def credentials_loader(self, in_credentials: str = "client_secrets.json") -> dic
# method ending
return out_auth

# -- HELPERS ---------------------------------------------------------------
@classmethod
def hlpr_date_as_datetime(cls, in_date: str) -> datetime:
"""Helper to handle differnts dates formats.
See: https://github.com/isogeo/isogeo-api-py-minsdk/issues/85
:param dict raw_object: metadata dictionary returned by a request.json()
:returns: a correct datetime object
:rtype: datetime
:Example:
.. code-block:: python
# for an event date
event_date = Metadata.hlpr_date_as_datetime("2018-06-04T00:00:00+00:00")
# for a metadata creation date with 6 digits as milliseconds
md_date = Metadata.hlpr_date_as_datetime("2019-05-17T13:01:08.559123+00:00")
# for a metadata creation date with more than 6 digits as milliseconds
md_date_larger = Metadata.hlpr_date_as_datetime("2019-06-13T16:21:38.1917618+00:00")
"""
if len(in_date) == 10:
out_date = datetime.strptime(in_date, _dtm_simple) # basic dates
elif len(in_date) == 25:
out_date = datetime.strptime(
in_date, _dtm_data
) # events datetimes (=dates)
elif len(in_date) == 32 and "." in in_date:
out_date = datetime.strptime(
in_date, _dtm_metadata
) # metadata timestamps with 6 milliseconds
elif len(in_date) >= 32 and "." in in_date:
milliseconds = re.search(_regex_milliseconds, in_date)
return cls.hlpr_date_as_datetime(
in_date.replace(milliseconds.group(1), milliseconds.group(1)[:6])
)
else:
raise TypeError(
"This format of timestamps is not recognized: {}.Try by yourself!".format(
in_date
)
)

return out_date


# #############################################################################
# ##### Stand alone program ########
Expand Down
29 changes: 29 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

# Standard library
import unittest
from datetime import datetime
from os import environ
from pathlib import Path
from urllib.parse import urlparse

Expand Down Expand Up @@ -136,10 +138,12 @@ def test_set_base_url_bad_parameter(self):
"""Raise error if platform parameter is bad."""
with self.assertRaises(ValueError):
self.utils.set_base_url(platform="skynet")
self.utils.set_base_url(platform=environ.get("ISOGEO_PLATFORM", "qa"))

# -- URLs Builders - edit (app) ------------------------------------------
def test_get_edit_url_ok(self):
"""Test URL builder for edition link on APP"""
self.utils.set_base_url(platform=environ.get("ISOGEO_PLATFORM", "qa"))
url = self.utils.get_edit_url(
md_id="0269803d50c446b09f5060ef7fe3e22b",
md_type="vector-dataset",
Expand Down Expand Up @@ -345,3 +349,28 @@ def test_pages_counter(self):
self.assertEqual(p_default, 5)
p_default = self.utils.pages_counter(total=156, page_size=22)
self.assertEqual(p_default, 8)

# -- Methods helpers
def test_helper_datetimes(self):
"""Test class method to help formatting dates."""
# simple dates str
simple_date = self.utils.hlpr_date_as_datetime("2019-08-09")
self.assertIsInstance(simple_date, datetime)
self.assertEqual(simple_date.year, 2019)

# events datetimes str
event_date = self.utils.hlpr_date_as_datetime("2018-06-04T00:00:00+00:00")
self.assertIsInstance(event_date, datetime)
self.assertEqual(event_date.year, 2018)

# metadata timestamps str - 6 milliseconds
md_date = self.utils.hlpr_date_as_datetime("2019-05-17T13:01:08.559123+00:00")
self.assertIsInstance(md_date, datetime)
self.assertEqual(md_date.year, 2019)

# metadata timestamps str - more than 6 milliseconds
md_date_larger = self.utils.hlpr_date_as_datetime(
"2019-06-13T16:21:38.1917618+00:00"
)
self.assertIsInstance(md_date_larger, datetime)
self.assertEqual(md_date_larger.year, 2019)

0 comments on commit e0b2285

Please sign in to comment.