diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index fdda631..e8f5b07 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,139 +1,143 @@ # 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, List, Optional, Type, Union +from swh.graphql.backends.archive import Archive +from swh.graphql.backends.search import Search from swh.graphql.errors import PaginationError from swh.graphql.utils import utils from swh.storage.interface import PagedResult from .base_node import BaseNode @dataclass class PageInfo: hasNextPage: bool endCursor: Optional[str] @dataclass class ConnectionEdge: node: Any cursor: Optional[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: Optional[Any] = obj self.info = info self.kwargs = kwargs + self.archive = Archive() + self.search = Search() self._paged_data: PagedResult = paged_data @property def edges(self) -> List[ConnectionEdge]: """ 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) ] @property def nodes(self) -> List[Union[BaseNode, object]]: """ 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( 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) -> PageInfo: # 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) -> Optional[int]: # To support the schema naming convention """ Will be None for most of the connections override if needed/possible """ return None def get_paged_data(self) -> PagedResult: """ Cache to avoid multiple calls to the backend :meth:`_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_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) -> Optional[str]: """ 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 d1a965a..06384e1 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,81 +1,83 @@ # 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.backends.archive import Archive 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.archive = Archive() 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 _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 0e28e6b..e5335b2 100644 --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -1,95 +1,93 @@ # 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 BaseSnapshotBranchNode class BaseContentNode(BaseSWHNode): """ Base resolver for all the content nodes """ def _get_content_by_hash(self, checksums: dict): - content = archive.Archive().get_contents(checksums) + content = self.archive.get_contents(checksums) # in case of a conflict, return the first element return content[0] if content else None @property def checksum(self): # FIXME, use a Node instead return {k: v.hex() for (k, v) in self._node.hashes().items()} @property def id(self): return self._node.sha1_git @property def data(self): # FIXME, return a Node object # FIXME, add more ways to retrieve data like binary string archive_url = "https://archive.softwareheritage.org/api/1/" content_sha1 = self._node.hashes()["sha1"] return { "url": f"{archive_url}content/sha1:{content_sha1.hex()}/raw/", } @property def fileType(self): # FIXME, fetch data from the indexers return None @property def language(self): # FIXME, fetch data from the indexers return None @property def license(self): # FIXME, fetch data from the indexers return None 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): checksums = {"sha1_git": self.kwargs.get("swhid").object_id} return self._get_content_by_hash(checksums) class HashContentNode(BaseContentNode): """ Node resolver for a content requested with one or more checksums """ def _get_node_data(self): checksums = dict(self.kwargs.get("checksums")) return self._get_content_by_hash(checksums) class TargetContentNode(BaseContentNode): """ Node resolver for a content requested as a target This request could be from directory entry, release or a branch """ obj: Union[DirectoryEntryNode, BaseReleaseNode, BaseSnapshotBranchNode] def _get_node_data(self): return self._get_content_by_hash(checksums={"sha1_git": self.obj.target_hash}) diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py index fc15802..fc07520 100644 --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -1,75 +1,72 @@ # 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 swh.model.swhids import ObjectType from .base_node import BaseSWHNode from .release import BaseReleaseNode from .revision import BaseRevisionNode from .snapshot_branch import BaseSnapshotBranchNode 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): swhid = self.kwargs.get("swhid") # path = "" if ( swhid.object_type == ObjectType.DIRECTORY - and archive.Archive().is_object_available( - swhid.object_id, swhid.object_type - ) + and self.archive.is_object_available(swhid.object_id, swhid.object_type) ): # _get_directory_by_id is not making any backend call # hence the is_directory_available validation return self._get_directory_by_id(swhid.object_id) return None class RevisionDirectoryNode(BaseDirectoryNode): """ Node resolver for a directory requested from a revision """ obj: BaseRevisionNode def _get_node_data(self): # self.obj.directory_hash is the requested directory Id return self._get_directory_by_id(self.obj.directory_hash) class TargetDirectoryNode(BaseDirectoryNode): """ Node resolver for a directory requested as a target """ from .directory_entry import DirectoryEntryNode obj: Union[BaseSnapshotBranchNode, BaseReleaseNode, DirectoryEntryNode] def _get_node_data(self): 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 7d46b31..e1aea34 100644 --- a/swh/graphql/resolvers/directory_entry.py +++ b/swh/graphql/resolvers/directory_entry.py @@ -1,46 +1,43 @@ # 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.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseNode class DirectoryEntryNode(BaseNode): """ Node resolver for a directory entry """ @property def target_hash(self): # for DirectoryNode return self._node.target @property def targetType(self): # To support the schema naming convention return self._node.type class DirectoryEntryConnection(BaseConnection): """ Connection resolver for entries in a directory """ from .directory import BaseDirectoryNode obj: BaseDirectoryNode _node_class = DirectoryEntryNode def _get_paged_result(self) -> PagedResult: # 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 - ) + entries = self.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 bfbff96..5327da4 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,65 +1,64 @@ # 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, search from swh.model.model import Origin from swh.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseSWHNode from .search import SearchResultNode class BaseOriginNode(BaseSWHNode): 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 "Origin" class OriginNode(BaseOriginNode): """ Node resolver for an origin requested directly with its URL """ def _get_node_data(self): - return archive.Archive().get_origin(self.kwargs.get("url")) + return self.archive.get_origin(self.kwargs.get("url")) class TargetOriginNode(BaseOriginNode): """ Node resolver for an origin requested as a target """ obj: SearchResultNode def _get_node_data(self): # The target origin URL is guaranteed to exist in the archive # Hence returning the origin object without any explicit check in the archive # This assumes that the search index and archive are in sync return Origin(self.obj.target_url) class OriginConnection(BaseConnection): """ Connection resolver for the origins """ _node_class = BaseOriginNode def _get_paged_result(self) -> PagedResult: # Use the search backend if a urlPattern is given if self.kwargs.get("urlPattern"): - origins = search.Search().get_origins( + origins = self.search.get_origins( query=self.kwargs.get("urlPattern"), after=self._get_after_arg(), first=self._get_first_arg(), ) results = [Origin(ori["url"]) for ori in origins.results] return PagedResult(results=results, next_page_token=origins.next_page_token) # Use the archive backend by default - return archive.Archive().get_origins( + return self.archive.get_origins( after=self._get_after_arg(), first=self._get_first_arg() ) diff --git a/swh/graphql/resolvers/release.py b/swh/graphql/resolvers/release.py index 704972e..c766823 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,54 +1,52 @@ # 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 BaseSnapshotBranchNode 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] + return (self.archive.get_releases([release_id]) or None)[0] @property 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[BaseSnapshotBranchNode, BaseReleaseNode] def _get_node_data(self): # 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/revision.py b/swh/graphql/resolvers/revision.py index ab25cb3..ce8b0ab 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,110 +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.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] + return (self.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( + parents = self.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: - log = archive.Archive().get_revision_log([self.obj.swhid.object_id]) + log = self.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/resolvers/search.py b/swh/graphql/resolvers/search.py index cf706b5..29145ca 100644 --- a/swh/graphql/resolvers/search.py +++ b/swh/graphql/resolvers/search.py @@ -1,53 +1,52 @@ # 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, search from swh.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseNode class SearchResultNode(BaseNode): """ """ @property def targetType(self): # To support the schema naming convention return self._node.type class ResolveSwhidConnection(BaseConnection): _node_class = SearchResultNode def _get_paged_result(self) -> PagedResult: swhid = self.kwargs.get("swhid") results = [] - if archive.Archive().is_object_available(swhid.object_id, swhid.object_type): + if self.archive.is_object_available(swhid.object_id, swhid.object_type): results = [ { "target_hash": swhid.object_id, "type": swhid.object_type.name.lower(), } ] return PagedResult(results=results) class SearchConnection(BaseConnection): _node_class = SearchResultNode def _get_paged_result(self) -> PagedResult: - origins = search.Search().get_origins( + origins = self.search.get_origins( query=self.kwargs.get("query"), after=self._get_after_arg(), first=self._get_first_arg(), ) # FIXME hard coding type to origin for now, as it is the only searchable object results = [ {"target_url": ori["url"], "type": "origin"} for ori in origins.results ] return PagedResult(results=results, next_page_token=origins.next_page_token) diff --git a/swh/graphql/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py index f756815..d9c278e 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,97 +1,94 @@ # 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 swh.model.swhids import ObjectType from swh.storage.interface import PagedResult 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): """ """ swhid = self.kwargs.get("swhid") if ( swhid.object_type == ObjectType.SNAPSHOT - and archive.Archive().is_object_available( - swhid.object_id, swhid.object_type - ) + and self.archive.is_object_available(swhid.object_id, swhid.object_type) ): return self._get_snapshot_by_id(swhid.object_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 BaseSnapshotBranchNode obj: Union[BaseVisitStatusNode, BaseSnapshotBranchNode] def _get_node_data(self): 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) -> PagedResult: - results = archive.Archive().get_origin_snapshots(self.obj.url) + results = self.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 f751d2d..8ea0c45 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,125 +1,124 @@ # 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.errors import ObjectNotFoundError from swh.graphql.utils import utils from swh.storage.interface import PagedResult from .base_connection import BaseConnection from .base_node import BaseNode class BaseSnapshotBranchNode(BaseNode): # 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: tuple): # node_data is a tuple as returned by _get_paged_result in # SnapshotBranchConnection and _get_node_data in AliasSnapshotBranchNode # overriding to support this special data structure branch_name, branch_obj = node_data node = { "name": branch_name, "type": branch_obj.target_type.value, "target_hash": branch_obj.target, } return namedtuple("NodeObj", node.keys())(*node.values()) def is_type_of(self): return "Branch" @property def targetType(self): # To support the schema naming convention return self._node.type def snapshot_swhid(self): """ Logic to handle multiple branch alias redirects Alias redirects can be any level deep. Hence the parent snapshot can be reached only by a loop This code expects every BranchNode to have a Snapshot parent in the GraphQL query at some level. """ from .snapshot import BaseSnapshotNode parent = self.obj while parent: if isinstance( parent, BaseSnapshotNode ): # Reached the nearest SnapshotNode object return parent.swhid parent = parent.obj # Reached the root query node. This will never happen with the current entrypoints raise ObjectNotFoundError("There is no snapshot associated with the branch") class AliasSnapshotBranchNode(BaseSnapshotBranchNode): obj: BaseSnapshotBranchNode def _get_node_data(self): snapshot_swhid = self.snapshot_swhid() target_branch = self.obj.target_hash - alias_branch = archive.Archive().get_snapshot_branches( + alias_branch = self.archive.get_snapshot_branches( snapshot_swhid.object_id, first=1, name_include=target_branch ) if target_branch not in alias_branch["branches"]: raise ObjectNotFoundError( f"Branch name with {target_branch.decode()} is not available" ) # this will be serialized in _get_node_from_data method in the base class return (target_branch, alias_branch["branches"][target_branch]) class SnapshotBranchConnection(BaseConnection): """ Connection resolver for the branches in a snapshot """ from .snapshot import BaseSnapshotNode obj: BaseSnapshotNode _node_class = BaseSnapshotBranchNode def _get_paged_result(self) -> PagedResult: - result = archive.Archive().get_snapshot_branches( + result = self.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 # this will be serialized in _get_node_from_data method in the node class 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: BaseSnapshotBranchNode): # Snapshot branch is using a different cursor, hence the override return utils.get_encoded_cursor(node.name) diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py index 9d44d0a..19d24b3 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,67 +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 swh.storage.interface import PagedResult 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): """ 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( + return self.archive.get_origin_visit( self.kwargs.get("originUrl"), int(self.kwargs.get("visitId")) ) class LatestVisitNode(BaseVisitNode): """ 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) + return self.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) -> PagedResult: # self.obj.url is the origin URL - return archive.Archive().get_origin_visits( + return self.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 268b018..142b14d 100644 --- a/swh/graphql/resolvers/visit_status.py +++ b/swh/graphql/resolvers/visit_status.py @@ -1,54 +1,51 @@ # 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 swh.storage.interface import PagedResult 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): """ 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 - ) + return self.archive.get_latest_visit_status(self.obj.origin, self.obj.visitId) class VisitStatusConnection(BaseConnection): """ Connection resolver for the visit-status objects in a visit """ obj: BaseVisitNode _node_class = BaseVisitStatusNode def _get_paged_result(self) -> PagedResult: # self.obj.origin is the origin URL - return archive.Archive().get_visit_status( + return self.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/unit/backends/test_archive.py b/swh/graphql/tests/unit/backends/test_archive.py index 122186d..647884c 100644 --- a/swh/graphql/tests/unit/backends/test_archive.py +++ b/swh/graphql/tests/unit/backends/test_archive.py @@ -1,4 +1,4 @@ # from swh.graphql.backends import archive # def test_get_origin(): -# assert isinstance(archive.Archive().get_origins(), object) +# assert isinstance(self.archive.get_origins(), object)