diff --git a/PKG-INFO b/PKG-INFO index 027e2e19ff8cdb9c1e90878a33ca7b65d53aa0d6..05a543d5a0519428ad58701d005373b828c54e8e 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: swh.model -Version: 6.6.3 +Version: 6.7.0 Summary: Software Heritage data model Home-page: https://forge.softwareheritage.org/diffusion/DMOD/ Author: Software Heritage developers diff --git a/swh.model.egg-info/PKG-INFO b/swh.model.egg-info/PKG-INFO index 027e2e19ff8cdb9c1e90878a33ca7b65d53aa0d6..05a543d5a0519428ad58701d005373b828c54e8e 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: 6.6.3 +Version: 6.7.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 3572284d6fb328520ae82c5ca86352abd4a0e94b..03b35d6cf06370db7e359b35f69e3cfa4d21949c 100644 --- a/swh/model/model.py +++ b/swh/model/model.py @@ -858,6 +858,14 @@ class OriginVisitStatus(BaseModel): def unique_key(self) -> KeyType: return {"origin": self.origin, "visit": str(self.visit), "date": str(self.date)} + def origin_swhid(self) -> ExtendedSWHID: + return Origin(url=self.origin).swhid() + + def snapshot_swhid(self) -> Optional[CoreSWHID]: + if self.snapshot is None: + return None + return CoreSWHID(object_type=SwhidObjectType.SNAPSHOT, object_id=self.snapshot) + class TargetType(Enum): """The type of content pointed to by a snapshot branch. Usually a @@ -910,6 +918,15 @@ class SnapshotBranch(BaseModel): def from_dict(cls, d): return cls(target=d["target"], target_type=TargetType(d["target_type"])) + def swhid(self) -> Optional[CoreSWHID]: + """Returns a SWHID for the current branch or None if the branch has no + target or is an alias.""" + if self.target is None or self.target_type == TargetType.ALIAS: + return None + return CoreSWHID( + object_id=self.target, object_type=SwhidObjectType[self.target_type.name] + ) + @attr.s(frozen=True, slots=True, field_transformer=optimize_all_validators) class Snapshot(HashableObject, BaseModel): @@ -1005,6 +1022,14 @@ class Release(HashableObjectWithManifest, BaseModel): """Returns a SWHID representing this object.""" return CoreSWHID(object_type=SwhidObjectType.RELEASE, object_id=self.id) + def target_swhid(self) -> Optional[CoreSWHID]: + """Returns the SWHID for the target of this release or None if unset.""" + if self.target is None: + return None + return CoreSWHID( + object_id=self.target, object_type=SwhidObjectType[self.target_type.name] + ) + def anonymize(self) -> "Release": """Returns an anonymized version of the Release object. @@ -1133,6 +1158,19 @@ class Revision(HashableObjectWithManifest, BaseModel): """Returns a SWHID representing this object.""" return CoreSWHID(object_type=SwhidObjectType.REVISION, object_id=self.id) + def directory_swhid(self) -> CoreSWHID: + """Returns the SWHID for the directory referenced by the revision.""" + return CoreSWHID( + object_type=SwhidObjectType.DIRECTORY, object_id=self.directory + ) + + def parent_swhids(self) -> List[CoreSWHID]: + """Returns a list of SWHID for the parent revisions.""" + return [ + CoreSWHID(object_type=SwhidObjectType.REVISION, object_id=parent) + for parent in self.parents + ] + def anonymize(self) -> "Revision": """Returns an anonymized version of the Revision object. @@ -1159,6 +1197,12 @@ class DirectoryEntry(BaseModel): perms = attr.ib(type=int, validator=generic_type_validator, converter=int, repr=oct) """Usually one of the values of `swh.model.from_disk.DentryPerms`.""" + DIR_ENTRY_TYPE_TO_SWHID_OBJECT_TYPE = { + "file": SwhidObjectType.CONTENT, + "dir": SwhidObjectType.DIRECTORY, + "rev": SwhidObjectType.REVISION, + } + @name.validator def check_name(self, attribute, value): if value.__class__ is not bytes: @@ -1166,6 +1210,13 @@ class DirectoryEntry(BaseModel): if b"/" in value: raise ValueError(f"{value!r} is not a valid directory entry name.") + def swhid(self) -> CoreSWHID: + """Returns a SWHID for this directory entry""" + return CoreSWHID( + object_type=DirectoryEntry.DIR_ENTRY_TYPE_TO_SWHID_OBJECT_TYPE[self.type], + object_id=self.target, + ) + @attr.s(frozen=True, slots=True, field_transformer=optimize_all_validators) class Directory(HashableObjectWithManifest, BaseModel): @@ -1505,6 +1556,12 @@ class SkippedContent(BaseContent): def unique_key(self) -> KeyType: return self.hashes() + def swhid(self) -> Optional[CoreSWHID]: + """Returns a SWHID representing this object or None if unset.""" + if self.sha1_git is None: + return None + return CoreSWHID(object_type=SwhidObjectType.CONTENT, object_id=self.sha1_git) + class MetadataAuthorityType(Enum): DEPOSIT_CLIENT = "deposit_client" diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py index 409cc8bbe04b7ceb705bedc42fcc8b9433f764f0..920c8dd5c91799e5c8bc572f897586a575a13b7d 100644 --- a/swh/model/tests/test_model.py +++ b/swh/model/tests/test_model.py @@ -426,6 +426,30 @@ def test_origin_visit_status_naive_datetime(): ) +@pytest.fixture +def origin_visit_status_example(): + tz = datetime.timezone(datetime.timedelta(minutes=+60)) + return OriginVisitStatus( + origin="http://foo/", + visit=42, + date=datetime.datetime.now(tz=tz), + status="full", + snapshot=hash_to_bytes("6e65b86363953b780d92b0a928f3e8fcdd10db36"), + ) + + +def test_origin_visit_status_snapshot_swhid(origin_visit_status_example): + assert origin_visit_status_example.snapshot_swhid() == CoreSWHID.from_string( + "swh:1:snp:6e65b86363953b780d92b0a928f3e8fcdd10db36" + ) + + +def test_origin_visit_status_origin_swhid(origin_visit_status_example): + assert origin_visit_status_example.origin_swhid() == ExtendedSWHID.from_string( + "swh:1:ori:e0cee4b024ab93b037a1c182865942f5430c6fa4" + ) + + # Timestamp @@ -895,6 +919,13 @@ def test_skipped_content_naive_datetime(): ) +def test_skipped_content_swhid(): + skipped_content = SkippedContent.from_data(b"foo", reason="reason") + assert skipped_content.swhid() == CoreSWHID.from_string( + "swh:1:cnt:19102815663d23f8b75a47e7a01965dcdc96468c" + ) + + # Directory @@ -1092,6 +1123,42 @@ def test_directory_from_possibly_duplicated_entries__preserve_manifest(): assert dir_.raw_manifest == b"blah" +@pytest.fixture +def directory_with_every_possible_type(): + return Directory.from_dict( + { + "entries": [ + { + "type": "file", + "perms": 33188, + "name": b"README", + "target": hash_to_bytes("37ec8ea2110c0b7a32fbb0e872f6e7debbf95e21"), + }, + { + "type": "dir", + "perms": 16384, + "name": b"src", + "target": hash_to_bytes("61e6e867f5d7ba3b40540869bc050b0c4fed9e95"), + }, + { + "type": "rev", + "perms": 57344, + "name": b"submodule", + "target": hash_to_bytes("3d531e169db92a16a9a8974f0ae6edf52e52659e"), + }, + ], + } + ) + + +def test_directory_entry_swhids(directory_with_every_possible_type): + assert [entry.swhid() for entry in directory_with_every_possible_type.entries] == [ + CoreSWHID.from_string("swh:1:cnt:37ec8ea2110c0b7a32fbb0e872f6e7debbf95e21"), + CoreSWHID.from_string("swh:1:dir:61e6e867f5d7ba3b40540869bc050b0c4fed9e95"), + CoreSWHID.from_string("swh:1:rev:3d531e169db92a16a9a8974f0ae6edf52e52659e"), + ] + + # Release @@ -1129,6 +1196,13 @@ def test_release_raw_manifest(release): release2.check() +def test_release_target_swhid(): + release = Release.from_dict(release_example) + assert release.target_swhid() == CoreSWHID.from_string( + "swh:1:rev:741b2252a5e14d6c60a913c77a6099abe73a854a" + ) + + # Revision @@ -1379,6 +1453,55 @@ def test_revision_none_author_or_committer(): Revision.from_dict(rev_dict) +def test_revision_directory_swhid(): + revision = Revision.from_dict(revision_example) + assert revision.directory_swhid() == CoreSWHID.from_string( + "swh:1:dir:85a74718d377195e1efd0843ba4f3260bad4fe07" + ) + + +def test_revision_parent_swhids(): + revision_d = revision_example.copy() + revision_d["parents"].append( + hash_to_bytes("b2a7e1260492e344fab3cbf91bc13c91e05426fd") + ) + revision = Revision.from_dict(revision_d) + assert revision.parent_swhids() == [ + CoreSWHID.from_string("swh:1:rev:01e2d0627a9a6edb24c37db45db5ecb31e9de808"), + CoreSWHID.from_string("swh:1:rev:b2a7e1260492e344fab3cbf91bc13c91e05426fd"), + ] + + +@pytest.fixture +def snapshot_with_all_types(): + return Snapshot.from_dict(snapshot_example) + + +def test_snapshot_branch_swhids(snapshot_with_all_types): + assert { + name: branch and branch.swhid() + for (name, branch) in snapshot_with_all_types.branches.items() + } == { + b"directory": CoreSWHID.from_string( + "swh:1:dir:1bd0e65f7d2ff14ae994de17a1e7fe65111dcad8" + ), + b"content": CoreSWHID.from_string( + "swh:1:cnt:fe95a46679d128ff167b7c55df5d02356c5a1ae1" + ), + b"alias": None, + b"revision": CoreSWHID.from_string( + "swh:1:rev:aafb16d69fd30ff58afdd69036a26047f3aebdc6" + ), + b"release": CoreSWHID.from_string( + "swh:1:rel:7045404f3d1c54e6473c71bbb716529fbad4be24" + ), + b"snapshot": CoreSWHID.from_string( + "swh:1:snp:1a8893e6a86f444e8be8e7bda6cb34fb1735a00e" + ), + b"dangling": None, + } + + @given(strategies.objects(split_content=True)) def test_object_type(objtype_and_obj): obj_type, obj = objtype_and_obj diff --git a/tox.ini b/tox.ini index e042fedd23469697bcd76417ffd0c2e9f326137f..c905abc9dc4dc375d4b20fa87b35225360be3e63 100644 --- a/tox.ini +++ b/tox.ini @@ -43,7 +43,7 @@ commands = extras = testing deps = - mypy==1.0 + mypy==1.0.1 commands = mypy swh