diff --git a/swh/graphql/middlewares/graphql.py b/swh/graphql/middlewares/graphql.py index 0841dda..a203330 100644 --- a/swh/graphql/middlewares/graphql.py +++ b/swh/graphql/middlewares/graphql.py @@ -1,7 +1,11 @@ """ To implement graphql middleware """ +class CostError: + pass + + def cost_limiter(): pass diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index 4344145..ca05d00 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,102 +1,111 @@ """ """ from abc import ABC, abstractmethod 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): + _model_class = None + def __init__(self, obj, info, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._page_data = None self.pageInfo = self.page_info self.totalCount = self.total_count def __call__(self): return self @property def edges(self): return self._get_edges() @property def nodes(self): """ Override if needed return a list of objects + + If a model class is set, + return a list of its intance + else a list of nodes """ + + if self._model_class is not None: + return [self._model_class(obj) for obj in self.page_data.results] return self.page_data.results @property def page_info(self): # FIXME Replace with a dataclass # return PageInfo(self.page_data.next_page_token) return { "hasNextPage": bool(self.page_data.next_page_token), "endCursor": utils.get_encoded_cursor(self.page_data.next_page_token), } @property def total_count(self): """ Will be None for most of the connections override if needed """ return None @property def page_data(self): """ Cache to avoid multiple calls to the backend """ if self._page_data is None: # FIXME, make this call async (not for v1) self._page_data = self._get_page_result() return self._page_data @abstractmethod def _get_page_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 [{"cursor": "test", "node": each} for each 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 """ return self.kwargs.get("first", 50) diff --git a/swh/graphql/resolvers/base_model.py b/swh/graphql/resolvers/base_model.py new file mode 100644 index 0000000..7e21780 --- /dev/null +++ b/swh/graphql/resolvers/base_model.py @@ -0,0 +1,21 @@ +from abc import ABC + + +class BaseModel(ABC): + def __init__(self, node): + """ + Wrapper class for the swh.model objects + """ + self._node = node + + def __getattr__(self, name): + """ + SWH storage is not consistent with + return types. + It is returing object in some cases + and dict in some other + Fix to handle that inconsistency + """ + if type(self._node) is dict: + return self._node.get(name) + return getattr(self._node, name) diff --git a/swh/graphql/resolvers/base_node.py b/swh/graphql/resolvers/base_node.py index 542663c..408e2d3 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,31 +1,39 @@ """ """ from abc import ABC, abstractmethod class BaseNode(ABC): + _model_class = None + def __init__(self, obj, info, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._node = None def __call__(self): + """ + If a model class is set, + return its instance, else node as it is + """ + if self._model_class is not None: + return self._model_class(self.node) return self.node @property def node(self): # This is a small cache to avoid multiple # backend calls if self._node is None: self._node = self._get_node() return self._node @abstractmethod def _get_node(self): """ Override for desired behaviour """ # FIXME, make this call async (not for v1) return None diff --git a/swh/graphql/resolvers/branch.py b/swh/graphql/resolvers/branch.py index c1f1299..6e3ef6c 100644 --- a/swh/graphql/resolvers/branch.py +++ b/swh/graphql/resolvers/branch.py @@ -1,21 +1,31 @@ # from swh.graphql.backends import archive from swh.storage.interface import PagedResult from .base_connection import BaseConnection +from .base_model import BaseModel + + +class BranchModel(BaseModel): + pass class SnapshotBranchConnection(BaseConnection): + _model_class = BranchModel + def _get_page_result(self): """ Branches are avaialble in the snapshot object itself Not making a query """ # FIXME Mocking PagedResult to make base_connection work # FIX this in swh-storage # FIX id + import pdb + + pdb.set_trace() results = [ {"name": key, "type": value["target_type"], "id": "temp-id"} - for (key, value) in self.obj["branches"].items() - ] + for (key, value) in self.obj.branches.items() + ][:5] # FIXME, this pagination is broken, fix it with swh-storage return PagedResult(results=results, next_page_token=self.obj["next_branch"]) diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py index 0c2adad..9532668 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,18 +1,27 @@ from swh.graphql.backends import archive from .base_connection import BaseConnection +from .base_model import BaseModel from .base_node import BaseNode -class OriginConnection(BaseConnection): - def _get_page_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() - ) +class OriginModel(BaseModel): + pass class OriginNode(BaseNode): + _model_class = OriginModel + def _get_node(self): # FIXME, make this call async (not for v1) return archive.Archive().get_origin(self.kwargs.get("url")) + + +class OriginConnection(BaseConnection): + _model_class = OriginModel + + def _get_page_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/resolver_factory.py b/swh/graphql/resolvers/resolver_factory.py new file mode 100644 index 0000000..6f9d6e3 --- /dev/null +++ b/swh/graphql/resolvers/resolver_factory.py @@ -0,0 +1,33 @@ +from .branch import SnapshotBranchConnection +from .origin import OriginConnection, OriginNode +from .snapshot import SnapshotNode, VisitSnapshotNode +from .visit import OriginVisitConnection, OriginVisitNode +from .visit_status import VisitStatusConnection + + +def get_node_resolver(resolver_type): + # FIXME, replace with a proper factory method + mapping = { + "origin": OriginNode, + "visit": OriginVisitNode, + "visit-snapshot": VisitSnapshotNode, + "snapshot": SnapshotNode, + } + # resolver_type = get_mapping_key(info) # FIXME, get full name + if resolver_type not in mapping: + raise AttributeError(f"Invalid type request {resolver_type}") + return mapping[resolver_type] + + +def get_connection_resolver(resolver_type): + # FIXME, replace with a proper factory method + mapping = { + "origins": OriginConnection, + "origin-visits": OriginVisitConnection, + "visit-status": VisitStatusConnection, + "snapshot-branches": SnapshotBranchConnection, + } + # resolver_type = get_mapping_key(info) # FIXME, get full name + if resolver_type not in mapping: + raise AttributeError(f"Invalid type request {resolver_type}") + return mapping[resolver_type] diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index 95694f4..a08f569 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,135 +1,85 @@ # FIXME, get rid of this module by directly decorating node/connection classes +# High level resolvers from ariadne import ObjectType -from swh.graphql.utils import utils - -from .branch import SnapshotBranchConnection -from .origin import OriginConnection, OriginNode -from .snapshot import SnapshotNode, VisitSnapshotNode -from .visit import OriginVisitConnection, OriginVisitNode, VisitStatusConnection +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") # def get_mapping_key(info): # """ # Logic to resolve mapping type # """ # # FIXME, move to utils # if info.path.prev: # return f"{info.path.prev.key}_{info.path.key}" # return info.path.key - -def get_node_resolver(resolver_type): - # FIXME, replace with a proper factory method - mapping = { - "origin": OriginNode, - "visit": OriginVisitNode, - "visit-snapshot": VisitSnapshotNode, - "snapshot": SnapshotNode, - } - # resolver_type = get_mapping_key(info) # FIXME, get full name - if resolver_type not in mapping: - raise AttributeError(f"Invalid type request {resolver_type}") - return mapping[resolver_type] - - -def get_connection_resolver(resolver_type): - # FIXME, replace with a proper factory method - mapping = { - "origins": OriginConnection, - "origin-visits": OriginVisitConnection, - "visit-status": VisitStatusConnection, - "snapshot-branches": SnapshotBranchConnection, - } - # resolver_type = get_mapping_key(info) # FIXME, get full name - if resolver_type not in mapping: - raise AttributeError(f"Invalid type request {resolver_type}") - return mapping[resolver_type] - - -# Nodes +# Node resolvers @query.field("origin") def origin_resolver(obj, info, **kw): """ """ # FIXME change to static factory in base class 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)() -# Resolvers for node fields -# Using ariadne resolution instead of adding properties in Node -# That is to avoid looping in NodeConnection - - -@visit.field("id") -def visit_id(visit, info): - # FIXME, find a better id for visit - return utils.encode(f"{visit.origin}-{str(visit.visit)}") - - -@visitstatus.field("id") -def visit_status_id(_, info): - # FIXME, find a proper id - return utils.encode("temp-id") - - @visitstatus.field("snapshot") def visit_snapshot(obj, info, **kw): resolver = get_node_resolver("visit-snapshot") 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)() - - # Connections @query.field("origins") def origins_resolver(obj, info, **kw): # FIXME change to static factory in base class 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)() + + # Other diff --git a/swh/graphql/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py index 582f97f..1ab15e3 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,41 +1,51 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection +from .base_model import BaseModel from .base_node import BaseNode +class SnapshotModel(BaseModel): + pass + + class SnapshotNode(BaseNode): """ For directly accessing a snapshot with swhid """ + _model_class = SnapshotModel + def _get_node(self): """ self.obj is visitstatus here snapshot swhid is avaialbe in the object """ snapshot_swhid = utils.str_to_swid(self.kwargs.get("SWHId")) return archive.Archive().get_snapshot(snapshot_swhid) class VisitSnapshotNode(BaseNode): # FIXME, maybe it is a good idea to make a # common function for both Node classes (for handling exceptions) """ For accessing a snapshot through the visit type """ + _model_class = SnapshotModel def _get_node(self): """ self.obj is visitstatus here snapshot swhid is avaialbe in the object """ snapshot_swhid = self.obj.snapshot return archive.Archive().get_snapshot(snapshot_swhid) class SnapshotConnection(BaseConnection): """ To get all the snapshots under an origin """ + + _model_class = SnapshotModel diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py index 02b8292..399bb6a 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,34 +1,36 @@ from swh.graphql.backends import archive +from swh.graphql.utils import utils from .base_connection import BaseConnection +from .base_model import BaseModel from .base_node import BaseNode +class VisitModel(BaseModel): + @property + def id(self): + return utils.encode(f"{self.origin}-{str(self.visit)}") + + class OriginVisitNode(BaseNode): + _model_class = VisitModel + def _get_node(self): # FIXME, make this call async (not for v1) return archive.Archive().get_origin_visit( self.kwargs.get("originUrl"), int(self.kwargs.get("id")) ) class OriginVisitConnection(BaseConnection): + _model_class = VisitModel + def _get_page_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() ) - - -class VisitStatusConnection(BaseConnection): - def _get_page_result(self): - return archive.Archive().get_visit_status( - self.obj.origin, - self.obj.visit, - 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 new file mode 100644 index 0000000..017ba0c --- /dev/null +++ b/swh/graphql/resolvers/visit_status.py @@ -0,0 +1,23 @@ +from swh.graphql.backends import archive +from swh.graphql.utils import utils + +from .base_connection import BaseConnection +from .base_model import BaseModel + + +class VisitStatusModel(BaseModel): + @property + def id(self): + return utils.encode("temp-id") + + +class VisitStatusConnection(BaseConnection): + _model_class = VisitStatusModel + + def _get_page_result(self): + return archive.Archive().get_visit_status( + self.obj.origin, + self.obj.visit, + after=self._get_after_arg(), + first=self._get_first_arg(), + )