Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/swh/graphql/app.py b/swh/graphql/app.py
index 326e1b2..21e7865 100644
--- a/swh/graphql/app.py
+++ b/swh/graphql/app.py
@@ -1,40 +1,40 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
# import pkg_resources
import os
from pathlib import Path
from ariadne import gql, load_schema_from_path, make_executable_schema
from .resolvers import resolvers, scalars
type_defs = gql(
# pkg_resources.resource_string("swh.graphql", "schem/schema.graphql").decode()
load_schema_from_path(
os.path.join(Path(__file__).parent.resolve(), "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,
+ resolvers.binary_string,
scalars.id_scalar,
- scalars.string_scalar,
scalars.datetime_scalar,
scalars.swhid_scalar,
)
diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py
index 3675fd3..e52722f 100644
--- a/swh/graphql/resolvers/resolvers.py
+++ b/swh/graphql/resolvers/resolvers.py
@@ -1,245 +1,257 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
"""
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 Node object (eg: resolvers.visit.OriginVisitNode.id)
- As an attribute/item in the object/dict returned by the backend (eg: Origin.url)
"""
from ariadne import ObjectType, UnionType
from graphql.type import GraphQLResolveInfo
from swh.graphql import resolvers as rs
+from swh.graphql.utils import utils
from .resolver_factory import get_connection_resolver, get_node_resolver
query: ObjectType = ObjectType("Query")
origin: ObjectType = ObjectType("Origin")
visit: ObjectType = ObjectType("Visit")
visit_status: ObjectType = ObjectType("VisitStatus")
snapshot: ObjectType = ObjectType("Snapshot")
snapshot_branch: ObjectType = ObjectType("Branch")
revision: ObjectType = ObjectType("Revision")
release: ObjectType = ObjectType("Release")
directory: ObjectType = ObjectType("Directory")
directory_entry: ObjectType = ObjectType("DirectoryEntry")
+binary_string: ObjectType = ObjectType("BinaryString")
branch_target: UnionType = UnionType("BranchTarget")
release_target: UnionType = UnionType("ReleaseTarget")
directory_entry_target: UnionType = UnionType("DirectoryEntryTarget")
# Node resolvers
# A node resolver should return an instance of BaseNode
@query.field("origin")
def origin_resolver(obj: None, info: GraphQLResolveInfo, **kw) -> rs.origin.OriginNode:
""" """
resolver = get_node_resolver("origin")
return resolver(obj, info, **kw)()
@origin.field("latestVisit")
def latest_visit_resolver(
obj: rs.origin.OriginNode, info: GraphQLResolveInfo, **kw
) -> rs.visit.LatestVisitNode:
""" """
resolver = get_node_resolver("latest-visit")
return resolver(obj, info, **kw)()
@query.field("visit")
def visit_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.visit.OriginVisitNode:
""" """
resolver = get_node_resolver("visit")
return resolver(obj, info, **kw)()
@visit.field("latestStatus")
def latest_visit_status_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.visit_status.LatestVisitStatusNode:
""" """
resolver = get_node_resolver("latest-status")
return resolver(obj, info, **kw)()
@query.field("snapshot")
def snapshot_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.snapshot.SnapshotNode:
""" """
resolver = get_node_resolver("snapshot")
return resolver(obj, info, **kw)()
@visit_status.field("snapshot")
def visit_snapshot_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.snapshot.VisitSnapshotNode:
resolver = get_node_resolver("visit-snapshot")
return resolver(obj, info, **kw)()
@snapshot_branch.field("target")
def snapshot_branch_target_resolver(
obj: rs.snapshot_branch.SnapshotBranchNode, info: GraphQLResolveInfo, **kw
):
"""
Snapshot branch target can be a revision or a release
"""
resolver_type = f"branch-{obj.type}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)()
@query.field("revision")
def revision_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.revision.RevisionNode:
resolver = get_node_resolver("revision")
return resolver(obj, info, **kw)()
@revision.field("directory")
def revision_directory_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.directory.RevisionDirectoryNode:
resolver = get_node_resolver("revision-directory")
return resolver(obj, info, **kw)()
@query.field("release")
def release_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.release.ReleaseNode:
resolver = get_node_resolver("release")
return resolver(obj, info, **kw)()
@release.field("target")
def release_target_resolver(obj, info: GraphQLResolveInfo, **kw):
"""
release target can be a release, revision,
directory or content
obj is release here, target type is
obj.target_type
"""
resolver_type = f"release-{obj.target_type.value}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)()
@query.field("directory")
def directory_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.directory.DirectoryNode:
resolver = get_node_resolver("directory")
return resolver(obj, info, **kw)()
@directory_entry.field("target")
def directory_entry_target_resolver(
obj: rs.directory_entry.DirectoryEntryNode, info: GraphQLResolveInfo, **kw
):
"""
directory entry target can be a directory or a content
"""
resolver_type = f"dir-entry-{obj.type}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)()
@query.field("content")
def content_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.content.ContentNode:
resolver = get_node_resolver("content")
return resolver(obj, info, **kw)()
# Connection resolvers
# A connection resolver should return an instance of BaseConnection
@query.field("origins")
def origins_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.origin.OriginConnection:
resolver = get_connection_resolver("origins")
return resolver(obj, info, **kw)()
@origin.field("visits")
def visits_resolver(
obj: rs.origin.OriginNode, info: GraphQLResolveInfo, **kw
) -> rs.visit.OriginVisitConnection:
resolver = get_connection_resolver("origin-visits")
return resolver(obj, info, **kw)()
@origin.field("snapshots")
def origin_snapshots_resolver(
obj: rs.origin.OriginNode, info: GraphQLResolveInfo, **kw
) -> rs.snapshot.OriginSnapshotConnection:
""" """
resolver = get_connection_resolver("origin-snapshots")
return resolver(obj, info, **kw)()
@visit.field("status")
def visitstatus_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.visit_status.VisitStatusConnection:
resolver = get_connection_resolver("visit-status")
return resolver(obj, info, **kw)()
@snapshot.field("branches")
def snapshot_branches_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.snapshot_branch.SnapshotBranchConnection:
resolver = get_connection_resolver("snapshot-branches")
return resolver(obj, info, **kw)()
@revision.field("parents")
def revision_parents_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.revision.ParentRevisionConnection:
resolver = get_connection_resolver("revision-parents")
return resolver(obj, info, **kw)()
# @revision.field("revisionLog")
# def revision_log_resolver(obj, info, **kw):
# resolver = get_connection_resolver("revision-log")
# return resolver(obj, info, **kw)()
@directory.field("entries")
def directory_entry_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.directory_entry.DirectoryEntryConnection:
resolver = get_connection_resolver("directory-entries")
return resolver(obj, info, **kw)()
# Any other type of resolver
@release_target.type_resolver
@directory_entry_target.type_resolver
@branch_target.type_resolver
def union_resolver(obj, *_) -> str:
"""
Generic resolver for all the union types
"""
return obj.is_type_of()
+
+
+@binary_string.field("text")
+def binary_string_text_resolver(obj, *args, **kw):
+ return obj.decode(utils.ENCODING, "replace")
+
+
+@binary_string.field("base64")
+def binary_string_base64_resolver(obj, *args, **kw):
+ return utils.get_b64_string(obj)
diff --git a/swh/graphql/resolvers/scalars.py b/swh/graphql/resolvers/scalars.py
index 5d34594..e083470 100644
--- a/swh/graphql/resolvers/scalars.py
+++ b/swh/graphql/resolvers/scalars.py
@@ -1,51 +1,43 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
from datetime import datetime
from ariadne import ScalarType
from swh.graphql.utils import utils
from swh.model.model import TimestampWithTimezone
from swh.model.swhids import CoreSWHID
datetime_scalar = ScalarType("DateTime")
swhid_scalar = ScalarType("SWHID")
id_scalar = ScalarType("ID")
-string_scalar = ScalarType("String")
@id_scalar.serializer
def serialize_id(value):
if type(value) is bytes:
return value.hex()
return value
-@string_scalar.serializer
-def serialize_string(value):
- if type(value) is bytes:
- return value.decode("utf-8")
- return value
-
-
@datetime_scalar.serializer
def serialize_datetime(value):
# FIXME, handle error and return None
if type(value) == TimestampWithTimezone:
value = value.to_datetime()
if type(value) == datetime:
return utils.get_formatted_date(value)
return None
@swhid_scalar.value_parser
def validate_swhid(value):
return CoreSWHID.from_string(value)
@swhid_scalar.serializer
def serialize_swhid(value):
return str(value)
diff --git a/swh/graphql/resolvers/visit.py b/swh/graphql/resolvers/visit.py
index a2222d1..6979850 100644
--- a/swh/graphql/resolvers/visit.py
+++ b/swh/graphql/resolvers/visit.py
@@ -1,56 +1,56 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.graphql.backends import archive
from swh.graphql.utils import utils
from .base_connection import BaseConnection
from .base_node import BaseNode
class BaseVisitNode(BaseNode):
@property
def id(self):
# FIXME, use a better id
- return utils.b64encode(f"{self.origin}-{str(self.visit)}")
+ return utils.get_b64_string(f"{self.origin}-{str(self.visit)}")
@property
def visitId(self): # To support the schema naming convention
return self._node.visit
class OriginVisitNode(BaseVisitNode):
"""
Get the visit directly with an origin URL and a visit ID
"""
def _get_node_data(self):
return archive.Archive().get_origin_visit(
self.kwargs.get("originUrl"), int(self.kwargs.get("visitId"))
)
class LatestVisitNode(BaseVisitNode):
"""
Get the latest visit for an origin
self.obj is the origin object here
self.obj.url is the origin URL
"""
def _get_node_data(self):
return archive.Archive().get_origin_latest_visit(self.obj.url)
class OriginVisitConnection(BaseConnection):
_node_class = BaseVisitNode
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/schema/schema.graphql b/swh/graphql/schema/schema.graphql
index a83ef4f..e41bb6d 100644
--- a/swh/graphql/schema/schema.graphql
+++ b/swh/graphql/schema/schema.graphql
@@ -1,890 +1,906 @@
"""
SoftWare Heritage persistent Identifier
"""
scalar SWHID
"""
ISO-8601 encoded date string
"""
scalar DateTime
"""
Object with an id
"""
interface Node {
"""
Id of the object. This is for caching purpose and
should not be used outside the GraphQL API
"""
id: ID!
}
"""
SWH merkle node object with a SWHID
"""
interface MerkleNode {
"""
SWHID of the object
"""
swhid: SWHID!
}
"""
Information about pagination
"""
type PageInfo {
"""
Cursor to request the next page in the connection
"""
endCursor: String
"""
Are there more pages in the connection?
"""
hasNextPage: Boolean!
}
+"""
+Binary strings; different encodings
+"""
+type BinaryString {
+ """
+ Utf-8 encoded value, any non Unicode character will be replaced
+ """
+ text: String
+
+ """
+ base64 encoded value
+ """
+ base64: String
+}
+
+
"""
Connection to origins
"""
type OriginConnection {
"""
List of origin edges
"""
edges: [OriginEdge]
"""
List of origin objects
"""
nodes: [Origin]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of origin objects in the connection
"""
totalCount: Int
}
"""
Edge in origin connection
"""
type OriginEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
Origin object
"""
node: Origin
}
"""
A software origin object
"""
type Origin implements Node {
"""
Unique identifier
"""
id: ID!
"""
Origin URL
"""
url: String!
"""
Connection to all the visit objects for the origin
"""
visits(
"""
Returns the first _n_ elements from the list
"""
first: Int!
"""
Returns the page after this cursor
"""
after: String
): VisitConnection!
"""
Latest visit object for the origin
"""
latestVisit: Visit
"""
Connection to all the snapshots for the origin
"""
snapshots(
"""
Returns the first _n_ elements from the list
"""
first: Int!
"""
Returns the page after this cursor
"""
after: String
): SnapshotConnection
}
"""
Connection to origin visits
"""
type VisitConnection {
"""
List of visit edges
"""
edges: [VisitEdge]
"""
List of visit objects
"""
nodes: [Visit]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of visit objects in the connection
"""
totalCount: Int
}
"""
Edge in origin visit connection
"""
type VisitEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
Visit object
"""
node: Visit
}
"""
An origin visit object
"""
type Visit implements Node {
"""
Unique identifier
"""
id: ID!
"""
Visit number for the origin
"""
visitId: Int
"""
Visit date ISO-8601 encoded
"""
date: DateTime!
"""
Type of the origin visited. Eg: git/hg/svn/tar/deb
"""
type: String
"""
Connection to all the status objects for the visit
"""
status(
"""
Returns the first _n_ elements from the list
"""
first: Int
"""
Returns the page after this cursor
"""
after: String
): VisitStatusConnection
"""
Latest status object for the Visit
"""
latestStatus: VisitStatus
}
"""
Connection to visit status
"""
type VisitStatusConnection {
"""
List of visit status edges
"""
edges: [VisitStatusEdge]
"""
List of visit status objects
"""
nodes: [VisitStatus]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of visit status objects in the connection
"""
totalCount: Int
}
"""
Edge in visit status connection
"""
type VisitStatusEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
Visit status object
"""
node: VisitStatus
}
"""
A visit status object
"""
type VisitStatus {
"""
Status string of the visit (either full, partial or ongoing)
"""
status: String!
"""
ISO-8601 encoded date string
"""
date: DateTime!
"""
Snapshot object
"""
snapshot: Snapshot
"""
Type of the origin visited. Eg: git/hg/svn/tar/deb
"""
type: String
}
"""
Connection to snapshots
"""
type SnapshotConnection {
"""
List of snapshot edges
"""
edges: [SnapshotEdge]
"""
List of snapshot objects
"""
nodes: [Snapshot]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of snapshot objects in the connection
"""
totalCount: Int
}
"""
Edge in snapshot connection
"""
type SnapshotEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
Snapshot object
"""
node: Snapshot
}
"""
A snapshot object
"""
type Snapshot implements MerkleNode & Node {
"""
Unique identifier
"""
id: ID!
"""
SWHID of the snapshot object
"""
swhid: SWHID!
"""
Connection to all the snapshot branches
"""
branches(
"""
Returns the first _n_ elements from the list
"""
first: Int!
"""
Returns the page after this cursor
"""
after: String
"""
Filter by branch target types
"""
types: [BranchTargetType]
"""
Filter by branch name
"""
nameInclude: String
): BranchConnection
}
"""
Connection to snapshot branches
"""
type BranchConnection {
"""
List of branch edges
"""
edges: [BranchConnectionEdge]
"""
List of branch objects
"""
nodes: [Branch]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of branch objects in the connection
"""
totalCount: Int
}
"""
Edge in snapshot branch connection
"""
type BranchConnectionEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
Branch object
"""
node: Branch
}
"""
A user object
"""
type Person {
"""
User's email address
"""
- email: String
+ email: BinaryString
"""
User's name
"""
- name: String
+ name: BinaryString
"""
User's full name
"""
- fullname: String
+ fullname: BinaryString
}
"""
Possible branch target objects
"""
union BranchTarget = Revision | Release | Branch | Content | Directory | Snapshot
"""
Possible Branch target types
"""
enum BranchTargetType {
revision
release
alias
content
directory
snapshot
}
"""
A snapshot branch object
"""
type Branch {
"""
Branch name
"""
- name: String
+ name: BinaryString
"""
Type of Branch target
"""
type: BranchTargetType
"""
Branch target object
"""
target: BranchTarget
}
"""
Connection to revisions
"""
type RevisionConnection {
"""
List of revision edges
"""
edges: [RevisionEdge]
"""
List of revision objects
"""
nodes: [Revision]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of revision objects in the connection
"""
totalCount: Int
}
"""
Edge in revision connection
"""
type RevisionEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
Revision object
"""
node: Revision
}
"""
A revision object
"""
type Revision implements MerkleNode & Node {
"""
Unique identifier
"""
id: ID!
"""
SWHID of the revision object
"""
swhid: SWHID!
"""
Message associated to the revision
"""
- message: String
+ message: BinaryString
"""
"""
author: Person
"""
"""
committer: Person
"""
Revision date ISO-8601 encoded
"""
date: DateTime
"""
Type of the revision, eg: git/hg
"""
type: String
"""
The unique directory object that revision points to
"""
directory: Directory
"""
Connection to all the parents of the revision
"""
parents(
"""
Returns the first _n_ elements from the list
"""
first: Int
"""
Returns the page after this cursor
"""
after: String
): RevisionConnection
"""
Connection to all the revisions heading to this one
aka the commit log
"""
revisionLog(
"""
Returns the first _n_ elements from the list
"""
first: Int!
"""
Returns the page after the cursor
"""
after: String
): RevisionConnection
}
"""
Possible release target objects
"""
union ReleaseTarget = Release | Revision | Directory | Content
"""
Possible release target types
"""
enum ReleaseTargetType {
release
revision
content
directory
}
"""
A release object
"""
type Release implements MerkleNode & Node {
"""
Unique identifier
"""
id: ID!
"""
SWHID of the release object
"""
swhid: SWHID!
"""
The name of the release
"""
- name: String
+ name: BinaryString
"""
The message associated to the release
"""
- message: String
+ message: BinaryString
"""
"""
author: Person
"""
Release date ISO-8601 encoded
"""
date: DateTime
"""
Type of release target
"""
targetType: ReleaseTargetType
"""
Release target object
"""
target: ReleaseTarget
}
"""
Connection to directory entries
"""
type DirectoryEntryConnection {
"""
List of directory entry edges
"""
edges: [DirectoryEntryEdge]
"""
List of directory entry objects
"""
nodes: [DirectoryEntry]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of directory entry objects in the connection
"""
totalCount: Int
}
"""
Edge in directory entry connection
"""
type DirectoryEntryEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
Directory entry object
"""
node: DirectoryEntry
}
"""
Possible directory entry target objects
"""
union DirectoryEntryTarget = Directory | Content
"""
Possible directory entry types
"""
enum DirectoryEntryType {
dir
file
rev
}
"""
A directory entry object
"""
type DirectoryEntry {
"""
The directory entry name
"""
- name: String
+ name: BinaryString
"""
Directory entry object type; can be file, dir or rev
"""
type: DirectoryEntryType
"""
Directory entry target object
"""
target: DirectoryEntryTarget
}
"""
A directory object
"""
type Directory implements MerkleNode & Node {
"""
Unique identifier
"""
id: ID!
"""
SWHID of the directory object
"""
swhid: SWHID!
"""
Connection to the directory entries
"""
entries(
"""
Returns the first _n_ elements from the list
"""
first: Int
"""
Returns the page after this cursor
"""
after: String
): DirectoryEntryConnection
}
"""
An object with different checksums
"""
type ContentChecksum {
"""
"""
blake2s256: String
"""
"""
sha1: String
"""
"""
sha1_git: String
"""
"""
sha256: String
}
"""
A content object
"""
type Content implements MerkleNode & Node {
"""
Unique identifier
"""
id: ID!
"""
SWHID of the content object
"""
swhid: SWHID!
"""
Checksums for the content
"""
checksum: ContentChecksum
"""
Length of the content in bytes
"""
length: Int
"""
Content status, visible or hidden
"""
status: String
}
"""
The query root of the GraphQL interface.
"""
type Query {
"""
Get an origin with its url
"""
origin(
"""
URL of the Origin
"""
url: String!
): Origin
"""
Get a Connection to all the origins
"""
origins(
"""
Returns the first _n_ elements from the list
"""
first: Int!
"""
Returns the page after the cursor
"""
after: String
"""
Filter origins with a URL pattern
"""
urlPattern: String
): OriginConnection
"""
Get the visit object with an origin URL and a visit id
"""
visit(
"""
URL of the origin
"""
originUrl: String!
"""
Visit id to get
"""
visitId: Int!
): Visit
"""
Get the snapshot with a SWHID
"""
snapshot(
"""
SWHID of the snapshot object
"""
swhid: SWHID!
): Snapshot
"""
Get the revision with a SWHID
"""
revision(
"""
SWHID of the revision object
"""
swhid: SWHID!
): Revision
"""
Get the release with a SWHID
"""
release(
"""
SWHID of the release object
"""
swhid: SWHID!
): Release
"""
Get the directory with a SWHID
"""
directory(
"""
SWHID of the directory object
"""
swhid: SWHID!
): Directory
"""
Get the content with a SWHID
"""
content(
"""
SWHID of the content object
"""
swhid: SWHID!
): Content
}
diff --git a/swh/graphql/tests/unit/resolvers/test_resolvers.py b/swh/graphql/tests/unit/resolvers/test_resolvers.py
index ff29a8f..6856de6 100644
--- a/swh/graphql/tests/unit/resolvers/test_resolvers.py
+++ b/swh/graphql/tests/unit/resolvers/test_resolvers.py
@@ -1,120 +1,128 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import pytest
from swh.graphql import resolvers
from swh.graphql.resolvers import resolvers as rs
class TestResolvers:
""" """
@pytest.fixture
def dummy_node(self):
return {"test": "test"}
@pytest.mark.parametrize(
"resolver_func, node_cls",
[
(rs.origin_resolver, resolvers.origin.OriginNode),
(rs.visit_resolver, resolvers.visit.OriginVisitNode),
(rs.latest_visit_resolver, resolvers.visit.LatestVisitNode),
(
rs.latest_visit_status_resolver,
resolvers.visit_status.LatestVisitStatusNode,
),
(rs.snapshot_resolver, resolvers.snapshot.SnapshotNode),
(rs.visit_snapshot_resolver, resolvers.snapshot.VisitSnapshotNode),
(rs.revision_resolver, resolvers.revision.RevisionNode),
(rs.revision_directory_resolver, resolvers.directory.RevisionDirectoryNode),
(rs.release_resolver, resolvers.release.ReleaseNode),
(rs.directory_resolver, resolvers.directory.DirectoryNode),
(rs.content_resolver, resolvers.content.ContentNode),
],
)
def test_node_resolver(self, mocker, dummy_node, resolver_func, node_cls):
mock_get = mocker.patch.object(node_cls, "_get_node", return_value=dummy_node)
node_obj = resolver_func(None, None)
# assert the _get_node method is called on the right object
assert isinstance(node_obj, node_cls)
assert mock_get.assert_called
@pytest.mark.parametrize(
"resolver_func, connection_cls",
[
(rs.origins_resolver, resolvers.origin.OriginConnection),
(rs.visits_resolver, resolvers.visit.OriginVisitConnection),
(rs.origin_snapshots_resolver, resolvers.snapshot.OriginSnapshotConnection),
(rs.visitstatus_resolver, resolvers.visit_status.VisitStatusConnection),
(
rs.snapshot_branches_resolver,
resolvers.snapshot_branch.SnapshotBranchConnection,
),
(rs.revision_parents_resolver, resolvers.revision.ParentRevisionConnection),
# (rs.revision_log_resolver, resolvers.revision.LogRevisionConnection),
(
rs.directory_entry_resolver,
resolvers.directory_entry.DirectoryEntryConnection,
),
],
)
def test_connection_resolver(self, resolver_func, connection_cls):
connection_obj = resolver_func(None, None)
# assert the right object is returned
assert isinstance(connection_obj, connection_cls)
@pytest.mark.parametrize(
"branch_type, node_cls",
[
("revision", resolvers.revision.TargetRevisionNode),
("release", resolvers.release.TargetReleaseNode),
],
)
def test_snapshot_branch_target_resolver(
self, mocker, dummy_node, branch_type, node_cls
):
obj = mocker.Mock(type=branch_type)
mock_get = mocker.patch.object(node_cls, "_get_node", return_value=dummy_node)
node_obj = rs.snapshot_branch_target_resolver(obj, None)
assert isinstance(node_obj, node_cls)
assert mock_get.assert_called
@pytest.mark.parametrize(
"target_type, node_cls",
[
("revision", resolvers.revision.TargetRevisionNode),
("release", resolvers.release.TargetReleaseNode),
("directory", resolvers.directory.TargetDirectoryNode),
("content", resolvers.content.TargetContentNode),
],
)
def test_release_target_resolver(self, mocker, dummy_node, target_type, node_cls):
obj = mocker.Mock(target_type=(mocker.Mock(value=target_type)))
mock_get = mocker.patch.object(node_cls, "_get_node", return_value=dummy_node)
node_obj = rs.release_target_resolver(obj, None)
assert isinstance(node_obj, node_cls)
assert mock_get.assert_called
@pytest.mark.parametrize(
"target_type, node_cls",
[
("dir", resolvers.directory.TargetDirectoryNode),
("file", resolvers.content.TargetContentNode),
],
)
def test_directory_entry_target_resolver(
self, mocker, dummy_node, target_type, node_cls
):
obj = mocker.Mock(type=target_type)
mock_get = mocker.patch.object(node_cls, "_get_node", return_value=dummy_node)
node_obj = rs.directory_entry_target_resolver(obj, None)
assert isinstance(node_obj, node_cls)
assert mock_get.assert_called
def test_unit_resolver(self, mocker):
obj = mocker.Mock()
obj.is_type_of.return_value = "test"
assert rs.union_resolver(obj) == "test"
+
+ def test_binary_string_text_resolver(self):
+ text = rs.binary_string_text_resolver(b"test", None)
+ assert text == "test"
+
+ def test_binary_string_base64_resolver(self):
+ b64string = rs.binary_string_base64_resolver(b"test", None)
+ assert b64string == "dGVzdA=="
diff --git a/swh/graphql/tests/unit/utils/test_utils.py b/swh/graphql/tests/unit/utils/test_utils.py
index bc7c5ac..95325cf 100644
--- a/swh/graphql/tests/unit/utils/test_utils.py
+++ b/swh/graphql/tests/unit/utils/test_utils.py
@@ -1,66 +1,69 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import datetime
from swh.graphql.utils import utils
class TestUtils:
- def test_b64encode(self):
- assert utils.b64encode("testing") == "dGVzdGluZw=="
+ def test_get_b64_string(self):
+ assert utils.get_b64_string("testing") == "dGVzdGluZw=="
+
+ def test_get_b64_string_binary(self):
+ assert utils.get_b64_string(b"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_get_formatted_date(self):
date = datetime.datetime(
2015, 8, 4, 22, 26, 14, 804009, tzinfo=datetime.timezone.utc
)
assert utils.get_formatted_date(date) == "2015-08-04T22:26:14.804009+00:00"
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 c6f4094..87984af 100644
--- a/swh/graphql/utils/utils.py
+++ b/swh/graphql/utils/utils.py
@@ -1,54 +1,58 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import base64
from datetime import datetime
from typing import List
from swh.storage.interface import PagedResult
+ENCODING = "utf-8"
-def b64encode(text: str) -> str:
- return base64.b64encode(bytes(text, "utf-8")).decode("utf-8")
+
+def get_b64_string(source) -> str:
+ if type(source) is str:
+ source = source.encode(ENCODING)
+ return base64.b64encode(source).decode("ascii")
def get_encoded_cursor(cursor: str) -> str:
if cursor is None:
return None
- return b64encode(cursor)
+ return get_b64_string(cursor)
def get_decoded_cursor(cursor: str) -> str:
if cursor is None:
return None
- return base64.b64decode(cursor).decode("utf-8")
+ return base64.b64decode(cursor).decode(ENCODING)
def str_to_sha1(sha1: str) -> bytearray:
# FIXME, use core function
return bytearray.fromhex(sha1)
def get_formatted_date(date: datetime) -> str:
# FIXME, handle error + return other formats
return date.isoformat()
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)

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 3, 10:33 AM (2 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3262770

Event Timeline