diff --git a/PKG-INFO b/PKG-INFO index 8230755c7c18dff8e532ec7e02cdc9b9dd90a599..1942719b08e3ebbf416fe0fa9e031538eebede0e 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: swh.model -Version: 2.2.0 +Version: 2.3.0 Summary: Software Heritage data model Home-page: https://forge.softwareheritage.org/diffusion/DMOD/ Author: Software Heritage developers diff --git a/debian/changelog b/debian/changelog index a4e89de8c9fe7e99957b5e74e13b57288afb97e1..39a68119471c3498ca2572ed214cecb9e8255d75 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,12 @@ -swh-model (2.2.0-1~swh1~bpo10+1) buster-swh; urgency=medium +swh-model (2.3.0-1~swh1) unstable-swh; urgency=medium - * Rebuild for buster-swh + * New upstream release 2.3.0 - (tagged by Nicolas Dandrimont + <nicolas@dandrimont.eu> on 2021-03-19 17:15:00 +0100) + * Upstream changes: - Release swh.model 2.3.0 - Properly + truncate RawExtrinsicMetadata objects to a precision of one - + second, as does their unique id. - -- Software Heritage autobuilder (on jenkins-debian1) <jenkins@jenkins-debian1.internal.softwareheritage.org> Mon, 15 Mar 2021 09:36:37 +0000 + -- Software Heritage autobuilder (on jenkins-debian1) <jenkins@jenkins-debian1.internal.softwareheritage.org> Fri, 19 Mar 2021 16:17:48 +0000 swh-model (2.2.0-1~swh1) unstable-swh; urgency=medium diff --git a/swh.model.egg-info/PKG-INFO b/swh.model.egg-info/PKG-INFO index 8230755c7c18dff8e532ec7e02cdc9b9dd90a599..1942719b08e3ebbf416fe0fa9e031538eebede0e 100644 --- a/swh.model.egg-info/PKG-INFO +++ b/swh.model.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: swh.model -Version: 2.2.0 +Version: 2.3.0 Summary: Software Heritage data model Home-page: https://forge.softwareheritage.org/diffusion/DMOD/ Author: Software Heritage developers diff --git a/swh/model/model.py b/swh/model/model.py index 2030da8c1b615d2330472254cd4cb6a4d53e079a..1781a3660b9c916cb742d34579b329e3593052b8 100644 --- a/swh/model/model.py +++ b/swh/model/model.py @@ -885,6 +885,17 @@ class MetadataFetcher(BaseModel): return {"name": self.name, "version": self.version} +def normalize_discovery_date(value: Any) -> datetime.datetime: + if not isinstance(value, datetime.datetime): + raise TypeError("discovery_date must be a timezone-aware datetime.") + + if value.tzinfo is None: + raise ValueError("discovery_date must be a timezone-aware datetime.") + + # Normalize timezone to utc, and truncate microseconds to 0 + return value.astimezone(datetime.timezone.utc).replace(microsecond=0) + + @attr.s(frozen=True, slots=True) class RawExtrinsicMetadata(HashableObject, BaseModel): object_type: Final = "raw_extrinsic_metadata" @@ -893,7 +904,7 @@ class RawExtrinsicMetadata(HashableObject, BaseModel): target = attr.ib(type=ExtendedSWHID, validator=type_validator()) # source - discovery_date = attr.ib(type=datetime.datetime, validator=type_validator()) + discovery_date = attr.ib(type=datetime.datetime, converter=normalize_discovery_date) authority = attr.ib(type=MetadataAuthority, validator=type_validator()) fetcher = attr.ib(type=MetadataFetcher, validator=type_validator()) @@ -923,12 +934,6 @@ class RawExtrinsicMetadata(HashableObject, BaseModel): def compute_hash(self) -> bytes: return hash_to_bytes(raw_extrinsic_metadata_identifier(self.to_dict())) - @discovery_date.validator - def check_discovery_date(self, attribute, value): - """Checks the discovery_date has a timezone.""" - if value is not None and value.tzinfo is None: - raise ValueError("discovery_date must be a timezone-aware datetime.") - @origin.validator def check_origin(self, attribute, value): if value is None: diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py index 3c55b07cff38f6e750a2390fcff223e605bd2c18..5ea401c1befbe933b1d982dfebb80979527d9b9f 100644 --- a/swh/model/tests/test_model.py +++ b/swh/model/tests/test_model.py @@ -1140,3 +1140,39 @@ def test_metadata_validate_context_directory(): ), **_common_metadata_fields, ) + + +def test_metadata_normalize_discovery_date(): + fields_copy = {**_common_metadata_fields} + truncated_date = fields_copy.pop("discovery_date") + assert truncated_date.microsecond == 0 + + # Check for TypeError on disabled object type: we removed attrs_strict's + # type_validator + with pytest.raises(TypeError): + RawExtrinsicMetadata( + target=_content_swhid, discovery_date="not a datetime", **fields_copy + ) + + # Check for truncation to integral second + date_with_us = truncated_date.replace(microsecond=42) + md = RawExtrinsicMetadata( + target=_content_swhid, discovery_date=date_with_us, **fields_copy, + ) + + assert md.discovery_date == truncated_date + assert md.discovery_date.tzinfo == datetime.timezone.utc + + # Check that the timezone gets normalized. Timezones can be offset by a + # non-integral number of seconds, so we need to handle that. + timezone = datetime.timezone(offset=datetime.timedelta(hours=2)) + date_with_tz = truncated_date.astimezone(timezone) + + assert date_with_tz.tzinfo != datetime.timezone.utc + + md = RawExtrinsicMetadata( + target=_content_swhid, discovery_date=date_with_tz, **fields_copy, + ) + + assert md.discovery_date == truncated_date + assert md.discovery_date.tzinfo == datetime.timezone.utc