diff --git a/swh/graphql/app.py b/swh/graphql/app.py index d6b19fc..040a006 100644 --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -1,12 +1,12 @@ from ariadne import load_schema_from_path, make_executable_schema, gql from ariadne.asgi import GraphQL -from .resolvers import origin, query +from .resolvers import resolvers type_defs = gql(load_schema_from_path("swh/graphql/schema/schema.graphql")) schema = make_executable_schema( - type_defs, query, origin.origin, origin.origins, origin.visit + type_defs, resolvers.query, resolvers.origin, resolvers.origins, resolvers.visit ) app = GraphQL(schema, debug=True) diff --git a/swh/graphql/backends/archive.py b/swh/graphql/backends/archive.py index 4e5883a..2970527 100644 --- a/swh/graphql/backends/archive.py +++ b/swh/graphql/backends/archive.py @@ -1,18 +1,18 @@ from swh.storage import get_storage class Archive: def __init__(self): 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): - # change page_token to base64 encode + # 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) diff --git a/swh/graphql/resolvers/__init__.py b/swh/graphql/resolvers/__init__.py index 3a4ab4c..e69de29 100644 --- a/swh/graphql/resolvers/__init__.py +++ b/swh/graphql/resolvers/__init__.py @@ -1,3 +0,0 @@ -from ariadne import ObjectType - -query = ObjectType("Query") diff --git a/swh/graphql/resolvers/base_connection.py b/swh/graphql/resolvers/base_connection.py index 6806588..9fafb22 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,37 +1,84 @@ """ """ # from dataclasses import dataclass - +# @dataclass # class PageInfo: -# """ -# dataclass -# """ +# nex_page_token: str + # class Arguments: # """ # dataclass # """ # after # Elements that come after the specified cursor # first # Returns the first n elements class BaseConnection: + 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): - pass + return self._get_edges() @property def nodes(self): - pass + return self.page_data.results @property def page_info(self): + # return PageInfo(self.page_data.next_page_token) return { - "hasNextPage": True, - "endCursor": "", + "hasNextPage": bool(self.page_data.next_page_token), + "endCursor": self.page_data.next_page_token, } @property def total_count(self): - return 0 + """ + Will be None for most of the connections + """ + + 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 + self._page_data = self._get_page_results() + return self._page_data + + def _get_page_results(self): + """ + Override for desired behaviour + """ + + return None + + def _get_edges(self): + return [{"cursor": "test", "node": each} for each in self.page_data.results] + + def _encode_cursor(self): + # FIXME, move to utils + pass + + def _decode_cursor(self): + # FIXME, move to utils + pass diff --git a/swh/graphql/resolvers/base_node.py b/swh/graphql/resolvers/base_node.py index 001946f..c2aa54d 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,8 +1,32 @@ """ """ class BaseNode: + def __init__(self, obj, info, **kwargs): + self.obj = obj + self.info = info + self.kwargs = kwargs + + self._node = None + + def __call__(self): + return self + + def __getattr__(self, attr): + if attr in self.__dict__: + return getattr(self, attr) + return getattr(self.node, attr) + @property - def id(self): - pass + def node(self): + if self._node is None: + self._node = self._get_node() + return self._node + + def _get_node(self): + """ + Override for desired behaviour + """ + + return None diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py index 80b182f..0351c69 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,86 +1,16 @@ -from ariadne import ObjectType +from .base_connection import BaseConnection +from .base_node import BaseNode -from . import query from swh.graphql.backends import archive -origin = ObjectType("Origin") -origins = ObjectType("OriginConnection") -visit = ObjectType("Visit") +class OriginConnection(BaseConnection): + def _get_page_results(self): + return archive.Archive().get_origins( + after=self.kwargs.get("after"), first=self.kwargs.get("first", 50) + ) -@query.field("origin") -def resolve_origin(_, info, **kw): - """ - Top level query - Get the origin matching the URL - """ - # return BaseNode.factory('origin').get(filters) - - return archive.Archive().get_origin(kw["url"]) - - -# @origin.field("url") -# def origin_url(origin, info): -# return origin.url - - -@origin.field("id") -def origin_id(origin, info): - return origin.id.hex() - - -@query.field("origins") -def resolve_origins(_, info, **kw): - """ - Top level query - Get all the origins matching the criteria - """ - # return BaseList.factory('origin').get(filters, state) - - origins = archive.Archive().get_origins( - after=kw.get("after"), first=kw.get("first") - ) - # return results - - return { - "nodes": origins.results, - "pageInfo": { - "hasNextPage": bool(origins.next_page_token), - "endCursor": origins.next_page_token, - }, - } - - -@origin.field("visits") -def resolve_origin_visits(origin, info, **kw): - visits = archive.Archive().get_origin_visits( - origin.url, after=kw.get("after"), first=kw.get("first") - ) - return { - "nodes": visits.results, - "origin": origin, - "pageInfo": { - "hasNextPage": bool(visits.next_page_token), - "endCursor": visits.next_page_token, - }, - } - - -def resolve_visit(_, info, **kw): - pass - - -@visit.field("status") -def visit_status(visit, info): - return str(visit.visit) - - -@visit.field("date") -def visit_date(visit, info): - return visit.date.timestamp() - - -@visit.field("id") -def visit_id(visit, info): - return str(visit.visit) +class OriginNode(BaseNode): + def _get_node(self): + return archive.Archive().get_origin(self.kwargs.get("url")) diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index 63433e7..f219dc5 100644 --- a/swh/graphql/resolvers/resolvers.py +++ b/swh/graphql/resolvers/resolvers.py @@ -1,40 +1,72 @@ -from ariadne import ObjectType +from .origin import OriginConnection, OriginNode +from .visit import OriginVisitConnection +from ariadne import ObjectType -def resolver_node_factory(resolver_type): - mapping = {"origin": "", "visit": ""} - return mapping[resolver_type]() +query = ObjectType("Query") +origin = ObjectType("Origin") +origins = ObjectType("OriginConnection") +visit = ObjectType("Visit") -def resolver_list_factory(resolver_type): +def node_resolver_factory(resolver_type): mapping = { - "origins": "", - "visits": "", + "origin": OriginNode, } - return mapping[resolver_type]() + return mapping[resolver_type] -query = ObjectType("Query") +def connection_resolver_factory(resolver_type): + mapping = {"origins": OriginConnection, "origin_visits": OriginVisitConnection} + return mapping[resolver_type] + # Nodes -@query.origin -def origin(): - return resolver_node_factory("origin",) +@query.field("origin") +def resolve_origin(_, info, **kw): + """ + Top level query + Get the origin matching the URL + """ + + # FIXME change to static factory in base class + return node_resolver_factory("origin")(None, info, **kw) + + +@origin.field("id") +def origin_id(origin, info): + # Using ariadne decorator to avoid infinite loop issue with id + return origin.id.hex() + + +# def resolve_visit(_, info, **kw): +# pass + + +@visit.field("date") +def visit_date(visit, info): + return visit.date.timestamp() + + +@visit.field("id") +def visit_id(visit, info): + return str(visit.visit) # Connections -@query.origins -def origins(): - return resolver_list_factory("origin",) +@query.field("origins") +def resolve_origins(_, info, **kw): + # FIXME change to static factory in base class + return connection_resolver_factory("origins")(None, info, **kw) -# @origin.visits -# def visits(): -# pass +@origin.field("visits") +def origin_visits(origin, info, **kw): + return connection_resolver_factory("origin_visits")(origin, info, **kw) # Other diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py new file mode 100644 index 0000000..62dc9f4 --- /dev/null +++ b/swh/graphql/resolvers/visit.py @@ -0,0 +1,10 @@ +from .base_connection import BaseConnection + +from swh.graphql.backends import archive + + +class OriginVisitConnection(BaseConnection): + def _get_page_results(self): + return archive.Archive().get_origin_visits( + self.obj.url, after=self.kwargs.get("after"), first=self.kwargs.get("first") + ) diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index cbb0ce2..0c91f4e 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,99 +1,99 @@ interface Node { id: ID! } scalar Date type PageInfo { endCursor: String hasNextPage: Boolean! } type Origin implements Node { url: String! id: ID! visits( first: Int after: String ): VisitConnection! } type OriginEdge { cursor: String! - node: [Origin] + node: Origin } type OriginConnection { edges: [OriginEdge] nodes: [Origin] pageInfo: PageInfo! totalCount: Int } type Visit implements Node { id: ID! date: Date! status: VisitStatusConnection - origin: Origin + # origin: Origin } type VisitEdge { cursor: String! - node: [Visit] + node: Visit } type VisitConnection { edges: [VisitEdge] nodes: [Visit] pageInfo: PageInfo! totalCount: Int } type VisitStatus implements Node { id: ID! status: String! date: Date! 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 """ origin( url: String! ): Origin """ Get a list of origins matching the given filters """ origins( first: Int after: String ): OriginConnection! - visit( - originUrl: String! - id: String! - ): Visit + # visit( + # originUrl: String! + # id: String! + # ): Visit } diff --git a/test.py b/test.py new file mode 100644 index 0000000..3656acd --- /dev/null +++ b/test.py @@ -0,0 +1,3 @@ +from swh.storage import get_storage + +storage = get_storage(cls="remote", url="http://moma.internal.softwareheritage.org:5002")