Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9311827
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
36 KB
Subscribers
None
View Options
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
Details
Attached
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
Attached To
rDGQL GraphQL API
Event Timeline
Log In to Comment