diff --git a/swh/graphql/models/__init__.py b/swh/graphql/models/__init__.py new file mode 100644 index 0000000..19abb55 --- /dev/null +++ b/swh/graphql/models/__init__.py @@ -0,0 +1,13 @@ +from .origin import OriginModel +from .snapshot import SnapshotModel +from .snapshot_branch import SnapshotBranchModel +from .visit import VisitModel +from .visit_status import VisitStatusModel + +__all__ = [ + "OriginModel", + "SnapshotModel", + "SnapshotBranchModel", + "VisitModel", + "VisitStatusModel", +] diff --git a/swh/graphql/resolvers/base_model.py b/swh/graphql/models/base_model.py similarity index 100% rename from swh/graphql/resolvers/base_model.py rename to swh/graphql/models/base_model.py diff --git a/swh/graphql/models/origin.py b/swh/graphql/models/origin.py new file mode 100644 index 0000000..86bd84d --- /dev/null +++ b/swh/graphql/models/origin.py @@ -0,0 +1,6 @@ +from .base_model import BaseModel + + +class OriginModel(BaseModel): + """ + """ diff --git a/swh/graphql/models/snapshot.py b/swh/graphql/models/snapshot.py new file mode 100644 index 0000000..47903d1 --- /dev/null +++ b/swh/graphql/models/snapshot.py @@ -0,0 +1,6 @@ +from .base_model import BaseModel + + +class SnapshotModel(BaseModel): + """ + """ diff --git a/swh/graphql/models/snapshot_branch.py b/swh/graphql/models/snapshot_branch.py new file mode 100644 index 0000000..5344b57 --- /dev/null +++ b/swh/graphql/models/snapshot_branch.py @@ -0,0 +1,6 @@ +from .base_model import BaseModel + + +class SnapshotBranchModel(BaseModel): + """ + """ diff --git a/swh/graphql/models/visit.py b/swh/graphql/models/visit.py new file mode 100644 index 0000000..e1fa982 --- /dev/null +++ b/swh/graphql/models/visit.py @@ -0,0 +1,10 @@ +from swh.graphql.utils import utils + +from .base_model import BaseModel + + +class VisitModel(BaseModel): + @property + def id(self): + # FIXME, use a better id + return utils.encode(f"{self.origin}-{str(self.visit)}") diff --git a/swh/graphql/models/visit_status.py b/swh/graphql/models/visit_status.py new file mode 100644 index 0000000..ed0666b --- /dev/null +++ b/swh/graphql/models/visit_status.py @@ -0,0 +1,10 @@ +from swh.graphql.utils import utils + +from .base_model import BaseModel + + +class VisitStatusModel(BaseModel): + @property + def id(self): + # FIXME, find a proper id + return utils.encode("temp-id") diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index ca05d00..dc71e10 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,111 +1,112 @@ """ """ 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): + # FIXME, make cursor work per item 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/origin.py b/swh/graphql/resolvers/origin.py index 9532668..e07ad5e 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,27 +1,23 @@ from swh.graphql.backends import archive +from swh.graphql.models import OriginModel from .base_connection import BaseConnection -from .base_model import BaseModel from .base_node import BaseNode -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 index 1feab1d..b7d2b56 100644 --- a/swh/graphql/resolvers/resolver_factory.py +++ b/swh/graphql/resolvers/resolver_factory.py @@ -1,33 +1,42 @@ from .origin import OriginConnection, OriginNode from .snapshot import SnapshotNode, VisitSnapshotNode from .snapshot_branch import SnapshotBranchConnection from .visit import OriginVisitConnection, OriginVisitNode from .visit_status import VisitStatusConnection +# 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] diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index a08f569..3e29e7b 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,85 +1,82 @@ -# FIXME, get rid of this module by directly decorating node/connection classes -# High level resolvers +""" +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 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 - # Node resolvers +# A node resolver can return a model object or a data structure @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)() @visitstatus.field("snapshot") def visit_snapshot(obj, info, **kw): resolver = get_node_resolver("visit-snapshot") return resolver(obj, info, **kw)() -# Connections +# Connection resolvers +# A connection resolver will return a sub class of BaseConnection @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 +# Any other type of resolver diff --git a/swh/graphql/resolvers/scalars.py b/swh/graphql/resolvers/scalars.py index cef43f9..9a1c999 100644 --- a/swh/graphql/resolvers/scalars.py +++ b/swh/graphql/resolvers/scalars.py @@ -1,22 +1,22 @@ from ariadne import ScalarType datetime_scalar = ScalarType("DateTime") swhid_scalar = ScalarType("SWHId") binary_text_scalar = ScalarType("BinaryText") @datetime_scalar.serializer def serialize_datetime(value): - # FIXME, consider timezone + # FIXME, consider timezone, use core functions return value.timestamp() @swhid_scalar.serializer def serialize_swid(value): return value.hex() @binary_text_scalar.serializer def serialize_binary_text(value): # FIXME, consider non utf-8 return value.decode("utf-8") diff --git a/swh/graphql/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py index 8cc2c3d..97a77ed 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,49 +1,46 @@ from swh.graphql.backends import archive +from swh.graphql.models import SnapshotModel 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): """ """ # FIXME, use methods from SWH core 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 + For accessing a snapshot from a visitstatus 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/snapshot_branch.py b/swh/graphql/resolvers/snapshot_branch.py index 5b834dd..0058b33 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,28 +1,24 @@ # from swh.graphql.backends import archive +from swh.graphql.models import SnapshotBranchModel from swh.storage.interface import PagedResult from .base_connection import BaseConnection -from .base_model import BaseModel - - -class SnapshotBranchModel(BaseModel): - pass class SnapshotBranchConnection(BaseConnection): _model_class = SnapshotBranchModel 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 results = [ {"name": key, "type": value["target_type"], "id": "temp-id"} 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/visit.py b/swh/graphql/resolvers/visit.py index 948cd2c..f074c46 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,37 +1,29 @@ from swh.graphql.backends import archive -from swh.graphql.utils import utils +from swh.graphql.models import VisitModel from .base_connection import BaseConnection -from .base_model import BaseModel from .base_node import BaseNode -class VisitModel(BaseModel): - @property - def id(self): - # FIXME - 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() ) diff --git a/swh/graphql/resolvers/visit_status.py b/swh/graphql/resolvers/visit_status.py index fce1b27..00c1dff 100644 --- a/swh/graphql/resolvers/visit_status.py +++ b/swh/graphql/resolvers/visit_status.py @@ -1,24 +1,16 @@ from swh.graphql.backends import archive -from swh.graphql.utils import utils +from swh.graphql.models import VisitStatusModel from .base_connection import BaseConnection -from .base_model import BaseModel - - -class VisitStatusModel(BaseModel): - @property - def id(self): - # FIXME - 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(), ) diff --git a/swh/graphql/utils/utils.py b/swh/graphql/utils/utils.py index 5ba5028..bf5aa54 100644 --- a/swh/graphql/utils/utils.py +++ b/swh/graphql/utils/utils.py @@ -1,21 +1,22 @@ import base64 def encode(text): return base64.b64encode(bytes(text, "utf-8")).decode("utf-8") def get_encoded_cursor(cursor): if cursor is None: return None return base64.b64encode(bytes(cursor, "utf-8")).decode("utf-8") def get_decoded_cursor(cursor): if cursor is None: return None return base64.b64decode(cursor).decode("utf-8") def str_to_swid(str_swid): + # FIXME, use core function return bytearray.fromhex(str_swid)