Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py
index 946b423..2be89f2 100644
--- a/swh/graphql/resolvers/resolvers.py
+++ b/swh/graphql/resolvers/resolvers.py
@@ -1,299 +1,299 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
"""
High level resolvers
"""
# Any schema attribute can be resolved by any of the following ways
# and in the following priority order
# - In this module using a decorator (eg: @visitstatus.field("snapshot")
# Every object (type) is expected to resolve this way as they can accept arguments
# eg: origin.visits takes arguments to paginate
# - As a property in the Node object (eg: resolvers.visit.BaseVisitNode.id)
# Every scalar is expected to resolve this way
# - As an attribute/item in the object/dict returned by a backend (eg: Origin.url)
from ariadne import ObjectType, UnionType
from graphql.type import GraphQLResolveInfo
from swh.graphql import resolvers as rs
from swh.graphql.utils import utils
from .resolver_factory import get_connection_resolver, get_node_resolver
query: ObjectType = ObjectType("Query")
origin: ObjectType = ObjectType("Origin")
visit: ObjectType = ObjectType("Visit")
visit_status: ObjectType = ObjectType("VisitStatus")
snapshot: ObjectType = ObjectType("Snapshot")
snapshot_branch: ObjectType = ObjectType("Branch")
revision: ObjectType = ObjectType("Revision")
release: ObjectType = ObjectType("Release")
directory: ObjectType = ObjectType("Directory")
directory_entry: ObjectType = ObjectType("DirectoryEntry")
search_result: ObjectType = ObjectType("SearchResult")
binary_string: ObjectType = ObjectType("BinaryString")
branch_target: UnionType = UnionType("BranchTarget")
release_target: UnionType = UnionType("ReleaseTarget")
directory_entry_target: UnionType = UnionType("DirectoryEntryTarget")
search_result_target: UnionType = UnionType("SearchResultTarget")
# Node resolvers
# A node resolver should return an instance of BaseNode
@query.field("origin")
def origin_resolver(obj: None, info: GraphQLResolveInfo, **kw) -> rs.origin.OriginNode:
""" """
resolver = get_node_resolver("origin")
return resolver(obj, info, **kw)
@origin.field("latestVisit")
def latest_visit_resolver(
obj: rs.origin.OriginNode, info: GraphQLResolveInfo, **kw
) -> rs.visit.LatestVisitNode:
""" """
resolver = get_node_resolver("latest-visit")
return resolver(obj, info, **kw)
@query.field("visit")
def visit_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.visit.OriginVisitNode:
""" """
resolver = get_node_resolver("visit")
return resolver(obj, info, **kw)
@visit.field("latestStatus")
def latest_visit_status_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.visit_status.LatestVisitStatusNode:
""" """
resolver = get_node_resolver("latest-status")
return resolver(obj, info, **kw)
@query.field("snapshot")
def snapshot_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.snapshot.SnapshotNode:
""" """
resolver = get_node_resolver("snapshot")
return resolver(obj, info, **kw)
@visit_status.field("snapshot")
def visit_snapshot_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.snapshot.VisitSnapshotNode:
resolver = get_node_resolver("visit-snapshot")
return resolver(obj, info, **kw)
@snapshot_branch.field("target")
def snapshot_branch_target_resolver(
obj: rs.snapshot_branch.BaseSnapshotBranchNode, info: GraphQLResolveInfo, **kw
):
"""
Snapshot branch target can be a revision, release, directory,
content, snapshot or a branch itself (alias type)
"""
resolver_type = f"branch-{obj.type}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)
@query.field("revision")
def revision_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.revision.RevisionNode:
resolver = get_node_resolver("revision")
return resolver(obj, info, **kw)
@revision.field("directory")
def revision_directory_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.directory.RevisionDirectoryNode:
resolver = get_node_resolver("revision-directory")
return resolver(obj, info, **kw)
@query.field("release")
def release_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.release.ReleaseNode:
resolver = get_node_resolver("release")
return resolver(obj, info, **kw)
@release.field("target")
def release_target_resolver(obj, info: GraphQLResolveInfo, **kw):
"""
release target can be a release, revision,
directory or content
obj is release here, target type is
obj.target_type
"""
resolver_type = f"release-{obj.target_type.value}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)
@query.field("directory")
def directory_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.directory.DirectoryNode:
resolver = get_node_resolver("directory")
return resolver(obj, info, **kw)
@directory_entry.field("target")
def directory_entry_target_resolver(
obj: rs.directory_entry.DirectoryEntryNode, info: GraphQLResolveInfo, **kw
):
"""
directory entry target can be a directory or a content
"""
resolver_type = f"dir-entry-{obj.type}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)
@query.field("content")
def content_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.content.ContentNode:
resolver = get_node_resolver("content")
return resolver(obj, info, **kw)
@search_result.field("target")
def search_result_target_resolver(
obj: rs.search.SearchResultNode, info: GraphQLResolveInfo, **kw
):
resolver_type = f"search-result-{obj.type}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)
@query.field("contentByHash")
def content_by_hash_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.content.ContentNode:
resolver = get_node_resolver("content-by-hash")
return resolver(obj, info, **kw)
# Connection resolvers
# A connection resolver should return an instance of BaseConnection
@query.field("origins")
def origins_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.origin.OriginConnection:
resolver = get_connection_resolver("origins")
return resolver(obj, info, **kw)
@origin.field("visits")
def visits_resolver(
obj: rs.origin.OriginNode, info: GraphQLResolveInfo, **kw
) -> rs.visit.OriginVisitConnection:
resolver = get_connection_resolver("origin-visits")
return resolver(obj, info, **kw)
@origin.field("snapshots")
def origin_snapshots_resolver(
obj: rs.origin.OriginNode, info: GraphQLResolveInfo, **kw
) -> rs.snapshot.OriginSnapshotConnection:
""" """
resolver = get_connection_resolver("origin-snapshots")
return resolver(obj, info, **kw)
@visit.field("status")
def visitstatus_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.visit_status.VisitStatusConnection:
resolver = get_connection_resolver("visit-status")
return resolver(obj, info, **kw)
@snapshot.field("branches")
def snapshot_branches_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.snapshot_branch.SnapshotBranchConnection:
resolver = get_connection_resolver("snapshot-branches")
return resolver(obj, info, **kw)
@revision.field("parents")
def revision_parents_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.revision.ParentRevisionConnection:
resolver = get_connection_resolver("revision-parents")
return resolver(obj, info, **kw)
-# @revision.field("revisionLog")
-# def revision_log_resolver(obj, info, **kw):
-# resolver = get_connection_resolver("revision-log")
-# return resolver(obj, info, **kw)
+@revision.field("revisionLog")
+def revision_log_resolver(obj, info, **kw):
+ resolver = get_connection_resolver("revision-log")
+ return resolver(obj, info, **kw)
@directory.field("entries")
def directory_entry_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.directory_entry.DirectoryEntryConnection:
resolver = get_connection_resolver("directory-entries")
return resolver(obj, info, **kw)
@query.field("resolveSwhid")
def search_swhid_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.search.ResolveSwhidConnection:
resolver = get_connection_resolver("resolve-swhid")
return resolver(obj, info, **kw)
@query.field("search")
def search_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.search.ResolveSwhidConnection:
resolver = get_connection_resolver("search")
return resolver(obj, info, **kw)
# Any other type of resolver
@release_target.type_resolver
@directory_entry_target.type_resolver
@branch_target.type_resolver
@search_result_target.type_resolver
def union_resolver(obj, *_) -> str:
"""
Generic resolver for all the union types
"""
return obj.is_type_of()
@binary_string.field("text")
def binary_string_text_resolver(obj, *args, **kw):
return obj.decode(utils.ENCODING, "replace")
@binary_string.field("base64")
def binary_string_base64_resolver(obj, *args, **kw):
return utils.get_b64_string(obj)
diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py
index e5cd18b..ab25cb3 100644
--- a/swh/graphql/resolvers/revision.py
+++ b/swh/graphql/resolvers/revision.py
@@ -1,106 +1,110 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
from typing import Union
from swh.graphql.backends import archive
from swh.graphql.utils import utils
+from swh.model.model import Revision
from swh.model.swhids import CoreSWHID, ObjectType
from swh.storage.interface import PagedResult
from .base_connection import BaseConnection
from .base_node import BaseSWHNode
from .release import BaseReleaseNode
from .snapshot_branch import BaseSnapshotBranchNode
class BaseRevisionNode(BaseSWHNode):
"""
Base resolver for all the revision nodes
"""
def _get_revision_by_id(self, revision_id):
return (archive.Archive().get_revisions([revision_id]) or None)[0]
@property
def parent_swhids(self): # for ParentRevisionConnection resolver
return [
CoreSWHID(object_type=ObjectType.REVISION, object_id=parent_id)
for parent_id in self._node.parents
]
@property
def directory_hash(self): # for RevisionDirectoryNode resolver
return self._node.directory
@property
def type(self):
return self._node.type.value
def is_type_of(self):
# is_type_of is required only when resolving a UNION type
# This is for ariadne to return the right type
return "Revision"
class RevisionNode(BaseRevisionNode):
"""
Node resolver for a revision requested directly with its SWHID
"""
def _get_node_data(self):
return self._get_revision_by_id(self.kwargs.get("swhid").object_id)
class TargetRevisionNode(BaseRevisionNode):
"""
Node resolver for a revision requested as a target
"""
obj: Union[BaseSnapshotBranchNode, BaseReleaseNode]
def _get_node_data(self):
# self.obj.target_hash is the requested revision id
return self._get_revision_by_id(self.obj.target_hash)
class ParentRevisionConnection(BaseConnection):
"""
Connection resolver for parent revisions in a revision
"""
obj: BaseRevisionNode
_node_class = BaseRevisionNode
def _get_paged_result(self) -> PagedResult:
# self.obj is the current(child) revision
# self.obj.parent_swhids is the list of parent SWHIDs
# FIXME, using dummy(local) pagination, move pagination to backend
# To remove localpagination, just drop the paginated call
# STORAGE-TODO (pagination)
parents = archive.Archive().get_revisions(
[x.object_id for x in self.obj.parent_swhids]
)
return utils.paginated(parents, self._get_first_arg(), self._get_after_arg())
class LogRevisionConnection(BaseConnection):
"""
Connection resolver for the log (list of revisions) in a revision
"""
obj: BaseRevisionNode
_node_class = BaseRevisionNode
def _get_paged_result(self) -> PagedResult:
- # STORAGE-TODO (date in revisionlog is a dict)
log = archive.Archive().get_revision_log([self.obj.swhid.object_id])
+ # Storage is returning a list of dicts instead of model objects
+ # Following loop is to reverse that operation
+ # STORAGE-TODO; remove to_dict from storage.revision_log
+ log = [Revision.from_dict(rev) for rev in log]
# FIXME, using dummy(local) pagination, move pagination to backend
# To remove localpagination, just drop the paginated call
# STORAGE-TODO (pagination)
return utils.paginated(log, self._get_first_arg(), self._get_after_arg())
diff --git a/swh/graphql/tests/data.py b/swh/graphql/tests/data.py
index aebdcc9..1216816 100644
--- a/swh/graphql/tests/data.py
+++ b/swh/graphql/tests/data.py
@@ -1,90 +1,113 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.model.hashutil import hash_to_bytes
-from swh.model.model import ObjectType, Release
+from swh.model.model import ObjectType, Release, Revision, RevisionType
from swh.model.tests import swh_model_data
def populate_search_data(search):
search.origin_update({"url": origin.url} for origin in get_origins())
def get_origins():
return swh_model_data.ORIGINS
def get_snapshots():
return swh_model_data.SNAPSHOTS
def get_releases():
return swh_model_data.RELEASES
def get_revisions():
return swh_model_data.REVISIONS
def get_contents():
return swh_model_data.CONTENTS
def get_directories():
return swh_model_data.DIRECTORIES
def get_releases_with_target():
"""
GraphQL will not return a target object unless the target id
is present in the DB.
Return release objects with real targets instead of dummy
targets in swh.model.tests.swh_model_data
"""
with_revision = Release(
id=hash_to_bytes("9129dc4e14acd0e51ca3bcd6b80f4577d281fd25"),
name=b"v0.0.1",
target_type=ObjectType.REVISION,
target=get_revisions()[0].id,
message=b"foo",
synthetic=False,
)
with_release = Release(
id=hash_to_bytes("6429dc4e14acd0e51ca3bcd6b80f4577d281fd32"),
name=b"v0.0.1",
target_type=ObjectType.RELEASE,
target=get_releases()[0].id,
message=b"foo",
synthetic=False,
)
with_directory = Release(
id=hash_to_bytes("3129dc4e14acd0e51ca3bcd6b80f4577d281fd42"),
name=b"v0.0.1",
target_type=ObjectType.DIRECTORY,
target=get_directories()[0].id,
message=b"foo",
synthetic=False,
)
with_content = Release(
id=hash_to_bytes("7589dc4e14acd0e51ca3bcd6b80f4577d281fd34"),
name=b"v0.0.1",
target_type=ObjectType.CONTENT,
target=get_contents()[0].sha1_git,
message=b"foo",
synthetic=False,
)
return [with_revision, with_release, with_directory, with_content]
-GRAPHQL_EXTRA_TEST_OBJECTS = {"release": get_releases_with_target()}
+def get_revisions_with_parents():
+ """
+ Revisions with real revisions as parents
+ """
+ return [
+ Revision(
+ id=hash_to_bytes("37580d63b8dcc0ec73e74994e66896858542844c"),
+ message=b"hello",
+ date=swh_model_data.DATES[0],
+ committer=swh_model_data.COMMITTERS[0],
+ author=swh_model_data.COMMITTERS[0],
+ committer_date=swh_model_data.DATES[0],
+ type=RevisionType.GIT,
+ directory=b"\x01" * 20,
+ synthetic=False,
+ parents=(get_revisions()[0].id, get_revisions()[1].id),
+ )
+ ]
+
+
+GRAPHQL_EXTRA_TEST_OBJECTS = {
+ "release": get_releases_with_target(),
+ "revision": get_revisions_with_parents(),
+}
def populate_dummy_data(storage):
for object_type, objects in swh_model_data.TEST_OBJECTS.items():
method = getattr(storage, object_type + "_add")
method(objects)
for object_type, objects in GRAPHQL_EXTRA_TEST_OBJECTS.items():
method = getattr(storage, object_type + "_add")
method(objects)
diff --git a/swh/graphql/tests/functional/test_revision.py b/swh/graphql/tests/functional/test_revision.py
index a0e7042..6c20c0b 100644
--- a/swh/graphql/tests/functional/test_revision.py
+++ b/swh/graphql/tests/functional/test_revision.py
@@ -1,112 +1,159 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import pytest
from swh.model.swhids import CoreSWHID
from . import utils
from ..data import get_revisions
@pytest.mark.parametrize("revision", get_revisions())
def test_get_revision(client, revision):
query_str = """
{
revision(swhid: "%s") {
swhid
message {
text
}
author {
fullname {
text
}
name {
text
}
email {
text
}
}
committer {
fullname {
text
}
name {
text
}
email {
text
}
}
date
type
directory {
swhid
}
}
}
"""
data, _ = utils.get_query_response(client, query_str % revision.swhid())
assert data["revision"] == {
"swhid": str(revision.swhid()),
"message": {"text": revision.message.decode()},
"author": {
"fullname": {"text": revision.author.fullname.decode()},
"name": {"text": revision.author.name.decode()},
"email": {"text": revision.author.email.decode()},
},
"committer": {
"fullname": {"text": revision.committer.fullname.decode()},
"name": {"text": revision.committer.name.decode()},
"email": {"text": revision.committer.email.decode()},
},
"date": revision.date.to_datetime().isoformat(),
"type": revision.type.value,
"directory": {
"swhid": str(CoreSWHID(object_id=revision.directory, object_type="dir"))
},
}
def test_get_revision_with_invalid_swhid(client):
query_str = """
{
revision(swhid: "swh:1:cnt:invalid") {
swhid
}
}
"""
errors = utils.get_error_response(client, query_str)
# API will throw an error in case of an invalid SWHID
assert len(errors) == 1
assert "Invalid SWHID: invalid syntax" in errors[0]["message"]
def test_get_revision_as_target(client):
# SWHID of a snapshot with revision as target
snapshot_swhid = "swh:1:snp:9e78d7105c5e0f886487511e2a92377b4ee4c32a"
query_str = """
{
snapshot(swhid: "%s") {
branches(first: 1, types: [revision]) {
nodes {
type
target {
...on Revision {
swhid
}
}
}
}
}
}
"""
data, _ = utils.get_query_response(client, query_str % snapshot_swhid)
revision_obj = data["snapshot"]["branches"]["nodes"][0]["target"]
assert revision_obj == {
"swhid": "swh:1:rev:66c7c1cd9673275037140f2abff7b7b11fc9439c"
}
+
+
+def test_get_revision_log(client):
+ revision_swhid = "swh:1:rev:37580d63b8dcc0ec73e74994e66896858542844c"
+ query_str = """
+ {
+ revision(swhid: "%s") {
+ swhid
+ revisionLog(first: 3) {
+ nodes {
+ swhid
+ }
+ }
+ }
+ }
+ """
+ data, _ = utils.get_query_response(client, query_str % revision_swhid)
+ assert data["revision"]["revisionLog"] == {
+ "nodes": [
+ {"swhid": "swh:1:rev:37580d63b8dcc0ec73e74994e66896858542844c"},
+ {"swhid": "swh:1:rev:66c7c1cd9673275037140f2abff7b7b11fc9439c"},
+ {"swhid": "swh:1:rev:c7f96242d73c267adc77c2908e64e0c1cb6a4431"},
+ ]
+ }
+
+
+def test_get_revision_parents(client):
+ revision_swhid = "swh:1:rev:37580d63b8dcc0ec73e74994e66896858542844c"
+ query_str = """
+ {
+ revision(swhid: "%s") {
+ swhid
+ parents {
+ nodes {
+ swhid
+ }
+ }
+ }
+ }
+ """
+ data, _ = utils.get_query_response(client, query_str % revision_swhid)
+ assert data["revision"]["parents"] == {
+ "nodes": [
+ {"swhid": "swh:1:rev:66c7c1cd9673275037140f2abff7b7b11fc9439c"},
+ {"swhid": "swh:1:rev:c7f96242d73c267adc77c2908e64e0c1cb6a4431"},
+ ]
+ }

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 3, 10:39 AM (2 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3247983

Event Timeline