diff --git a/swh/graphql/app.py b/swh/graphql/app.py index a6c7a41..43a5661 100644 --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -1,19 +1,18 @@ -from ariadne import load_schema_from_path, make_executable_schema, gql +from ariadne import gql, load_schema_from_path, make_executable_schema from ariadne.asgi import GraphQL -from .resolvers import resolvers -from .resolvers import scalars +from .resolvers import resolvers, scalars type_defs = gql(load_schema_from_path("swh/graphql/schema/schema.graphql")) schema = make_executable_schema( type_defs, resolvers.query, resolvers.origin, resolvers.origins, resolvers.visit, scalars.datetime_scalar, scalars.swhid_scalar, ) app = GraphQL(schema, debug=True) diff --git a/swh/graphql/backends/archive.py b/swh/graphql/backends/archive.py index 2970527..49bb407 100644 --- a/swh/graphql/backends/archive.py +++ b/swh/graphql/backends/archive.py @@ -1,18 +1,22 @@ from swh.storage import get_storage class Archive: def __init__(self): + # FIXME, setup config self.storage = get_storage( cls="remote", url="http://moma.internal.softwareheritage.org:5002" ) def get_origin(self, url): return self.storage.origin_get([url])[0] def get_origins(self, after=None, first=50): # FIXME change page_token to base64 encode return self.storage.origin_list(page_token=after, limit=first) - def get_origin_visits(self, origin, after=None, first=50): - return self.storage.origin_visit_get(origin, page_token=after, limit=first) + def get_origin_visits(self, origin_url, after=None, first=50): + return self.storage.origin_visit_get(origin_url, page_token=after, limit=first) + + def get_origin_visit(self, origin_url, visit_id): + return self.storage.origin_visit_get_by(origin_url, visit_id) diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index 51f160c..4344145 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,97 +1,102 @@ """ """ 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): 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 + """ 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_results() + self._page_data = self._get_page_result() return self._page_data @abstractmethod - def _get_page_results(self): + 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.page_data.results] + 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 f9eca5c..0c2adad 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,18 +1,18 @@ +from swh.graphql.backends import archive + from .base_connection import BaseConnection from .base_node import BaseNode -from swh.graphql.backends import archive - class OriginConnection(BaseConnection): - def _get_page_results(self): + 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 OriginNode(BaseNode): def _get_node(self): # FIXME, make this call async (not for v1) return archive.Archive().get_origin(self.kwargs.get("url")) diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index eb44447..d93e41d 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,83 +1,84 @@ -from .origin import OriginConnection, OriginNode -from .visit import OriginVisitConnection - from ariadne import ObjectType +from swh.graphql.utils import utils + +from .origin import OriginConnection, OriginNode +from .visit import OriginVisit, OriginVisitConnection + query = ObjectType("Query") origin = ObjectType("Origin") origins = ObjectType("OriginConnection") visit = ObjectType("Visit") 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(info): # FIXME, replace with a proper factory method - mapping = { - "origin": OriginNode, - } - resolver_type = get_mapping_key(info) + mapping = {"origin": OriginNode, "visit": OriginVisit} + resolver_type = info.path.key # 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(info): # FIXME, replace with a proper factory method - mapping = {"origins": OriginConnection, "origin_visits": OriginVisitConnection} - resolver_type = get_mapping_key(info) + mapping = {"origins": OriginConnection, "visits": OriginVisitConnection} + resolver_type = info.path.key # 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 @query.field("visit") @query.field("origin") def node_resolver(obj, info, **kw): """ Resolver for all the node types """ # FIXME change to static factory in base class resolver = get_node_resolver(info) return resolver(obj, info, **kw)() # Resolvers for node fields # Safer to annotate them here than adding as # property in the node class @origin.field("id") def origin_id(origin, info): # Using ariadne decorator to avoid infinite loop issue with id return origin.id.hex() @visit.field("id") def visit_id(visit, info): - return str(visit.visit) + # FIXME, find a better id for visit + return utils.encode(f"{visit.origin}-{str(visit.visit)}") # Connections @origin.field("visits") @query.field("origins") def connection_resolver(obj, info, **kw): # FIXME change to static factory in base class resolver = get_connection_resolver(info) return resolver(obj, info, **kw)() # Other diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py index c8112bf..78ea9dc 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,18 +1,29 @@ +from swh.graphql.backends import archive + from .base_connection import BaseConnection from .base_node import BaseNode -from swh.graphql.backends import archive - class OriginVisit(BaseNode): def _get_node(self): # FIXME, make this call async (not for v1) - pass + return archive.Archive().get_origin_visit( + self.kwargs.get("originUrl"), int(self.kwargs.get("id")) + ) class OriginVisitConnection(BaseConnection): - def _get_page_results(self): + 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 OriginVisitStatusConnection(BaseConnection): + def _get_page_result(self): + pass diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index eca3d28..fa70449 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,118 +1,118 @@ interface Node { id: ID! } scalar SWHId scalar DateTime type PageInfo { endCursor: String hasNextPage: Boolean! } type Origin implements Node { # FIXME, find an idea for a UNIQUE id url: String! id: ID! visits( first: Int after: String ): VisitConnection! } type OriginEdge { cursor: String! node: Origin } type OriginConnection { edges: [OriginEdge] nodes: [Origin] pageInfo: PageInfo! totalCount: Int } type Visit implements Node { id: ID! date: DateTime! # status: VisitStatusConnection - # origin: Origin + # origin: Origin # FIXME, this can be added later } type VisitEdge { cursor: String! node: Visit } type VisitConnection { edges: [VisitEdge] nodes: [Visit] pageInfo: PageInfo! totalCount: Int } # type VisitStatus implements Node { # id: ID! # status: String! # date: DateTime! # snapshot: String # type: String # } # type VisitStatusEdge { # cursor: String! # node: [VisitStatus] # } # type VisitStatusConnection { # edges: [VisitStatusEdge] # nodes: [VisitStatus] # pageInfo: PageInfo! # totalCount: Int # } # type Snapshot implements Node { # id: ID! # # branches # } type Query { """ Get an origin with its url """ # FIXME, find some unique id to help cache # maybe base64 encode the URL origin( url: String! ): Origin """ Get a list of origins matching the given filters Can be used to search for an origin """ # FIMXE, use Input types to make this cleaner origins( first: Int after: String ): OriginConnection! """ Get a visit object with its id and/or origin and visit id """ # FIXME, find some unique id to help cache visit( originUrl: String! id: String! ): Visit } # Remove statusConnection # Make visit obj by making multiple calls # Make snapshot object # Make revisionconnection # Make contnetConnection diff --git a/swh/graphql/utils/utils.py b/swh/graphql/utils/utils.py index ebe0394..50c7337 100644 --- a/swh/graphql/utils/utils.py +++ b/swh/graphql/utils/utils.py @@ -1,13 +1,17 @@ -# import base64 +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 cursor + return base64.b64encode(bytes(cursor, "utf-8")).decode("utf-8") def get_decoded_cursor(cursor): if cursor is None: return None - return cursor + return base64.b64decode(cursor).decode("utf-8")