diff --git a/swh/graphql/resolvers/content.py b/swh/graphql/resolvers/content.py index 1f7ae1e..fab042a 100644 --- a/swh/graphql/resolvers/content.py +++ b/swh/graphql/resolvers/content.py @@ -1,54 +1,54 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_node import BaseNode class BaseContentNode(BaseNode): """ """ 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): return self._node.swhid() @property def checksum(self): # FIXME, return a Node object return self._node.hashes() @property def data(self): return def is_type_of(self): return "Content" class ContentNode(BaseContentNode): def _get_node_data(self): """ When a content is requested directly with an id """ - content_id = utils.str_to_swid(self.kwargs.get("SWHID")) + content_id = utils.str_to_sha1(self.kwargs.get("SWHID")) return self._get_content_by_id(content_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 124b04b..2175e72 100644 --- a/swh/graphql/resolvers/directory.py +++ b/swh/graphql/resolvers/directory.py @@ -1,49 +1,49 @@ from swh.graphql.utils import utils from .base_node import BaseNode class BaseDirectoryNode(BaseNode): def _get_directory_by_id(self, directory_id): # Now not fetching any data (schema is exposing just id) # same pattern is used in snapshot resolver # FIXME, use the right API to fetch metadata like name, path return { "id": directory_id, } 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 = utils.str_to_swid(self.kwargs.get("Sha1")) + directory_id = utils.str_to_sha1(self.kwargs.get("Sha1")) # path = "" return self._get_directory_by_id(directory_id) 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/release.py b/swh/graphql/resolvers/release.py index b76883a..c36b08b 100644 --- a/swh/graphql/resolvers/release.py +++ b/swh/graphql/resolvers/release.py @@ -1,47 +1,47 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_node import BaseNode class BaseReleaseNode(BaseNode): 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): - release_id = utils.str_to_swid(self.kwargs.get("Sha1")) + release_id = utils.str_to_sha1(self.kwargs.get("Sha1")) return self._get_release_by_id(release_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 435d1ac..214294b 100644 --- a/swh/graphql/resolvers/revision.py +++ b/swh/graphql/resolvers/revision.py @@ -1,74 +1,74 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode class BaseRevisionNode(BaseNode): 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 an id """ def _get_node_data(self): - revision_id = utils.str_to_swid(self.kwargs.get("Sha1")) + revision_id = utils.str_to_sha1(self.kwargs.get("Sha1")) return self._get_revision_by_id(revision_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 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 parents = archive.Archive().get_revisions(self.obj.parentIds) return utils.paginated(parents, self._get_first_arg(), self._get_after_arg()) diff --git a/swh/graphql/resolvers/snapshot.py b/swh/graphql/resolvers/snapshot.py index 04e0099..018bb9f 100644 --- a/swh/graphql/resolvers/snapshot.py +++ b/swh/graphql/resolvers/snapshot.py @@ -1,36 +1,36 @@ from swh.graphql.utils import utils from .base_node import BaseNode class BaseSnapshotNode(BaseNode): def _get_snapshot_by_id(self, snapshot_id): # Now not fetching any data (schema is exposing just id) # same pattern is used in directory resolver return { "id": snapshot_id, } class SnapshotNode(BaseSnapshotNode): """ For directly accessing a snapshot with an Id """ def _get_node_data(self): """ """ - snapshot_id = utils.str_to_swid(self.kwargs.get("Sha1")) + snapshot_id = utils.str_to_sha1(self.kwargs.get("Sha1")) return self._get_snapshot_by_id(snapshot_id) 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) diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py index b7739c0..f6c1dc9 100644 --- a/swh/graphql/resolvers/visit.py +++ b/swh/graphql/resolvers/visit.py @@ -1,30 +1,30 @@ from swh.graphql.backends import archive from swh.graphql.utils import utils from .base_connection import BaseConnection from .base_node import BaseNode class OriginVisitNode(BaseNode): def _get_node_data(self): 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)}") + return utils.b64encode(f"{self.origin}-{str(self.visit)}") class OriginVisitConnection(BaseConnection): _node_class = OriginVisitNode def _get_paged_result(self): """ Get the visits for the given origin parent obj (self.obj) is origin here """ return archive.Archive().get_origin_visits( self.obj.url, after=self._get_after_arg(), first=self._get_first_arg() ) diff --git a/swh/graphql/tests/unit/utils/test_utils.py b/swh/graphql/tests/unit/utils/test_utils.py new file mode 100644 index 0000000..88f9a3a --- /dev/null +++ b/swh/graphql/tests/unit/utils/test_utils.py @@ -0,0 +1,53 @@ +from swh.graphql.utils import utils + + +class TestUtils: + def test_b64encode(self): + assert utils.b64encode("testing") == "dGVzdGluZw==" + + def test_get_encoded_cursor_is_none(self): + assert utils.get_encoded_cursor(None) is None + + def test_get_encoded_cursor(self): + assert utils.get_encoded_cursor(None) is None + assert utils.get_encoded_cursor("testing") == "dGVzdGluZw==" + + def test_get_decoded_cursor_is_none(self): + assert utils.get_decoded_cursor(None) is None + + def test_get_decoded_cursor(self): + assert utils.get_decoded_cursor("dGVzdGluZw==") == "testing" + + def test_str_to_sha1(self): + assert ( + utils.str_to_sha1("208f61cc7a5dbc9879ae6e5c2f95891e270f09ef") + == b" \x8fa\xccz]\xbc\x98y\xaen\\/\x95\x89\x1e'\x0f\t\xef" + ) + + def test_paginated(self): + source = [1, 2, 3, 4, 5] + response = utils.paginated(source, first=50) + assert response.results == source + assert response.next_page_token is None + + def test_paginated_first_arg(self): + source = [1, 2, 3, 4, 5] + response = utils.paginated(source, first=2) + assert response.results == source[:2] + assert response.next_page_token == "2" + + def test_paginated_after_arg(self): + source = [1, 2, 3, 4, 5] + response = utils.paginated(source, first=2, after="2") + assert response.results == [3, 4] + assert response.next_page_token == "4" + + response = utils.paginated(source, first=2, after="3") + assert response.results == [4, 5] + assert response.next_page_token is None + + def test_paginated_endcursor_outside(self): + source = [1, 2, 3, 4, 5] + response = utils.paginated(source, first=2, after="10") + assert response.results == [] + assert response.next_page_token is None diff --git a/swh/graphql/utils/utils.py b/swh/graphql/utils/utils.py index 4ac5e99..c489ab7 100644 --- a/swh/graphql/utils/utils.py +++ b/swh/graphql/utils/utils.py @@ -1,42 +1,43 @@ import base64 +from typing import List from swh.storage.interface import PagedResult -def encode(text): +def b64encode(text: str) -> str: return base64.b64encode(bytes(text, "utf-8")).decode("utf-8") -def get_encoded_cursor(cursor): +def get_encoded_cursor(cursor: str) -> str: if cursor is None: return None - return base64.b64encode(bytes(cursor, "utf-8")).decode("utf-8") + return b64encode(cursor) -def get_decoded_cursor(cursor): +def get_decoded_cursor(cursor: str) -> str: if cursor is None: return None return base64.b64decode(cursor).decode("utf-8") -def str_to_swid(str_swid): +def str_to_sha1(sha1: str) -> bytearray: # FIXME, use core function - return bytearray.fromhex(str_swid) + return bytearray.fromhex(sha1) -def paginated(source, first, after=0): +def paginated(source: List, first: int, after=0) -> PagedResult: """ Pagination at the GraphQL level This is a temporary fix and inefficient. Should eventually be moved to the backend (storage) level """ # FIXME, handle data errors here after = 0 if after is None else int(after) end_cursor = after + first results = source[after:end_cursor] next_page_token = None if len(source) > end_cursor: next_page_token = str(end_cursor) return PagedResult(results=results, next_page_token=next_page_token)