diff --git a/swh/graphql/models/__init__.py b/swh/graphql/models/__init__.py deleted file mode 100644 index d2cacf4..0000000 --- a/swh/graphql/models/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from .origin import OriginModel -from .release import ReleaseModel -from .revision import RevisionModel -from .snapshot import SnapshotModel -from .snapshot_branch import SnapshotBranchModel -from .visit import VisitModel -from .visit_status import VisitStatusModel - -__all__ = [ - "OriginModel", - "SnapshotModel", - "SnapshotBranchModel", - "VisitModel", - "VisitStatusModel", - "RevisionModel", - "ReleaseModel", -] diff --git a/swh/graphql/models/base_model.py b/swh/graphql/models/base_model.py deleted file mode 100644 index 4741ac6..0000000 --- a/swh/graphql/models/base_model.py +++ /dev/null @@ -1,30 +0,0 @@ -from abc import ABC -from collections import namedtuple - - -class BaseModel(ABC): - def __init__(self, node): - """ - Wrapper class for the model objects - - SWH storage is not consistent with - return types. - It is returing object in some cases - and dict in some other - Mocking an object in case of dict - """ - # FIXME, this could fail in nested dicts - if type(node) is dict: - self._node = namedtuple("ModelObj", node.keys())(*node.values()) - else: - self._node = node - - 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/models/origin.py b/swh/graphql/models/origin.py deleted file mode 100644 index 86bd84d..0000000 --- a/swh/graphql/models/origin.py +++ /dev/null @@ -1,6 +0,0 @@ -from .base_model import BaseModel - - -class OriginModel(BaseModel): - """ - """ diff --git a/swh/graphql/models/release.py b/swh/graphql/models/release.py deleted file mode 100644 index ea4b8d6..0000000 --- a/swh/graphql/models/release.py +++ /dev/null @@ -1,9 +0,0 @@ -from .base_model import BaseModel - - -class ReleaseModel(BaseModel): - """ - """ - - def is_type_of(self): - return "Release" diff --git a/swh/graphql/models/revision.py b/swh/graphql/models/revision.py deleted file mode 100644 index bbd3a47..0000000 --- a/swh/graphql/models/revision.py +++ /dev/null @@ -1,9 +0,0 @@ -from .base_model import BaseModel - - -class RevisionModel(BaseModel): - """ - """ - - def is_type_of(self): - return "Revision" diff --git a/swh/graphql/models/snapshot.py b/swh/graphql/models/snapshot.py deleted file mode 100644 index 47903d1..0000000 --- a/swh/graphql/models/snapshot.py +++ /dev/null @@ -1,6 +0,0 @@ -from .base_model import BaseModel - - -class SnapshotModel(BaseModel): - """ - """ diff --git a/swh/graphql/models/snapshot_branch.py b/swh/graphql/models/snapshot_branch.py deleted file mode 100644 index 5344b57..0000000 --- a/swh/graphql/models/snapshot_branch.py +++ /dev/null @@ -1,6 +0,0 @@ -from .base_model import BaseModel - - -class SnapshotBranchModel(BaseModel): - """ - """ diff --git a/swh/graphql/models/visit.py b/swh/graphql/models/visit.py deleted file mode 100644 index e1fa982..0000000 --- a/swh/graphql/models/visit.py +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index ed0666b..0000000 --- a/swh/graphql/models/visit_status.py +++ /dev/null @@ -1,10 +0,0 @@ -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 205a1e2..d70b833 100644 --- a/swh/graphql/resolvers/base_connection.py +++ b/swh/graphql/resolvers/base_connection.py @@ -1,112 +1,114 @@ """ """ 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): - _model_class: Any = None + _node_class: Any = 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): + 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 model class is set, + If a node class is set, return a list of its intance - else a list of nodes + else a list of raw results """ - if self._model_class is not None: - return [self._model_class(obj) for obj in self.page_data.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 + ] 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] + return [{"cursor": "test", "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 """ return self.kwargs.get("first", 50) diff --git a/swh/graphql/resolvers/base_node.py b/swh/graphql/resolvers/base_node.py index d83780a..003e934 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,40 +1,47 @@ """ """ + from abc import ABC, abstractmethod -from typing import Any +from collections import namedtuple class BaseNode(ABC): - _model_class: Any = None - def __init__(self, obj, info, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs + node_data = kwargs.get("node_data") + self._set_node(node_data) - self._node = None + 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 __call__(self): + 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): """ - 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 + return self @abstractmethod - def _get_node(self): + 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/content.py b/swh/graphql/resolvers/content.py index 2e20f6c..412265e 100644 --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -1,6 +1,7 @@ from .base_node import BaseNode class ContentNode(BaseNode): - def _get_node(self): - pass + def _get_node_data(self): + """ + """ diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py index c36b764..e8f2b35 100644 --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -1,7 +1,7 @@ from .base_node import BaseNode class DirectoryNode(BaseNode): - def _get_node(self): + def _get_node_data(self): """ """ diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py index e07ad5e..ca98377 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,23 +1,20 @@ from swh.graphql.backends import archive -from swh.graphql.models import OriginModel from .base_connection import BaseConnection from .base_node import BaseNode class OriginNode(BaseNode): - _model_class = OriginModel - - def _get_node(self): + def _get_node_data(self): # FIXME, make this call async (not for v1) return archive.Archive().get_origin(self.kwargs.get("url")) class OriginConnection(BaseConnection): - _model_class = OriginModel + _node_class = OriginNode 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/release.py b/swh/graphql/resolvers/release.py index dad17ac..eca22d1 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,11 +1,10 @@ -from swh.graphql.models import ReleaseModel - from .base_node import BaseNode class ReleaseNode(BaseNode): - _model_class = ReleaseModel - - def _get_node(self): + def _get_node_data(self): # FIXME, make this call async (not for v1) return {"rel": "test"} + + def is_type_of(self): + return "Release" diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py index fd8ddcb..5856213 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 = 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, *_): """ - To resolve any union type + Generic resolver for all the union types """ return obj.is_type_of() diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py index 88d3bc8..92820a2 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,11 +1,10 @@ -from swh.graphql.models import RevisionModel - from .base_node import BaseNode class RevisionNode(BaseNode): - _model_class = RevisionModel - - def _get_node(self): + def _get_node_data(self): # FIXME, make this call async (not for v1) return {"rev": "test"} + + def is_type_of(self): + return "Revision" diff --git a/swh/graphql/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py index 97a77ed..5348119 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,46 +1,41 @@ 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_node import BaseNode class SnapshotNode(BaseNode): """ For directly accessing a snapshot with swhid """ - _model_class = SnapshotModel - - def _get_node(self): + def _get_node_data(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 from a visitstatus type """ - _model_class = SnapshotModel + node_class = SnapshotNode - def _get_node(self): + def _get_node_data(self): """ self.obj is visitstatus here - snapshot swhid is avaialbe in the object + snapshot swhid is avaialbe in the parent (self.obj) """ - snapshot_swhid = self.obj.snapshot - return archive.Archive().get_snapshot(snapshot_swhid) + return archive.Archive().get_snapshot(self.obj.snapshot) -class SnapshotConnection(BaseConnection): - """ - To get all the snapshots under an origin - """ +# class SnapshotConnection(BaseConnection): +# """ +# To get all the snapshots under an origin +# """ - _model_class = SnapshotModel +# _node_class = SnapshotNode diff --git a/swh/graphql/resolvers/snapshot_branch.py b/swh/graphql/resolvers/snapshot_branch.py index 92aee99..e7b6257 100644 --- a/swh/graphql/resolvers/snapshot_branch.py +++ b/swh/graphql/resolvers/snapshot_branch.py @@ -1,48 +1,55 @@ # from swh.graphql.backends import archive -from swh.graphql.models import SnapshotBranchModel - # 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 class SnapshotBranchConnection(BaseConnection): - _model_class = SnapshotBranchModel + _node_class = SnapshotBranchNode def _get_page_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() ][:1] # 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 f074c46..693c5b7 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,29 +1,32 @@ from swh.graphql.backends import archive -from swh.graphql.models import VisitModel +from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode class OriginVisitNode(BaseNode): - _model_class = VisitModel - - def _get_node(self): + 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): - _model_class = VisitModel + _node_class = OriginVisitNode 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 00c1dff..4f5b485 100644 --- a/swh/graphql/resolvers/visit_status.py +++ b/swh/graphql/resolvers/visit_status.py @@ -1,16 +1,32 @@ from swh.graphql.backends import archive -from swh.graphql.models import VisitStatusModel +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): - _model_class = VisitStatusModel + """ + self.obj is the visit object + """ + + _node_class = VisitStatusNode 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/schema/schema.graphql b/swh/graphql/schema/schema.graphql index 21fb2c9..a750249 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,223 +1,223 @@ scalar SWHId scalar DateTime scalar BinaryText interface Node { id: ID! } interface SWHNode { id: SWHId! } type PageInfo { endCursor: String hasNextPage: Boolean! } type OriginConnection { edges: [OriginEdge] nodes: [Origin] pageInfo: PageInfo! totalCount: Int } type OriginEdge { cursor: String! node: Origin } type Origin implements SWHNode { id: SWHId! # FIXME, this is not swhid url: String! visits( first: Int after: String ): VisitConnection! } type VisitConnection { edges: [VisitEdge] nodes: [Visit] pageInfo: PageInfo! totalCount: Int } type VisitEdge { cursor: String! node: Visit } type Visit implements Node { id: ID! date: DateTime! type: String status( first: Int after: String ): VisitStatusConnection # origin: Origin # FIXME, this can be added later } type VisitStatusConnection { edges: [VisitStatusEdge] nodes: [VisitStatus] pageInfo: PageInfo! totalCount: Int } type VisitStatusEdge { cursor: String! node: VisitStatus } type VisitStatus implements Node { id: ID! status: String! date: DateTime! snapshot: Snapshot type: String } # FIXME, add OriginSnapshotConnection type Snapshot implements SWHNode { id: SWHId! branches( first: Int after: String ): BranchConnection # releases( # first: Int # after: String # ): ReleaseConnection # FIXME, add alias type as well } type BranchConnection { edges: [BranchConnectionEdge] nodes: [Branch] pageInfo: PageInfo! totalCount: Int } type BranchConnectionEdge { cursor: String! node: [Branch] } # FIXME, this could be alias or Directory as well union BranchTarget = Revision | Release type Branch implements Node { # FIXME, maybe implement Node is not needed here # As this has no independent existence id: ID! name: BinaryText type: String # FIXME, change to an enum target: BranchTarget } # type RevisionConnection { # } # type RevisionEdge { # } type Revision implements SWHNode { id: SWHId! - name: String + rev: String } # type ReleaseConnection { # } # type ReleasEdge { # } type Release implements SWHNode { id: SWHId! - age: String + rel: String } type Directory implements SWHNode { id: SWHId! } type Content implements SWHNode { id: SWHId! } 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 also 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 """ Get a snapshot with SWHId """ snapshot( SWHId: String! ): Snapshot # """ # Get all the snapshot for the given origin # """ # originSnapshot( # originUrl: String! # first: Int # after: String # ): SnapshotConnection # """ # Get the revision with the given swhid # """ # revision() # """ # Get the directory with the given swhid # """ # directory # """ # Get the content with the given swhid # """ # content( # SWHId: String! # ): Content # """ # Search with the given swhid # """ # searchWithSwhid }