diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index 2c16e8e..fee50f1 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,145 +1,144 @@ # 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 import binascii from dataclasses import dataclass from typing import Any, Optional, Type from swh.graphql.errors import PaginationError from swh.graphql.utils import utils from .base_node import BaseNode @dataclass class PageInfo: hasNextPage: bool endCursor: str @dataclass class ConnectionEdge: node: Any cursor: str class BaseConnection(ABC): """ Base resolver for all the connections """ _node_class: Optional[Type[BaseNode]] = None _page_size: int = 50 # default page size (default value for the first arg) _max_page_size: int = 1000 # maximum page size(max value for the first arg) 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, self.info, node_data=result, **self.kwargs) + self._node_class( + obj=self, info=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): """ Return the list of connection edges, each with a cursor """ return [ ConnectionEdge(node=node, cursor=self._get_index_cursor(index, node)) for (index, node) in enumerate(self.nodes) ] def _get_after_arg(self) -> str: """ Return the decoded next page token. Override to support a different cursor type """ # different implementation is used in SnapshotBranchConnection try: cursor = utils.get_decoded_cursor(self.kwargs.get("after")) except (UnicodeDecodeError, binascii.Error) as e: raise PaginationError("Invalid value for argument 'after'", errors=e) return cursor def _get_first_arg(self) -> int: """ """ # page_size is set to 50 by default # Input type check is not required; It is defined in schema as an int first = self.kwargs.get("first", self._page_size) if first < 0 or first > self._max_page_size: raise PaginationError( f"Value for argument 'first' is invalid; it must be between 0 and {self._max_page_size}" # noqa: B950 ) return first def _get_index_cursor(self, index: int, node: Any): """ Get the cursor to the given item index """ # default implementation which works with swh-storage pagaination # override this function to support other types (eg: SnapshotBranchConnection) offset_index = self._get_after_arg() or "0" index_cursor = int(offset_index) + index return utils.get_encoded_cursor(str(index_cursor)) diff --git a/swh/graphql/resolvers/base_node.py b/swh/graphql/resolvers/base_node.py index 90c85b2..d1a965a 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,85 +1,81 @@ # 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 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 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 + 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 + 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 964508b..12a86af 100644 --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -1,59 +1,59 @@ # 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 from .snapshot_branch import SnapshotBranchNode 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): return self._get_content_by_id(self.kwargs.get("swhid").object_id) class TargetContentNode(BaseContentNode): """ Node resolver for a content requested from a directory entry or from a release target """ obj: Union[DirectoryEntryNode, BaseReleaseNode, SnapshotBranchNode] def _get_node_data(self): - content_id = self.obj.targetHash + content_id = self.obj.target_hash return self._get_content_by_id(content_id) diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py index d6bc32a..5cbf024 100644 --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -1,70 +1,70 @@ # 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.model.model import Directory from .base_node import BaseSWHNode from .release import BaseReleaseNode from .revision import BaseRevisionNode from .snapshot_branch import SnapshotBranchNode 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): 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): - # self.obj.directorySWHID is the requested directory SWHID - directory_id = self.obj.directorySWHID.object_id + # self.obj.directory_swhid is the requested directory SWHID + directory_id = self.obj.directory_swhid.object_id return self._get_directory_by_id(directory_id) class TargetDirectoryNode(BaseDirectoryNode): """ Node resolver for a directory requested as a target """ from .directory_entry import DirectoryEntryNode obj: Union[SnapshotBranchNode, BaseReleaseNode, DirectoryEntryNode] def _get_node_data(self): - return self._get_directory_by_id(self.obj.targetHash) + return self._get_directory_by_id(self.obj.target_hash) diff --git a/swh/graphql/resolvers/directory_entry.py b/swh/graphql/resolvers/directory_entry.py index 5593fbd..c99da81 100644 --- a/swh/graphql/resolvers/directory_entry.py +++ b/swh/graphql/resolvers/directory_entry.py @@ -1,41 +1,41 @@ # 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 class DirectoryEntryNode(BaseNode): """ Node resolver for a directory entry """ @property - def targetHash(self): # To support the schema naming convention + def target_hash(self): # for DirectoryNode return self._node.target class DirectoryEntryConnection(BaseConnection): """ Connection resolver for entries in a directory """ from .directory import BaseDirectoryNode obj: BaseDirectoryNode _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/release.py b/swh/graphql/resolvers/release.py index 960391c..e9d875b 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,54 +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 + def target_hash(self): 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 return "Release" class ReleaseNode(BaseReleaseNode): """ 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): """ 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) + # self.obj.target_hash is the requested release id + return self._get_release_by_id(self.obj.target_hash) diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index c013ae5..4bd38f4 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,259 +1,262 @@ # 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")) +# - 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") 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() + 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)() +# 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)() + 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 edde914..1ccfb42 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,107 +1,107 @@ # 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 + 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 directorySWHID(self): # To support the schema naming convention + def directory_swhid(self): # for RevisionDirectoryNode resolver 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 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[SnapshotBranchNode, BaseReleaseNode] def _get_node_data(self): - # self.obj.targetHash is the requested revision id - return self._get_revision_by_id(self.obj.targetHash) + # 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): # self.obj is the current(child) revision - # self.obj.parentSWHIDs is the list of parent SWHIDs + # 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.parentSWHIDs] + [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): # 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 c39ac34..cda3305 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,90 +1,90 @@ # 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 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={}) 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 "Snapshot" class SnapshotNode(BaseSnapshotNode): """ 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): """ Node resolver for a snapshot requested from a visit-status """ obj: BaseVisitStatusNode def _get_node_data(self): # self.obj.snapshotSWHID is the requested snapshot SWHID snapshot_id = self.obj.snapshotSWHID.object_id return self._get_snapshot_by_id(snapshot_id) class TargetSnapshotNode(BaseSnapshotNode): """ Node resolver for a snapshot requested as a target """ from .snapshot_branch import SnapshotBranchNode obj: Union[BaseVisitStatusNode, SnapshotBranchNode] def _get_node_data(self): - snapshot_id = self.obj.targetHash + snapshot_id = self.obj.target_hash 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 349150a..f1b9a79 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,84 +1,84 @@ # 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 class SnapshotBranchNode(BaseNode): """ 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 # 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 + def target_hash(self): return self._node.target class SnapshotBranchConnection(BaseConnection): """ Connection resolver for the branches in a snapshot """ from .snapshot import SnapshotNode obj: SnapshotNode _node_class = SnapshotBranchNode def _get_paged_result(self): 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._get_name_include_arg(), ) # endCursor is the last branch name, logic for that end_cusrsor = ( result["next_branch"] 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): # after argument must be an empty string by default after = super()._get_after_arg() return after.encode() if after else b"" def _get_name_include_arg(self): name_include = self.kwargs.get("nameInclude", None) return name_include.encode() if name_include else None def _get_index_cursor(self, index: int, node: SnapshotBranchNode): # Snapshot branch is using a different cursor, hence the override return utils.get_encoded_cursor(node.name)