diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index d70b833..f64fb25 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,114 +1,119 @@ """ """ from abc import ABC, abstractmethod from typing import Any from swh.graphql.utils import utils # from dataclasses import dataclass # @dataclass # class PageInfo: # nex_page_token: str # class Arguments: # """ # dataclass # """ # after # Elements that come after the specified cursor # first # Returns the first n elements class BaseConnection(ABC): _node_class: Any = None + _page_size = 50 # deafult page size def __init__(self, obj, info, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs - self._page_data = None + self._paged_data = None self.pageInfo = self.page_info self.totalCount = self.total_count 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 intance + return a list of its (Node) intances 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.page_data.results + for result in self.get_paged_data().results ] - return self.page_data.results + return self.get_paged_data().results @property def page_info(self): # FIXME Replace with a dataclass # return PageInfo(self.page_data.next_page_token) + # FIXME, add more details like startCursor return { - "hasNextPage": bool(self.page_data.next_page_token), - "endCursor": utils.get_encoded_cursor(self.page_data.next_page_token), + "hasNextPage": bool(self.get_paged_data().next_page_token), + "endCursor": utils.get_encoded_cursor( + self.get_paged_data().next_page_token + ), } @property def total_count(self): """ Will be None for most of the connections - override if needed + override if needed/possible """ return None - @property - def page_data(self): + def get_paged_data(self): """ Cache to avoid multiple calls to - the backend + the backend (_get_paged_result) + return a PagedResult object """ - if self._page_data is None: + if self._paged_data is None: # FIXME, make this call async (not for v1) - self._page_data = self._get_page_result() - return self._page_data + self._paged_data = self._get_paged_result() + return self._paged_data @abstractmethod - def _get_page_result(self): + 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 - return [{"cursor": "test", "node": node} for node in self.nodes] + # 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): """ - Override to set the default page size + page_size is set to 50 by default """ - return self.kwargs.get("first", 50) + 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 003e934..43e8ed4 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,47 +1,46 @@ """ """ from abc import ABC, abstractmethod from collections import namedtuple class BaseNode(ABC): - def __init__(self, obj, info, **kwargs): + def __init__(self, obj, info, node_data=None, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs - node_data = kwargs.get("node_data") self._set_node(node_data) def _set_node(self, node_data): if node_data is None: node_data = self._get_node_data() self._node = self._get_node_from_data(node_data) def _get_node_from_data(self, node_data): if type(node_data) is dict: return namedtuple("NodeObj", node_data.keys())(*node_data.values()) return node_data def __call__(self, *args, **kw): """ """ return self @abstractmethod def _get_node_data(self): """ Override for desired behaviour """ # 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__ diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py index 9601425..58950f2 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,24 +1,24 @@ from swh.graphql.backends import archive from .base_connection import BaseConnection from .base_node import BaseNode class OriginNode(BaseNode): def _get_node_data(self): # FIXME, make this call async (not for v1) return archive.Archive().get_origin(self.kwargs.get("url")) # @property # def url(self): # return "test" class OriginConnection(BaseConnection): _node_class = OriginNode - def _get_page_result(self): + def _get_paged_result(self): # FIXME, make this call async (not for v1) return archive.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 935e710..5de5dcb 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,36 +1,35 @@ from swh.graphql.backends import archive from .base_node import BaseNode class ReleaseNode(BaseNode): def _get_node_data(self): """ """ class BranchReleaseNode(BaseNode): """ When the release is requested from a snapshot branch self.obj is a branch object self.obj.target is the release id """ def _get_node_data(self): - k = (archive.Archive().get_release(self.obj.target) or None)[0] - return k + return (archive.Archive().get_release(self.obj.target) or None)[0] def is_type_of(self): """ is_type_of is required only when requesting from a connection This is for ariadne to return the correct type in schema """ return "Release" @property def author(self): # return a PersoneNode object return self.obj.author diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index e0c8f05..ac3cc7f 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,102 +1,102 @@ """ 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 model object (eg: models.visit.VisitModel.id) - As an attribute/item in the object/dict returned by the backend (eg: Origin.url) """ from ariadne import ObjectType, UnionType from .resolver_factory import get_connection_resolver, get_node_resolver query = ObjectType("Query") origin = ObjectType("Origin") origins = ObjectType("OriginConnection") visit = ObjectType("Visit") visitstatus = ObjectType("VisitStatus") snapshot = ObjectType("Snapshot") branch = ObjectType("Branch") target = UnionType("BranchTarget") # Node resolvers # A node resolver can return a model object or a data structure @query.field("origin") def origin_resolver(obj, info, **kw): """ """ resolver = get_node_resolver("origin") return resolver(obj, info, **kw)() @query.field("visit") def visit_resolver(obj, info, **kw): """ """ resolver = get_node_resolver("visit") return resolver(obj, info, **kw)() @query.field("snapshot") def snapshot_resolver(obj, info, **kw): """ """ resolver = get_node_resolver("snapshot") return resolver(obj, info, **kw)() @visitstatus.field("snapshot") def visit_snapshot(obj, info, **kw): resolver = get_node_resolver("visit-snapshot") return resolver(obj, info, **kw)() @branch.field("target") def branch_target(obj, info, **kw): """ Branch target can be a revision or a release """ - resolver_type = "branch-" + obj.type + resolver_type = f"branch-{obj.type}" resolver = get_node_resolver(resolver_type) return resolver(obj, info, **kw)() # Connection resolvers # A connection resolver will return a sub class of BaseConnection @query.field("origins") def origins_resolver(obj, info, **kw): resolver = get_connection_resolver("origins") return resolver(obj, info, **kw)() @origin.field("visits") def visits_resolver(obj, info, **kw): resolver = get_connection_resolver("origin-visits") return resolver(obj, info, **kw)() @visit.field("status") def visitstatus_resolver(obj, info, **kw): resolver = get_connection_resolver("visit-status") return resolver(obj, info, **kw)() @snapshot.field("branches") def snapshot_branches(obj, info, **kw): resolver = get_connection_resolver("snapshot-branches") return resolver(obj, info, **kw)() # Any other type of resolver @target.type_resolver def union_resolver(obj, *_): """ Generic resolver for all the union types """ return obj.is_type_of() diff --git a/swh/graphql/resolvers/snapshot_branch.py b/swh/graphql/resolvers/snapshot_branch.py index cc45af6..d104145 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,55 +1,63 @@ # 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): """ """ def _get_node_data(self): - pass + """ + This Node is instantiated only from a + connection (SnapshotBranchConnection). + Since node_data is always available, + there is no reason to make a storage + query. Hence this function will never + be called. This stub is to make the + abstract base class work. + """ class SnapshotBranchConnection(BaseConnection): _node_class = SnapshotBranchNode - def _get_page_result(self): + def _get_paged_result(self): return self._get_from_parent_node() # FIXME making extra query to the storage # This is not really needed as we have the data # in the self.obj itself # Mocking paged data # result = archive.Archive().get_snapshot_branches( # utils.str_to_swid(self.obj.id.hex()), # after=self._get_after_arg(), # first=self._get_first_arg()) # return PagedResult(results=result['branches'], # next_page_token=result['next_branch'].hex()) def _get_from_parent_node(self): """ Branches are avaialble in the snapshot object itself Not making an extra query """ results = [ { "name": key, "type": value["target_type"], "id": "temp-id", "target": value["target"], } for (key, value) in self.obj.branches.items() ][: self._get_first_arg()] # FIXME, this pagination is broken, fix it with swh-storage # Mocking PagedResult obj return PagedResult(results=results, next_page_token=self.obj.next_branch) def total_count(self): # FIXME, this can be implemented with current swh.storage API return None diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py index 693c5b7..96c766f 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,32 +1,32 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode class OriginVisitNode(BaseNode): def _get_node_data(self): # FIXME, make this call async (not for v1) return archive.Archive().get_origin_visit( self.kwargs.get("originUrl"), int(self.kwargs.get("id")) ) @property def id(self): # FIXME, use a better id return utils.encode(f"{self.origin}-{str(self.visit)}") class OriginVisitConnection(BaseConnection): _node_class = OriginVisitNode - def _get_page_result(self): + def _get_paged_result(self): """ Get the visits for the given origin parent obj (self.obj) is origin here """ # FIXME, make this call async (not for v1) 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 4f5b485..f0a8ff8 100644 --- a/swh/graphql/resolvers/visit_status.py +++ b/swh/graphql/resolvers/visit_status.py @@ -1,32 +1,32 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode class VisitStatusNode(BaseNode): def _get_node_data(self): """ """ @property def id(self): # FIXME, find logic to generate an id return utils.encode("dummy-id") class VisitStatusConnection(BaseConnection): """ self.obj is the visit object """ _node_class = VisitStatusNode - def _get_page_result(self): + def _get_paged_result(self): return archive.Archive().get_visit_status( self.obj.origin, self.obj.visit, after=self._get_after_arg(), first=self._get_first_arg(), )