From 750d147131bad432a42fd337b0522f1dcc980c46 Mon Sep 17 00:00:00 2001
From: Valentin Lorentz <vlorentz@softwareheritage.org>
Date: Thu, 27 Feb 2020 14:33:07 +0100
Subject: [PATCH] Add from_datetime and from_iso8601 constructors for
 TimestampWithTimezone.

Will be used by loaders.
---
 mypy.ini                      |  3 ++
 requirements.txt              |  1 +
 swh/model/model.py            | 23 +++++++++++++--
 swh/model/tests/test_model.py | 55 +++++++++++++++++++++++++++++++++--
 4 files changed, 77 insertions(+), 5 deletions(-)

diff --git a/mypy.ini b/mypy.ini
index 1c622770..8e421de2 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -11,6 +11,9 @@ ignore_missing_imports = True
 [mypy-dulwich.*]
 ignore_missing_imports = True
 
+[mypy-iso8601.*]
+ignore_missing_imports = True
+
 [mypy-pkg_resources.*]
 ignore_missing_imports = True
 
diff --git a/requirements.txt b/requirements.txt
index 98825fa3..1577daa9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,4 @@ vcversioner
 attrs
 hypothesis
 python-dateutil
+iso8601
diff --git a/swh/model/model.py b/swh/model/model.py
index d3c9c7d5..75544410 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -7,10 +7,11 @@ import datetime
 
 from abc import ABCMeta, abstractmethod
 from enum import Enum
-from typing import List, Optional, Dict
+from typing import List, Optional, Dict, Union
 
 import attr
 import dateutil.parser
+import iso8601
 
 from .identifiers import (
     normalize_timestamp, directory_identifier, revision_identifier,
@@ -124,15 +125,31 @@ class TimestampWithTimezone(BaseModel):
             raise ValueError('offset too large: %d minutes' % value)
 
     @classmethod
-    def from_dict(cls, d):
+    def from_dict(cls, obj: Union[Dict, datetime.datetime, int]):
         """Builds a TimestampWithTimezone from any of the formats
         accepted by :func:`swh.model.normalize_timestamp`."""
-        d = normalize_timestamp(d)
+        # TODO: this accept way more types than just dicts; find a better
+        # name
+        d = normalize_timestamp(obj)
         return cls(
             timestamp=Timestamp.from_dict(d['timestamp']),
             offset=d['offset'],
             negative_utc=d['negative_utc'])
 
+    @classmethod
+    def from_datetime(cls, dt: datetime.datetime):
+        return cls.from_dict(dt)
+
+    @classmethod
+    def from_iso8601(cls, s):
+        """Builds a TimestampWithTimezone from an ISO8601-formatted string.
+        """
+        dt = iso8601.parse_date(s)
+        tstz = cls.from_datetime(dt)
+        if dt.tzname() == '-00:00':
+            tstz = attr.evolve(tstz, negative_utc=True)
+        return tstz
+
 
 @attr.s(frozen=True)
 class Origin(BaseModel):
diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py
index 82f3dc96..a97c3926 100644
--- a/swh/model/tests/test_model.py
+++ b/swh/model/tests/test_model.py
@@ -4,12 +4,16 @@
 # See top-level LICENSE file for more information
 
 import copy
+import datetime
 
 from hypothesis import given
 import pytest
 
-from swh.model.model import Content, Directory, Revision, Release, Snapshot
-from swh.model.model import MissingData
+from swh.model.model import (
+    Content, Directory, Revision, Release, Snapshot,
+    Timestamp, TimestampWithTimezone,
+    MissingData,
+)
 from swh.model.hashutil import hash_to_bytes
 from swh.model.hypothesis_strategies import objects, origins, origin_visits
 from swh.model.identifiers import (
@@ -56,6 +60,53 @@ def test_todict_origin_visits(origin_visit):
     assert origin_visit == type(origin_visit).from_dict(obj)
 
 
+def test_timestampwithtimezone_from_datetime():
+    tz = datetime.timezone(datetime.timedelta(minutes=+60))
+    date = datetime.datetime(
+        2020, 2, 27, 14, 39, 19, tzinfo=tz)
+
+    tstz = TimestampWithTimezone.from_datetime(date)
+
+    assert tstz == TimestampWithTimezone(
+        timestamp=Timestamp(
+            seconds=1582810759,
+            microseconds=0,
+        ),
+        offset=60,
+        negative_utc=False,
+    )
+
+
+def test_timestampwithtimezone_from_iso8601():
+    date = '2020-02-27 14:39:19.123456+0100'
+
+    tstz = TimestampWithTimezone.from_iso8601(date)
+
+    assert tstz == TimestampWithTimezone(
+        timestamp=Timestamp(
+            seconds=1582810759,
+            microseconds=123456,
+        ),
+        offset=60,
+        negative_utc=False,
+    )
+
+
+def test_timestampwithtimezone_from_iso8601_negative_utc():
+    date = '2020-02-27 13:39:19-0000'
+
+    tstz = TimestampWithTimezone.from_iso8601(date)
+
+    assert tstz == TimestampWithTimezone(
+        timestamp=Timestamp(
+            seconds=1582810759,
+            microseconds=0,
+        ),
+        offset=0,
+        negative_utc=True,
+    )
+
+
 def test_content_get_hash():
     hashes = dict(
         sha1=b'foo', sha1_git=b'bar', sha256=b'baz', blake2s256=b'qux')
-- 
GitLab