Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9311992
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Jul 3, 10:39 AM (2 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3247983
Attached To
rDGQL GraphQL API
Event Timeline
Log In to Comment