diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index f5f56b3..3adbb5f 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,112 +1,112 @@ # 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 abc import ABC, abstractmethod from dataclasses import dataclass from typing import Optional, Type from swh.graphql.utils import utils from .base_node import BaseNode @dataclass class PageInfo: hasNextPage: bool endCursor: str class BaseConnection(ABC): """ - Base class for all the connection resolvers + Base resolver for all the connections """ _node_class: Optional[Type[BaseNode]] = None _page_size = 50 # default page size def __init__(self, obj, info, paged_data=None, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._paged_data = paged_data def __call__(self, *args, **kw): return self @property def edges(self): return self._get_edges() @property def nodes(self): """ Override if needed; return a list of objects If a node class is set, return a list of its (Node) instances else a list of raw results """ if self._node_class is not None: return [ self._node_class(self.obj, self.info, node_data=result, **self.kwargs) for result in self.get_paged_data().results ] return self.get_paged_data().results @property def pageInfo(self): # To support the schema naming convention # FIXME, add more details like startCursor return PageInfo( hasNextPage=bool(self.get_paged_data().next_page_token), endCursor=utils.get_encoded_cursor(self.get_paged_data().next_page_token), ) @property def totalCount(self): # To support the schema naming convention return self._get_total_count() def _get_total_count(self): """ Will be None for most of the connections override if needed/possible """ return None def get_paged_data(self): """ Cache to avoid multiple calls to the backend (_get_paged_result) return a PagedResult object """ if self._paged_data is None: # FIXME, make this call async (not for v1) self._paged_data = self._get_paged_result() return self._paged_data @abstractmethod def _get_paged_result(self): """ Override for desired behaviour return a PagedResult object """ # FIXME, make this call async (not for v1) return None def _get_edges(self): # FIXME, make cursor work per item # Cursor can't be None here return [{"cursor": "dummy", "node": node} for node in self.nodes] def _get_after_arg(self): """ Return the decoded next page token override to use a specific token """ return utils.get_decoded_cursor(self.kwargs.get("after")) def _get_first_arg(self): """ page_size is set to 50 by default """ return self.kwargs.get("first", self._page_size) diff --git a/swh/graphql/resolvers/base_node.py b/swh/graphql/resolvers/base_node.py index 2f20a6a..90c85b2 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,81 +1,85 @@ # 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 abc import ABC from collections import namedtuple from swh.graphql.errors import ObjectNotFoundError class BaseNode(ABC): """ - Base class for all the Node resolvers + Base resolver for all the nodes """ def __init__(self, obj, info, node_data=None, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._node = self._get_node(node_data) # handle the errors, if any, after _node is set self._handle_node_errors() def _get_node(self, node_data): """ Get the node object from the given data if the data (node_data) is none make a function call to get data from backend """ if node_data is None: node_data = self._get_node_data() return self._get_node_from_data(node_data) def _get_node_from_data(self, node_data): """ Get the object from node_data In case of a dict, convert it to an object Override to support different data structures """ if type(node_data) is dict: return namedtuple("NodeObj", node_data.keys())(*node_data.values()) return node_data def _handle_node_errors(self): """ Handle any error related to node data raise an error in case the object returned is None override for specific behaviour """ if self._node is None: raise ObjectNotFoundError("Requested object is not available") def __call__(self, *args, **kw): return self def _get_node_data(self): """ Override for desired behaviour This will be called only when node_data is None """ # FIXME, make this call async (not for v1) return None def __getattr__(self, name): """ Any property defined in the sub-class will get precedence over the _node attributes """ return getattr(self._node, name) def is_type_of(self): return self.__class__.__name__ class BaseSWHNode(BaseNode): + """ + Base resolver for all the nodes with a SWHID field + """ + @property def swhid(self): return self._node.swhid() diff --git a/swh/graphql/resolvers/content.py b/swh/graphql/resolvers/content.py index bf7a8dd..044464e 100644 --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -1,49 +1,58 @@ # 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 .base_node import BaseSWHNode +from .directory_entry import DirectoryEntryNode +from .release import BaseReleaseNode class BaseContentNode(BaseSWHNode): - """ """ + """ + Base resolver for all the content nodes + """ def _get_content_by_id(self, content_id): content = archive.Archive().get_content(content_id) return content[0] if content else None @property def checksum(self): # FIXME, return a Node object return {k: v.hex() for (k, v) in self._node.hashes().items()} @property def id(self): return self._node.sha1_git 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 "Content" class ContentNode(BaseContentNode): + """ + Node resolver for a content requested directly with its SWHID + """ + def _get_node_data(self): - """ - When a content is requested directly - with its SWHID - """ return self._get_content_by_id(self.kwargs.get("swhid").object_id) class TargetContentNode(BaseContentNode): - def _get_node_data(self): - """ - When a content is requested from a - directory entry or from a release target + """ + Node resolver for a content requested from a + directory entry or from a release target + """ - content id is obj.targetHash here - """ + obj: Union[DirectoryEntryNode, BaseReleaseNode] + + def _get_node_data(self): content_id = self.obj.targetHash return self._get_content_by_id(content_id) diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py index a99d0c7..15d41ad 100644 --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -1,55 +1,62 @@ # 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.graphql.backends import archive from swh.model.model import Directory from .base_node import BaseSWHNode +from .revision import BaseRevisionNode class BaseDirectoryNode(BaseSWHNode): + """ + Base resolver for all the directory nodes + """ + def _get_directory_by_id(self, directory_id): # Return a Directory model object # entries is initialized as empty # Same pattern is used in snapshot return Directory(id=directory_id, entries=()) def is_type_of(self): return "Directory" class DirectoryNode(BaseDirectoryNode): + """ + Node resolver for a directory requested directly with its SWHID + """ + def _get_node_data(self): - """ - When a directory is requested directly with its SWHID - """ directory_id = self.kwargs.get("swhid").object_id # path = "" if archive.Archive().is_directory_available([directory_id]): + # _get_directory_by_id is not making any backend call + # hence the is_directory_available validation return self._get_directory_by_id(directory_id) return None class RevisionDirectoryNode(BaseDirectoryNode): + """ + Node resolver for a directory requested from a revision + """ + + obj: BaseRevisionNode + def _get_node_data(self): - """ - When a directory is requested from a revision - self.obj is revision here - self.obj.directorySWHID is the required directory SWHID - (set from resolvers.revision.py:BaseRevisionNode) - """ + # self.obj.directorySWHID is the requested directory SWHID directory_id = self.obj.directorySWHID.object_id return self._get_directory_by_id(directory_id) class TargetDirectoryNode(BaseDirectoryNode): - def _get_node_data(self): - """ - When a directory is requested as a target - self.obj can be a Release or a DirectoryEntry + """ + Node resolver for a directory requested as a target + """ - obj.targetHash is the requested directory id here - """ + def _get_node_data(self): return self._get_directory_by_id(self.obj.targetHash) diff --git a/swh/graphql/resolvers/directory_entry.py b/swh/graphql/resolvers/directory_entry.py index e3b1d4f..c87c2de 100644 --- a/swh/graphql/resolvers/directory_entry.py +++ b/swh/graphql/resolvers/directory_entry.py @@ -1,39 +1,40 @@ # 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.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode +from .directory import BaseDirectoryNode class DirectoryEntryNode(BaseNode): - """ """ + """ + Node resolver for a directory entry + """ @property def targetHash(self): # To support the schema naming convention return self._node.target class DirectoryEntryConnection(BaseConnection): - _node_class = DirectoryEntryNode + """ + Connection resolver for entries in a directory + """ - def _get_paged_result(self): - """ - When entries requested from a directory - self.obj.swhid is the directory SWHID here + obj: BaseDirectoryNode - This is not paginated from swh-storgae - using dummy pagination - """ + _node_class = DirectoryEntryNode + def _get_paged_result(self): # FIXME, using dummy(local) pagination, move pagination to backend # To remove localpagination, just drop the paginated call # STORAGE-TODO entries = ( archive.Archive().get_directory_entries(self.obj.swhid.object_id).results ) return utils.paginated(entries, self._get_first_arg(), self._get_after_arg()) diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py index e62ae35..ee67db1 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,25 +1,33 @@ # 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.graphql.backends import archive from .base_connection import BaseConnection from .base_node import BaseSWHNode class OriginNode(BaseSWHNode): + """ + Node resolver for an origin requested directly with its URL + """ + def _get_node_data(self): return archive.Archive().get_origin(self.kwargs.get("url")) class OriginConnection(BaseConnection): + """ + Connection resolver for the origins + """ + _node_class = OriginNode def _get_paged_result(self): return archive.Archive().get_origins( after=self._get_after_arg(), first=self._get_first_arg(), url_pattern=self.kwargs.get("urlPattern"), ) diff --git a/swh/graphql/resolvers/release.py b/swh/graphql/resolvers/release.py index e7b4973..960391c 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,50 +1,54 @@ # 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 .base_node import BaseSWHNode +from .snapshot_branch import SnapshotBranchNode class BaseReleaseNode(BaseSWHNode): + """ + Base resolver for all the release nodes + """ + def _get_release_by_id(self, release_id): return (archive.Archive().get_releases([release_id]) or None)[0] @property def targetHash(self): # To support the schema naming convention return self._node.target @property def targetType(self): # To support the schema naming convention return self._node.target_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 - """ + # is_type_of is required only when resolving a UNION type + # This is for ariadne to return the right type return "Release" class ReleaseNode(BaseReleaseNode): """ - When the release is requested directly with its SWHID + Node resolver for a release requested directly with its SWHID """ def _get_node_data(self): return self._get_release_by_id(self.kwargs.get("swhid").object_id) class TargetReleaseNode(BaseReleaseNode): """ - When a release is requested as a target - - self.obj could be a snapshotbranch or a release - self.obj.targetHash is the requested release id here + Node resolver for a release requested as a target """ + obj: Union[SnapshotBranchNode, BaseReleaseNode] + def _get_node_data(self): + # self.obj.targetHash is the requested release id return self._get_release_by_id(self.obj.targetHash) diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index e52722f..c013ae5 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,257 +1,259 @@ # 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 an annotation (eg: @visitstatus.field("snapshot")) -- As a property in the Node object (eg: resolvers.visit.OriginVisitNode.id) -- As an attribute/item in the object/dict returned by the backend (eg: Origin.url) """ + +# 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")) +# - As a property in the Node object (eg: resolvers.visit.BaseVisitNode.id) +# - 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") binary_string: ObjectType = ObjectType("BinaryString") branch_target: UnionType = UnionType("BranchTarget") release_target: UnionType = UnionType("ReleaseTarget") directory_entry_target: UnionType = UnionType("DirectoryEntryTarget") # 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.SnapshotBranchNode, info: GraphQLResolveInfo, **kw ): """ Snapshot branch target can be a revision or a release """ 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)() # 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)() @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)() # Any other type of resolver @release_target.type_resolver @directory_entry_target.type_resolver @branch_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 43cd922..a5e13dd 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,102 +1,109 @@ # 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.swhids import CoreSWHID, ObjectType from .base_connection import BaseConnection from .base_node import BaseSWHNode +from .release import BaseReleaseNode +from .snapshot_branch import SnapshotBranchNode 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 parentSWHIDs(self): # To support the schema naming convention return [ CoreSWHID(object_type=ObjectType.REVISION, object_id=parent_id) for parent_id in self._node.parents ] @property def directorySWHID(self): # To support the schema naming convention - """ """ return CoreSWHID( object_type=ObjectType.DIRECTORY, object_id=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 - """ + # 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): """ - When the revision is requested directly with its SWHID + 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): """ - When a revision is requested as a target - - self.obj could be a snapshotbranch or a release - self.obj.targetHash is the requested revision id here + Node resolver for a revision requested as a target """ + obj: Union[SnapshotBranchNode, BaseReleaseNode] + def _get_node_data(self): + # self.obj.targetHash is the requested revision id return self._get_revision_by_id(self.obj.targetHash) class ParentRevisionConnection(BaseConnection): """ - When parent revisions is requested from a - revision - self.obj is the current(child) revision - self.obj.parentSWHIDs is the list of - parent SWHIDs + Connection resolver for parent revisions in a revision """ + obj: BaseRevisionNode + _node_class = BaseRevisionNode def _get_paged_result(self): + # self.obj is the current(child) revision + # self.obj.parentSWHIDs 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.parentSWHIDs] ) return utils.paginated(parents, self._get_first_arg(), self._get_after_arg()) class LogRevisionConnection(BaseConnection): """ - When revisionslog is requested from a - revision - self.obj is the current revision id + Connection resolver for the log (list of revisions) in a revision """ + obj: BaseRevisionNode + _node_class = BaseRevisionNode def _get_paged_result(self): + # self.obj is the current revision id + # STORAGE-TODO (date in revisionlog is a dict) log = archive.Archive().get_revision_log([self.obj.swhid.object_id]) # 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/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py index a9da294..1fe4d66 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,59 +1,69 @@ # 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.graphql.backends import archive from swh.graphql.utils import utils from swh.model.model import Snapshot from .base_connection import BaseConnection from .base_node import BaseSWHNode +from .origin import OriginNode +from .visit_status import BaseVisitStatusNode class BaseSnapshotNode(BaseSWHNode): + """ + Base resolver for all the snapshot nodes + """ + def _get_snapshot_by_id(self, snapshot_id): # Return a Snapshot model object # branches is initialized as empty # Same pattern is used in directory return Snapshot(id=snapshot_id, branches={}) class SnapshotNode(BaseSnapshotNode): """ - For directly accessing a snapshot with its SWHID + Node resolver for a snapshot requested directly with its SWHID """ def _get_node_data(self): """ """ snapshot_id = self.kwargs.get("swhid").object_id if archive.Archive().is_snapshot_available([snapshot_id]): return self._get_snapshot_by_id(snapshot_id) return None class VisitSnapshotNode(BaseSnapshotNode): """ - For accessing a snapshot from a visitstatus type + Node resolver for a snapshot requested from a visit-status """ + obj: BaseVisitStatusNode + def _get_node_data(self): - """ - self.obj is visitstatus here - self.obj.snapshotSWHID is the requested snapshot SWHID - """ + # self.obj.snapshotSWHID is the requested snapshot SWHID snapshot_id = self.obj.snapshotSWHID.object_id return self._get_snapshot_by_id(snapshot_id) class OriginSnapshotConnection(BaseConnection): + """ + Connection resolver for the snapshots in an origin + """ + + obj: OriginNode + _node_class = BaseSnapshotNode def _get_paged_result(self): - """ """ results = archive.Archive().get_origin_snapshots(self.obj.url) snapshots = [Snapshot(id=snapshot, branches={}) for snapshot in results] # FIXME, using dummy(local) pagination, move pagination to backend # To remove localpagination, just drop the paginated call # STORAGE-TODO return utils.paginated(snapshots, self._get_first_arg(), self._get_after_arg()) diff --git a/swh/graphql/resolvers/snapshot_branch.py b/swh/graphql/resolvers/snapshot_branch.py index c5690d8..2ac39cb 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,78 +1,79 @@ # 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 collections import namedtuple from swh.graphql.backends import archive from swh.graphql.utils import utils from swh.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseNode +from .snapshot import SnapshotNode class SnapshotBranchNode(BaseNode): """ - target field for this Node is a UNION in the schema - It is resolved in resolvers.resolvers.py + Node resolver for a snapshot branch """ + # target field for this Node is a UNION type + # It is resolved in the top level (resolvers.resolvers.py) + def _get_node_from_data(self, node_data): - """ - node_data is not a dict in this case - overriding to support this special data structure - """ + # node_data is not a dict in this case + # overriding to support this special data structure # STORAGE-TODO; return an object in the normal format branch_name, branch_obj = node_data node = { "name": branch_name, "type": branch_obj.target_type.value, "target": branch_obj.target, } return namedtuple("NodeObj", node.keys())(*node.values()) @property def targetHash(self): # To support the schema naming convention return self._node.target class SnapshotBranchConnection(BaseConnection): + """ + Connection resolver for the branches in a snapshot + """ + + obj: SnapshotNode + _node_class = SnapshotBranchNode def _get_paged_result(self): - """ - When branches requested from a snapshot - self.obj.swhid is the snapshot SWHID here - (as returned from resolvers/snapshot.py) - """ - + # self.obj.swhid is the snapshot SWHID result = archive.Archive().get_snapshot_branches( self.obj.swhid.object_id, after=self._get_after_arg(), first=self._get_first_arg(), target_types=self.kwargs.get("types"), name_include=self.kwargs.get("nameInclude"), ) # FIXME Cursor must be a hex to be consistent with # the base class, hack to make that work end_cusrsor = ( result["next_branch"].hex() if result["next_branch"] is not None else None ) # FIXME, this pagination is not consistent with other connections # FIX in swh-storage to return PagedResult # STORAGE-TODO return PagedResult( results=result["branches"].items(), next_page_token=end_cusrsor ) def _get_after_arg(self): - """ - Snapshot branch is using a different cursor; logic to handle that - """ + # Snapshot branch is using a different cursor; logic to handle that + # FIXME Cursor must be a hex to be consistent with # the base class, hack to make that work after = utils.get_decoded_cursor(self.kwargs.get("after", "")) return bytes.fromhex(after) diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py index 6979850..e0cb7fb 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,56 +1,66 @@ # 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.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode +from .origin import OriginNode class BaseVisitNode(BaseNode): + """ + Base resolver for all the visit nodes + """ + @property def id(self): # FIXME, use a better id return utils.get_b64_string(f"{self.origin}-{str(self.visit)}") @property def visitId(self): # To support the schema naming convention return self._node.visit class OriginVisitNode(BaseVisitNode): """ - Get the visit directly with an origin URL and a visit ID + Node resolver for a visit requested directly with an origin URL + and a visit ID """ def _get_node_data(self): return archive.Archive().get_origin_visit( self.kwargs.get("originUrl"), int(self.kwargs.get("visitId")) ) class LatestVisitNode(BaseVisitNode): """ - Get the latest visit for an origin - self.obj is the origin object here - self.obj.url is the origin URL + Node resolver for the latest visit in an origin """ + obj: OriginNode + def _get_node_data(self): + # self.obj.url is the origin URL return archive.Archive().get_origin_latest_visit(self.obj.url) class OriginVisitConnection(BaseConnection): + """ + Connection resolver for the visit objects in an origin + """ + + obj: OriginNode + _node_class = BaseVisitNode def _get_paged_result(self): - """ - Get the visits for the given origin - parent obj (self.obj) is origin here - """ + # self.obj.url is the origin URL return archive.Archive().get_origin_visits( self.obj.url, after=self._get_after_arg(), first=self._get_first_arg() ) diff --git a/swh/graphql/resolvers/visit_status.py b/swh/graphql/resolvers/visit_status.py index 7c9b33d..89c95e1 100644 --- a/swh/graphql/resolvers/visit_status.py +++ b/swh/graphql/resolvers/visit_status.py @@ -1,48 +1,53 @@ # 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.graphql.backends import archive from swh.model.swhids import CoreSWHID, ObjectType from .base_connection import BaseConnection from .base_node import BaseNode +from .visit import BaseVisitNode class BaseVisitStatusNode(BaseNode): - """ """ + """ + Base resolver for all the visit-status nodes + """ @property def snapshotSWHID(self): # To support the schema naming convention return CoreSWHID(object_type=ObjectType.SNAPSHOT, object_id=self._node.snapshot) class LatestVisitStatusNode(BaseVisitStatusNode): """ - Get the latest visit status for a visit - self.obj is the visit object here - self.obj.origin is the origin URL + Node resolver for a visit-status requested from a visit """ + obj: BaseVisitNode + def _get_node_data(self): + # self.obj.origin is the origin URL return archive.Archive().get_latest_visit_status( self.obj.origin, self.obj.visitId ) class VisitStatusConnection(BaseConnection): """ - self.obj is the visit object - self.obj.origin is the origin URL + Connection resolver for the visit-status objects in a visit """ + obj: BaseVisitNode _node_class = BaseVisitStatusNode def _get_paged_result(self): + # self.obj.origin is the origin URL return archive.Archive().get_visit_status( self.obj.origin, self.obj.visitId, after=self._get_after_arg(), first=self._get_first_arg(), ) diff --git a/swh/graphql/tests/data.py b/swh/graphql/tests/data.py index 8c4db7c..95e2603 100644 --- a/swh/graphql/tests/data.py +++ b/swh/graphql/tests/data.py @@ -1,24 +1,20 @@ # 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 -# This module will be removed once the test data -# generation in SWH-wb moved to a shared location -# or to a new test data project - from swh.model.tests import swh_model_data def populate_dummy_data(storage): for object_type, objects in swh_model_data.TEST_OBJECTS.items(): method = getattr(storage, object_type + "_add") method(objects) def get_origins(): return swh_model_data.ORIGINS def get_snapshots(): return swh_model_data.SNAPSHOTS