Repository: ResearchObject/ro-crate-py

Issue:
Ensure Entity.properties yields ready-to-write JSON
This line in the root dataset code auto-converts `datePublished` into a Python datetime object:
https://github.com/ResearchObject/ro-crate-py/blob/4ab8975b9478b7ba2f13fb0d13bd1b3b5287ac54/rocrate/model/root_dataset.py#L28

Things are fine when writing an RO-Crate, since this is handled in the `write` method of the `Medatata` class. However, users might want to generate the JSON metadata for the crate without necessarily writing it out. We should return serialized JSON from the `properties` method.

Discussion:

Patch:
diff --git a/requirements.txt b/requirements.txt
index 4eca3b0..869d0d0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,3 +7,4 @@ urllib3==1.25.6
 arcp==0.2.1
 galaxy2cwl
 jinja2
+python-dateutil
diff --git a/rocrate/_version.py b/rocrate/_version.py
index 3d718ba..b5fdc75 100644
--- a/rocrate/_version.py
+++ b/rocrate/_version.py
@@ -1,1 +1,1 @@
-__version__ = "0.2.3.dev0"
+__version__ = "0.2.2"
diff --git a/rocrate/model/dataset.py b/rocrate/model/dataset.py
index 0f98409..6cbd624 100644
--- a/rocrate/model/dataset.py
+++ b/rocrate/model/dataset.py
@@ -47,19 +47,6 @@ class Dataset(DataEntity):
     def format_id(self, identifier):
         return identifier.rstrip("/") + "/"
 
-    # name contentSize dateModified encodingFormat identifier sameAs
-    @property
-    def datePublished(self):
-        date = self["datePublished"]
-        return date and datetime.datetime.fromisoformat(date)
-
-    @datePublished.setter
-    def datePublished(self, date):
-        if hasattr(date, "isoformat"):  # datetime object
-            self["datePublished"] = date.isoformat()
-        else:
-            self["datePublished"] = str(date)
-
     def directory_entries(self, base_path=None):
         # iterate over the source dir contents to list all entries
         directory_entries = []
diff --git a/rocrate/model/entity.py b/rocrate/model/entity.py
index d127572..55d2738 100644
--- a/rocrate/model/entity.py
+++ b/rocrate/model/entity.py
@@ -19,6 +19,7 @@
 
 import uuid
 
+from dateutil.parser import isoparse
 from .. import vocabs
 
 
@@ -123,3 +124,16 @@ class Entity(object):
 
     def filepath(self):
         return self.id
+
+    @property
+    def datePublished(self):
+        d = self['datePublished']
+        return d if not d else isoparse(d)
+
+    @datePublished.setter
+    def datePublished(self, value):
+        try:
+            value = value.isoformat()
+        except AttributeError:
+            pass
+        self['datePublished'] = value
diff --git a/rocrate/model/metadata.py b/rocrate/model/metadata.py
index 357c7d7..aff8a95 100644
--- a/rocrate/model/metadata.py
+++ b/rocrate/model/metadata.py
@@ -21,7 +21,6 @@ import os
 import json
 import tempfile
 
-from ..utils import json_serial
 from .file import File
 from .dataset import Dataset
 
@@ -59,8 +58,7 @@ class Metadata(File):
         write_path = self.filepath(base_path)
         as_jsonld = self.generate()
         with open(write_path, 'w') as outfile:
-            json.dump(as_jsonld, outfile, indent=4, sort_keys=True,
-                      default=str)
+            json.dump(as_jsonld, outfile, indent=4, sort_keys=True)
 
     def write_zip(self, zip_out):
         write_path = self.filepath()
@@ -69,8 +67,7 @@ class Metadata(File):
         # TODO: fix this, there is no need to use a tmp file
         tmpfile = tempfile.NamedTemporaryFile(mode='w', delete=False)
         tmpfile_path = tmpfile.name
-        json.dump(as_jsonld, tmpfile, indent=4, sort_keys=True,
-                  default=json_serial)
+        json.dump(as_jsonld, tmpfile, indent=4, sort_keys=True)
         tmpfile.close()
         zip_out.write(tmpfile_path, write_path)
         os.remove(tmpfile_path)
diff --git a/rocrate/model/root_dataset.py b/rocrate/model/root_dataset.py
index 70aedfa..4e51f1c 100644
--- a/rocrate/model/root_dataset.py
+++ b/rocrate/model/root_dataset.py
@@ -25,7 +25,9 @@ from .dataset import Dataset
 class RootDataset(Dataset):
 
     def __init__(self, crate, properties=None):
-        default_properties = {'datePublished': datetime.datetime.now()}
+        default_properties = {
+            'datePublished': datetime.datetime.now().isoformat()
+        }
         if properties:
             default_properties.update(properties)
         super(RootDataset, self).__init__(crate, None, './',
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index 02ccf7f..f718fb6 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -243,14 +243,13 @@ class ROCrate():
     def name(self, value):
         self.root_dataset['name'] = value
 
-    # TODO: should change to dateCreated or that is only for workflowhub?
     @property
     def datePublished(self):
-        return self.root_dataset['datePublished']
+        return self.root_dataset.datePublished
 
     @datePublished.setter
     def datePublished(self, value):
-        self.root_dataset['datePublished'] = value
+        self.root_dataset.datePublished = value
 
     @property
     def creator(self):
diff --git a/rocrate/utils.py b/rocrate/utils.py
index 481e36a..d1f71cf 100644
--- a/rocrate/utils.py
+++ b/rocrate/utils.py
@@ -40,10 +40,3 @@ def as_list(list_or_other):
         and not isinstance(list_or_other, str)):  # FIXME: bytes?
         return list_or_other
     return [list_or_other]
-
-
-def json_serial(obj):
-    """JSON serializer for objects not serializable by default json code"""
-    if isinstance(obj, (datetime, date)):
-        return obj.isoformat()
-    raise TypeError("Type %s not serializable" % type(obj))


Test patch:
diff --git a/test/test_model.py b/test/test_model.py
index c3936af..84bea97 100644
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -65,6 +65,7 @@ def test_contextual_entities():
     person_prop = person_dereference.properties()
     assert person_prop['@type'] == 'Person'
     assert person_prop['name'] == 'Joe Pesci'
+    assert not new_person.datePublished
 
 
 def test_properties():
@@ -78,6 +79,9 @@ def test_properties():
     crate.description = crate_description
     assert crate.description == crate_description
 
+    assert crate.datePublished == crate.root_dataset.datePublished
+    assert isinstance(crate.root_dataset.datePublished, datetime.datetime)
+    assert isinstance(crate.root_dataset["datePublished"], str)
     crate_datePublished = datetime.datetime.now()
     crate.datePublished = crate_datePublished
     assert crate.datePublished == crate_datePublished