diff --git a/python_wikibase/data_types/__init__.py b/python_wikibase/data_types/__init__.py index 3578069..17d73a5 100644 --- a/python_wikibase/data_types/__init__.py +++ b/python_wikibase/data_types/__init__.py @@ -2,5 +2,6 @@ from python_wikibase.data_types.geo_location import GeoLocation from python_wikibase.data_types.quantity import Quantity from python_wikibase.data_types.string_value import StringValue +from python_wikibase.data_types.time import Time -__all__ = ["ExternalId", "GeoLocation", "Quantity", "StringValue"] +__all__ = ["ExternalId", "GeoLocation", "Quantity", "StringValue", "Time"] diff --git a/python_wikibase/data_types/data_type.py b/python_wikibase/data_types/data_type.py index e28717d..105f1ec 100644 --- a/python_wikibase/data_types/data_type.py +++ b/python_wikibase/data_types/data_type.py @@ -50,7 +50,7 @@ def unmarshal_data_value(py_wb, main_snak): elif data_type == "tabular-data": raise NotImplementedError # TODO elif data_type == "time": - raise NotImplementedError # TODO + return py_wb.Time().unmarshal(data_value) elif data_type == "url": raise NotImplementedError # TODO elif data_type == "wikibase-form": diff --git a/python_wikibase/data_types/time.py b/python_wikibase/data_types/time.py new file mode 100644 index 0000000..cc83f52 --- /dev/null +++ b/python_wikibase/data_types/time.py @@ -0,0 +1,84 @@ +import collections +import datetime + +from python_wikibase.data_types.data_type import DataType + +GREGORIAN_CALENDAR_WIKIDATA_URL = "http://www.wikidata.org/entity/Q1985727" + + +class DataTypeException(BaseException): + pass + +# Largely inspired by https://github.com/dahlia/wikidata/blob/master/wikidata/datavalue.py + + +class Time(DataType): + def __init__(self, py_wb, api, language): + super().__init__(py_wb, api, language) + self.value = None + + def __str__(self): + return self.value.isoformat() + + def unmarshal(self, data_value): + value = data_value["value"] + + calendar_model = value['calendarmodel'] + if calendar_model != GREGORIAN_CALENDAR_WIKIDATA_URL: + raise DataTypeException(f"Unsupported calendar model: [{calendar_model}]") + + # See https://www.mediawiki.org/wiki/Wikibase/DataModel/JSON#time + # for property to extract from data_value + + time = value['time'] + # Strip '+' at beginning of string and 'Z' at the end + time = time[1:-1] + + tz = value['timezone'] + # Don't take in account 'before' and 'after' property + precision = value['precision'] + + if precision not in (11, 14): + raise DataTypeException(f"Not supported precision: [{precistion}]") + + if precision == 11: + self.value = datetime.date.fromisoformat(time[:-9]) + elif precision == 14: + val = datetime.datetime.fromisoformat(time) + self.value = val.replace(tzinfo=datetime.timezone(offset=datetime.timedelta(minutes=tz))) + return self + + def marshal(self): + + common_properties = { + "timezone": 0, + "before": 0, + "after": 0, + "calendarmodel": GREGORIAN_CALENDAR_WIKIDATA_URL + } + + if isinstance(self.value, datetime.date): + return { + **common_properties, + "time": "+{}T00:00:00Z".format(self.value.isoformat()), + "precision": 11 + } + elif isinstance(self.value, datetime.datetime): + dt = self.value.astimezone(tz=datetime.timezone.utc) + return { + **common_properties, + "time": "+{}Z".format(iso_str[:19]), + "precision": 14 + } + + def create(self, value): + if isinstance(value, datetime.datetime): + # consider as utc if timezone info is missing + if value.tzinfo is None: + value = value.replace(tzinfo=datetime.timezone.utc) + + elif not isinstance(value, datetime.date): + raise DataTypeException("Unknown value type {!r}".format(value)) + self.value = value + + return self diff --git a/python_wikibase/python_wikibase.py b/python_wikibase/python_wikibase.py index ce8c7f0..da174d6 100644 --- a/python_wikibase/python_wikibase.py +++ b/python_wikibase/python_wikibase.py @@ -13,7 +13,7 @@ Reference, References, ) -from python_wikibase.data_types import ExternalId, GeoLocation, Quantity, StringValue +from python_wikibase.data_types import ExternalId, GeoLocation, Quantity, StringValue, Time DEFAULT_CONFIG = { "api_url": "https://www.wikidata.org/w/api.php", @@ -96,3 +96,6 @@ def Quantity(self): def StringValue(self): return StringValue(self, self.api, self.language) + + def Time(self): + return Time(self, self.api, self.language) diff --git a/tests/conftest.py b/tests/conftest.py index 7bda2c4..83cd37a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ PROP_ITEM_LABEL, PROP_LABEL, PROP_QUANTITY_LABEL, + PROP_TIME_LABEL, STRING_VALUE, ) @@ -110,8 +111,16 @@ def prop_quantity(py_wb): prop.delete() +@pytest.fixture(scope="function") +def prop_time(py_wb): + prop = py_wb.Property().create(PROP_TIME_LABEL, data_type="Time") + assert prop.label.get(LANGUAGE) == PROP_TIME_LABEL + yield prop + prop.delete() + # Values + @pytest.fixture(scope="function") def string_value(py_wb): string_value = py_wb.StringValue().create(STRING_VALUE) diff --git a/tests/constants.py b/tests/constants.py index 6be72eb..e4841a8 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -11,5 +11,6 @@ PROP_ITEM_LABEL = "Wikibase item property" PROP_GEO_LOCATION_LABEL = "GeoLocation property" PROP_QUANTITY_LABEL = "Quantity property" +PROP_TIME_LABEL = "Time property" STRING_VALUE = "String value" diff --git a/tests/test_claim.py b/tests/test_claim.py index 0687125..a605f03 100644 --- a/tests/test_claim.py +++ b/tests/test_claim.py @@ -78,3 +78,13 @@ def test_quantity_with_unit(self, py_wb, item, prop_quantity, item_unit): assert claim.value.amount == amount assert float(claim.value) == amount assert claim.value.marshal() == quantity.marshal() + + # Time + + def test_time(self, py_wb, item, prop_time): + date = datetime.date.fromisoformart("2012-05-12") + time_ = py_wb.Time().create(date_str) + claim = item.claims.add(prop_time, time_) + assert claim.property.data_type == "Time" + assert claim.value.date == date + assert claim.value.marshal() == time_.marshal()