diff --git a/swh/graphql/app.py b/swh/graphql/app.py index 4316837..71d7269 100644 --- a/swh/graphql/app.py +++ b/swh/graphql/app.py @@ -1,26 +1,27 @@ from ariadne import gql, load_schema_from_path, make_executable_schema 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.visit, resolvers.visit_status, resolvers.snapshot, resolvers.snapshot_branch, resolvers.revision, resolvers.release, resolvers.directory, resolvers.directory_entry, resolvers.branch_target, resolvers.release_target, resolvers.directory_entry_target, + scalars.id_scalar, scalars.datetime_scalar, scalars.swhid_scalar, - scalars.sha1_scalar, - scalars.binary_text_scalar, + scalars.hash_value_scalar, + scalars.binary_string_scalar, ) diff --git a/swh/graphql/resolvers/base_node.py b/swh/graphql/resolvers/base_node.py index 38fcd12..9f389f3 100644 --- a/swh/graphql/resolvers/base_node.py +++ b/swh/graphql/resolvers/base_node.py @@ -1,70 +1,76 @@ from abc import ABC from collections import namedtuple from swh.graphql.errors import ObjectNotFoundError class BaseNode(ABC): """ Base class for all the Node resolvers """ def __init__(self, obj, info, node_data=None, **kwargs): self.obj = obj self.info = info self.kwargs = kwargs self._node = self._get_node(node_data) # handle the errors, if any, after _node is set self._handle_node_errors() def _get_node(self, node_data): """ Get the node object from the given data if the data (node_data) is none make a function call to get data from backend """ if node_data is None: node_data = self._get_node_data() return self._get_node_from_data(node_data) def _get_node_from_data(self, node_data): """ Get the object from node_data In case of a dict, convert it to an object Override to support different data structures """ if type(node_data) is dict: return namedtuple("NodeObj", node_data.keys())(*node_data.values()) return node_data def _handle_node_errors(self): """ Handle any error related to node data raise an error in case the object returned is None override for specific behaviour """ if self._node is None: raise ObjectNotFoundError("Requested object is not available") def __call__(self, *args, **kw): return self def _get_node_data(self): """ Override for desired behaviour This will be called only when node_data is None """ # 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__ + + +class BaseSWHNode(BaseNode): + @property + def SWHID(self): + return self._node.swhid() diff --git a/swh/graphql/resolvers/content.py b/swh/graphql/resolvers/content.py index 0aa1faa..408cea6 100644 --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -1,52 +1,44 @@ from swh.graphql.backends import archive -from .base_node import BaseNode +from .base_node import BaseSWHNode -class BaseContentNode(BaseNode): +class BaseContentNode(BaseSWHNode): """ """ def _get_content_by_id(self, content_id): content = archive.Archive().get_content(content_id) return content[0] if content else None - @property - def id(self): - return self._node.unique_key() - - @property - def swhId(self): # To support the schema naming convention - return self._node.swhid() - @property def checksum(self): # FIXME, return a Node object return self._node.hashes() @property - def data(self): - return + def id(self): + return self._node.sha1_git def is_type_of(self): return "Content" class ContentNode(BaseContentNode): def _get_node_data(self): """ When a content is requested directly with an id """ return self._get_content_by_id(self.kwargs.get("SWHID").object_id) class TargetContentNode(BaseContentNode): def _get_node_data(self): """ When a content is requested from a directory entry or from a release target content id is obj.target here """ content_id = self.obj.target return self._get_content_by_id(content_id) diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py index 15393ff..0c18611 100644 --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -1,50 +1,50 @@ from swh.graphql.backends import archive from swh.model.model import Directory -from .base_node import BaseNode +from .base_node import BaseSWHNode -class BaseDirectoryNode(BaseNode): +class BaseDirectoryNode(BaseSWHNode): def _get_directory_by_id(self, directory_id): # Return a Directory model object # entries is initialized as empty # Same pattern is used in snapshot return Directory(id=directory_id, entries=()) def is_type_of(self): return "Directory" class DirectoryNode(BaseDirectoryNode): def _get_node_data(self): """ When a directory is requested directly with an id """ directory_id = self.kwargs.get("SWHID").object_id # path = "" if archive.Archive().is_directory_available([directory_id]): return self._get_directory_by_id(directory_id) return None class RevisionDirectoryNode(BaseDirectoryNode): def _get_node_data(self): """ When a directory is requested from a revision self.obj is revision here self.obj.directoryId is the required dir id (set from resolvers.revision.py:BaseRevisionNode) """ directory_id = self.obj.directoryId return self._get_directory_by_id(directory_id) class TargetDirectoryNode(BaseDirectoryNode): def _get_node_data(self): """ When a directory is requested as a target self.obj can be a Release or a DirectoryEntry obj.target is the requested directory id here """ return self._get_directory_by_id(self.obj.target) diff --git a/swh/graphql/resolvers/origin.py b/swh/graphql/resolvers/origin.py index a26ba8a..fe9fda8 100644 --- a/swh/graphql/resolvers/origin.py +++ b/swh/graphql/resolvers/origin.py @@ -1,20 +1,20 @@ from swh.graphql.backends import archive from .base_connection import BaseConnection -from .base_node import BaseNode +from .base_node import BaseSWHNode -class OriginNode(BaseNode): +class OriginNode(BaseSWHNode): def _get_node_data(self): return archive.Archive().get_origin(self.kwargs.get("url")) class OriginConnection(BaseConnection): _node_class = OriginNode def _get_paged_result(self): return archive.Archive().get_origins( after=self._get_after_arg(), first=self._get_first_arg(), url_pattern=self.kwargs.get("urlPattern"), ) diff --git a/swh/graphql/resolvers/release.py b/swh/graphql/resolvers/release.py index 5ef3ce7..07b3087 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,45 +1,45 @@ from swh.graphql.backends import archive -from .base_node import BaseNode +from .base_node import BaseSWHNode -class BaseReleaseNode(BaseNode): +class BaseReleaseNode(BaseSWHNode): def _get_release_by_id(self, release_id): return (archive.Archive().get_releases([release_id]) or None)[0] @property def targetId(self): # To support the schema naming convention return self._node.target @property def targetType(self): # To support the schema naming convention return self._node.target_type.value def is_type_of(self): """ is_type_of is required only when resolving a UNION type This is for ariadne to return the right type """ return "Release" class ReleaseNode(BaseReleaseNode): """ When the release is requested directly with an id """ def _get_node_data(self): return self._get_release_by_id(self.kwargs.get("SWHID").object_id) class TargetReleaseNode(BaseReleaseNode): """ When a release is requested as a target self.obj could be a snapshotbranch or a release self.obj.target is the requested release id here """ def _get_node_data(self): return self._get_release_by_id(self.obj.target) diff --git a/swh/graphql/resolvers/revision.py b/swh/graphql/resolvers/revision.py index 7d85ab6..cb545a6 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,92 +1,92 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection -from .base_node import BaseNode +from .base_node import BaseSWHNode -class BaseRevisionNode(BaseNode): +class BaseRevisionNode(BaseSWHNode): def _get_revision_by_id(self, revision_id): return (archive.Archive().get_revisions([revision_id]) or None)[0] @property def parentIds(self): # To support the schema naming convention return self._node.parents @property def directoryId(self): # To support the schema naming convention """ """ return self._node.directory @property def type(self): return self._node.type.value def is_type_of(self): """ is_type_of is required only when resolving a UNION type This is for ariadne to return the right type """ return "Revision" class RevisionNode(BaseRevisionNode): """ When the revision is requested directly with its id (hash) """ def _get_node_data(self): return self._get_revision_by_id(self.kwargs.get("SWHID").object_id) class TargetRevisionNode(BaseRevisionNode): """ When a revision is requested as a target self.obj could be a snapshotbranch or a release self.obj.target is the requested revision id here """ def _get_node_data(self): """ self.obj.target is the Revision id """ return self._get_revision_by_id(self.obj.target) class ParentRevisionConnection(BaseConnection): """ When parent revisions requested from a revision self.obj is the current(child) revision self.obj.parentIds is the list of requested revisions """ _node_class = BaseRevisionNode def _get_paged_result(self): # FIXME, using dummy(local) pagination, move pagination to backend # To remove localpagination, just drop the paginated call # STORAGE-TODO (pagination) parents = archive.Archive().get_revisions(self.obj.parentIds) return utils.paginated(parents, self._get_first_arg(), self._get_after_arg()) class LogRevisionConnection(BaseConnection): """ When revisionslog is requested from a revision self.obj is the current revision id """ _node_class = BaseRevisionNode def _get_paged_result(self): # STORAGE-TODO (date in revisionlog is a dict) log = archive.Archive().get_revision_log([self.obj.id]) # FIXME, using dummy(local) pagination, move pagination to backend # To remove localpagination, just drop the paginated call # STORAGE-TODO (pagination) return utils.paginated(log, self._get_first_arg(), self._get_after_arg()) diff --git a/swh/graphql/resolvers/scalars.py b/swh/graphql/resolvers/scalars.py index 670c83d..e5d5051 100644 --- a/swh/graphql/resolvers/scalars.py +++ b/swh/graphql/resolvers/scalars.py @@ -1,41 +1,50 @@ +from datetime import datetime + from ariadne import ScalarType from swh.graphql.utils import utils -from swh.model.fields.hashes import validate_sha1_git from swh.model.model import TimestampWithTimezone -from swh.model.swhids import QualifiedSWHID +from swh.model.swhids import CoreSWHID datetime_scalar = ScalarType("DateTime") swhid_scalar = ScalarType("SWHID") -sha1_scalar = ScalarType("Sha1") -binary_text_scalar = ScalarType("BinaryText") +hash_value_scalar = ScalarType("HashValue") +binary_string_scalar = ScalarType("BinaryString") +id_scalar = ScalarType("ID") -@sha1_scalar.serializer -def serialize_sha1(value): - return value.hex() +@id_scalar.serializer +def serialize_id(value): + if type(value) is bytes: + return value.hex() + return value -@sha1_scalar.value_parser -def validate_and_get_sha1_git(value): - # FIXME, handle the error and raise a Graphql one - validate_sha1_git(value) - return bytearray.fromhex(value) +@hash_value_scalar.serializer +def serialize_hash_value(value): + return value.hex() -@binary_text_scalar.serializer -def serialize_binary_text(value): +@binary_string_scalar.serializer +def serialize_binary_string(value): return value.decode("utf-8") @datetime_scalar.serializer def serialize_datetime(value): # FIXME, handle error and return None if type(value) == TimestampWithTimezone: value = value.to_datetime() - return utils.get_formatted_date(value) + if type(value) == datetime: + return utils.get_formatted_date(value) + return None @swhid_scalar.value_parser def validate_swhid(value): - return QualifiedSWHID.from_string(value) + return CoreSWHID.from_string(value) + + +@swhid_scalar.serializer +def serialize_swhid(value): + return str(value) diff --git a/swh/graphql/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py index 8ba9aeb..effa686 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,53 +1,53 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from swh.model.model import Snapshot from .base_connection import BaseConnection -from .base_node import BaseNode +from .base_node import BaseSWHNode -class BaseSnapshotNode(BaseNode): +class BaseSnapshotNode(BaseSWHNode): def _get_snapshot_by_id(self, snapshot_id): # Return a Snapshot model object # branches is initialized as empty # Same pattern is used in directory return Snapshot(id=snapshot_id, branches={}) class SnapshotNode(BaseSnapshotNode): """ For directly accessing a snapshot with an Id """ def _get_node_data(self): """ """ snapshot_id = self.kwargs.get("SWHID").object_id if archive.Archive().is_snapshot_available([snapshot_id]): return self._get_snapshot_by_id(snapshot_id) return None class VisitSnapshotNode(BaseSnapshotNode): """ For accessing a snapshot from a visitstatus type """ def _get_node_data(self): """ self.obj is visitstatus here self.obj.snapshot is the requested snapshot id """ return self._get_snapshot_by_id(self.obj.snapshot) class OriginSnapshotConnection(BaseConnection): _node_class = BaseSnapshotNode def _get_paged_result(self): """ """ results = archive.Archive().get_origin_snapshots(self.obj.url) snapshots = [{"id": snapshot} for snapshot in results] # FIXME, using dummy(local) pagination, move pagination to backend # To remove localpagination, just drop the paginated call # STORAGE-TODO return utils.paginated(snapshots, self._get_first_arg(), self._get_after_arg()) diff --git a/swh/graphql/schema/schema.graphql b/swh/graphql/schema/schema.graphql index f8306e7..96b1a7e 100644 --- a/swh/graphql/schema/schema.graphql +++ b/swh/graphql/schema/schema.graphql @@ -1,339 +1,343 @@ scalar SWHID -scalar Sha1 +scalar HashValue scalar DateTime -scalar BinaryText +scalar BinaryString interface Node { id: ID! } interface SWHNode { - id: Sha1! + SWHID: 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: Sha1! +type Origin implements Node { + id: ID! url: String! visits( first: Int! after: String ): VisitConnection! latestVisit: Visit snapshots( first: Int! after: String ): SnapshotConnection } type VisitConnection { edges: [VisitEdge] nodes: [Visit] pageInfo: PageInfo! totalCount: Int } type VisitEdge { cursor: String! node: Visit } type Visit implements Node { id: ID! visitId: Int date: DateTime! type: String status( first: Int after: String ): VisitStatusConnection latestStatus: VisitStatus } type VisitStatusConnection { edges: [VisitStatusEdge] nodes: [VisitStatus] pageInfo: PageInfo! totalCount: Int } type VisitStatusEdge { cursor: String! node: VisitStatus } type VisitStatus { status: String! date: DateTime! - snapshotId: Sha1 + snapshotId: HashValue snapshot: Snapshot type: String } type SnapshotConnection { edges: [SnapshotEdge] nodes: [Snapshot] pageInfo: PageInfo! totalCount: Int } type SnapshotEdge { cursor: String! node: Snapshot } -type Snapshot implements SWHNode { - id: Sha1! +type Snapshot implements SWHNode & Node { + id: ID! + SWHID: SWHID! branches( first: Int! after: String types: [BranchTypes] nameInclude: String ): BranchConnection } type BranchConnection { edges: [BranchConnectionEdge] nodes: [Branch] pageInfo: PageInfo! totalCount: Int } type BranchConnectionEdge { cursor: String! node: Branch } type Person { - email: BinaryText - name: BinaryText - fullname: BinaryText + email: BinaryString + name: BinaryString + fullname: BinaryString } # FIXME, this can be Content, Directory, Snapshot, or Alias as well union BranchTarget = Revision | Release enum BranchTypes { revision release alias content directory snapshot } type Branch { - name: BinaryText + name: BinaryString type: BranchTypes - targetId: Sha1 + targetId: HashValue target: BranchTarget } type RevisionConnection { edges: [RevisionEdge] nodes: [Revision] pageInfo: PageInfo! totalCount: Int } type RevisionEdge { cursor: String! node: Revision } -type Revision implements SWHNode { - id: Sha1! - message: BinaryText +type Revision implements SWHNode & Node { + id: ID! + SWHID: SWHID! + message: BinaryString author: Person committer: Person date: DateTime type: String # Revision type: FIXME, change to an enum - directoryId: Sha1 + directoryId: HashValue directory: Directory - parentIds: [Sha1] + parentIds: [HashValue] parents( first: Int after: String ): RevisionConnection revisionLog( first: Int! after: String ): RevisionConnection } union ReleaseTarget = Release | Revision | Directory | Content enum ReleaseTargetType { release revision content directory } -type Release implements SWHNode { - id: Sha1! - name: BinaryText - message: BinaryText +type Release implements SWHNode & Node { + id: ID! + SWHID: SWHID! + name: BinaryString + message: BinaryString author: Person date: DateTime - targetId: Sha1 + targetId: HashValue targetType: ReleaseTargetType target: ReleaseTarget } type DirectoryEntryConnection { edges: [DirectoryEntryEdge] nodes: [DirectoryEntry] pageInfo: PageInfo! totalCount: Int } type DirectoryEntryEdge { cursor: String! node: DirectoryEntry } union DirectoryEntryTarget = Directory | Content enum DirectoryEntryType { dir file rev } type DirectoryEntry { - name: BinaryText + name: BinaryString type: DirectoryEntryType - targetId: Sha1 + targetId: HashValue target: DirectoryEntryTarget } -type Directory implements SWHNode { - id: Sha1! +type Directory implements SWHNode & Node { + id: ID! + SWHID: SWHID! entries( first: Int after: String ): DirectoryEntryConnection } type ContentChecksum { # FIXME, temp types - blake2s256: Sha1 - sha1: Sha1 - sha1_git: Sha1 - sha256: Sha1 + blake2s256: HashValue + sha1: HashValue + sha1_git: HashValue + sha256: HashValue } # type ContentType { # test: String # } # type ContentLanguage { # test: String # } # type ContentLicense { # test: String # } -type Content implements SWHNode { - id: Sha1! - swhId: String +type Content implements SWHNode & Node { + id: ID! + SWHID: SWHID! checksum: ContentChecksum # data: # filetype: ContentType # language: ContentLanguage # license: ContentLicense length: Int status: String - data: BinaryText + data: BinaryString } type Query { """ Get an origin with its 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 urlPattern: String ): OriginConnection """ Get a visit object with its id and/or origin and visit id """ visit( originUrl: String! visitId: Int! ): Visit """ Get a snapshot with Sha1 """ snapshot( SWHID: SWHID! ): Snapshot """ Get the revision with the given Sha1 """ revision( SWHID: SWHID! ): Revision """ Get the release with the given Sha1 """ release( SWHID: SWHID! ): Release """ Get the directory with the given Sha1 """ directory( SWHID: SWHID! ): Directory """ Get the content with the given Sha1 """ content( SWHID: SWHID! ): Content # """ # Search with the given swhid # """ # searchWithSwhid }