Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/swh/graphql/backends/archive.py b/swh/graphql/backends/archive.py
index c253232..738b340 100644
--- a/swh/graphql/backends/archive.py
+++ b/swh/graphql/backends/archive.py
@@ -1,79 +1,91 @@
# 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 os
+from typing import Any, Dict, Optional
+
from swh.graphql import server
+from swh.model.model import Sha1Git
from swh.model.swhids import ObjectType
class Archive:
def __init__(self):
self.storage = server.get_storage()
def get_origin(self, url):
return self.storage.origin_get([url])[0]
def get_origins(self, after=None, first=50):
return self.storage.origin_list(page_token=after, limit=first)
def get_origin_visits(self, origin_url, after=None, first=50):
return self.storage.origin_visit_get(origin_url, page_token=after, limit=first)
def get_origin_visit(self, origin_url, visit_id):
return self.storage.origin_visit_get_by(origin_url, visit_id)
def get_origin_latest_visit(self, origin_url):
return self.storage.origin_visit_get_latest(origin_url)
def get_visit_status(self, origin_url, visit_id, after=None, first=50):
return self.storage.origin_visit_status_get(
origin_url, visit_id, page_token=after, limit=first
)
def get_latest_visit_status(self, origin_url, visit_id):
return self.storage.origin_visit_status_get_latest(origin_url, visit_id)
def get_origin_snapshots(self, origin_url):
return self.storage.origin_snapshot_get_all(origin_url)
def get_snapshot_branches(
self, snapshot, after=b"", first=50, target_types=None, name_include=None
):
return self.storage.snapshot_get_branches(
snapshot,
branches_from=after,
branches_count=first,
target_types=target_types,
branch_name_include_substring=name_include,
)
def get_revisions(self, revision_ids):
return self.storage.revision_get(revision_ids=revision_ids)
def get_revision_log(self, revision_ids, after=None, first=50):
return self.storage.revision_log(revisions=revision_ids, limit=first)
def get_releases(self, release_ids):
return self.storage.release_get(releases=release_ids)
+ def get_directory_entry_by_path(
+ self, directory_id: Sha1Git, path: str
+ ) -> Optional[Dict[str, Any]]:
+ paths = [x.encode() for x in path.strip(os.path.sep).split(os.path.sep)]
+ return self.storage.directory_entry_get_by_path(
+ directory=directory_id, paths=paths
+ )
+
def get_directory_entries(self, directory_id, after=None, first=50):
return self.storage.directory_get_entries(
directory_id, limit=first, page_token=after
)
def is_object_available(self, object_id: str, object_type: ObjectType) -> bool:
mapping = {
ObjectType.CONTENT: self.storage.content_missing_per_sha1_git,
ObjectType.DIRECTORY: self.storage.directory_missing,
ObjectType.RELEASE: self.storage.release_missing,
ObjectType.REVISION: self.storage.revision_missing,
ObjectType.SNAPSHOT: self.storage.snapshot_missing,
}
return not list(mapping[object_type]([object_id]))
def get_contents(self, checksums: dict):
return self.storage.content_find(checksums)
def get_content_data(self, content_sha1):
return self.storage.content_get_data(content_sha1)
diff --git a/swh/graphql/resolvers/content.py b/swh/graphql/resolvers/content.py
index e5335b2..71e91ca 100644
--- a/swh/graphql/resolvers/content.py
+++ b/swh/graphql/resolvers/content.py
@@ -1,93 +1,93 @@
# 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 typing import Union
from .base_node import BaseSWHNode
-from .directory_entry import DirectoryEntryNode
+from .directory_entry import BaseDirectoryEntryNode
from .release import BaseReleaseNode
from .snapshot_branch import BaseSnapshotBranchNode
class BaseContentNode(BaseSWHNode):
"""
Base resolver for all the content nodes
"""
def _get_content_by_hash(self, checksums: dict):
content = self.archive.get_contents(checksums)
# in case of a conflict, return the first element
return content[0] if content else None
@property
def checksum(self):
# FIXME, use a Node instead
return {k: v.hex() for (k, v) in self._node.hashes().items()}
@property
def id(self):
return self._node.sha1_git
@property
def data(self):
# FIXME, return a Node object
# FIXME, add more ways to retrieve data like binary string
archive_url = "https://archive.softwareheritage.org/api/1/"
content_sha1 = self._node.hashes()["sha1"]
return {
"url": f"{archive_url}content/sha1:{content_sha1.hex()}/raw/",
}
@property
def fileType(self):
# FIXME, fetch data from the indexers
return None
@property
def language(self):
# FIXME, fetch data from the indexers
return None
@property
def license(self):
# FIXME, fetch data from the indexers
return None
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 "Content"
class ContentNode(BaseContentNode):
"""
Node resolver for a content requested directly with its SWHID
"""
def _get_node_data(self):
checksums = {"sha1_git": self.kwargs.get("swhid").object_id}
return self._get_content_by_hash(checksums)
class HashContentNode(BaseContentNode):
"""
Node resolver for a content requested with one or more checksums
"""
def _get_node_data(self):
checksums = dict(self.kwargs.get("checksums"))
return self._get_content_by_hash(checksums)
class TargetContentNode(BaseContentNode):
"""
Node resolver for a content requested as a target
This request could be from directory entry, release or a branch
"""
- obj: Union[DirectoryEntryNode, BaseReleaseNode, BaseSnapshotBranchNode]
+ obj: Union[BaseDirectoryEntryNode, BaseReleaseNode, BaseSnapshotBranchNode]
def _get_node_data(self):
return self._get_content_by_hash(checksums={"sha1_git": self.obj.target_hash})
diff --git a/swh/graphql/resolvers/directory.py b/swh/graphql/resolvers/directory.py
index fc07520..1776ae6 100644
--- a/swh/graphql/resolvers/directory.py
+++ b/swh/graphql/resolvers/directory.py
@@ -1,72 +1,71 @@
# 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 typing import Union
from swh.model.model import Directory
from swh.model.swhids import ObjectType
from .base_node import BaseSWHNode
from .release import BaseReleaseNode
from .revision import BaseRevisionNode
from .snapshot_branch import BaseSnapshotBranchNode
class BaseDirectoryNode(BaseSWHNode):
"""
Base resolver for all the directory nodes
"""
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):
"""
Node resolver for a directory requested directly with its SWHID
"""
def _get_node_data(self):
swhid = self.kwargs.get("swhid")
- # path = ""
if (
swhid.object_type == ObjectType.DIRECTORY
and self.archive.is_object_available(swhid.object_id, swhid.object_type)
):
# _get_directory_by_id is not making any backend call
# hence the is_directory_available validation
return self._get_directory_by_id(swhid.object_id)
return None
class RevisionDirectoryNode(BaseDirectoryNode):
"""
Node resolver for a directory requested from a revision
"""
obj: BaseRevisionNode
def _get_node_data(self):
# self.obj.directory_hash is the requested directory Id
return self._get_directory_by_id(self.obj.directory_hash)
class TargetDirectoryNode(BaseDirectoryNode):
"""
Node resolver for a directory requested as a target
"""
- from .directory_entry import DirectoryEntryNode
+ from .directory_entry import BaseDirectoryEntryNode
- obj: Union[BaseSnapshotBranchNode, BaseReleaseNode, DirectoryEntryNode]
+ obj: Union[BaseSnapshotBranchNode, BaseReleaseNode, BaseDirectoryEntryNode]
def _get_node_data(self):
return self._get_directory_by_id(self.obj.target_hash)
diff --git a/swh/graphql/resolvers/directory_entry.py b/swh/graphql/resolvers/directory_entry.py
index e1aea34..b19ddf2 100644
--- a/swh/graphql/resolvers/directory_entry.py
+++ b/swh/graphql/resolvers/directory_entry.py
@@ -1,43 +1,54 @@
# 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.utils import utils
from swh.storage.interface import PagedResult
from .base_connection import BaseConnection
from .base_node import BaseNode
-class DirectoryEntryNode(BaseNode):
- """
- Node resolver for a directory entry
- """
-
+class BaseDirectoryEntryNode(BaseNode):
@property
def target_hash(self): # for DirectoryNode
return self._node.target
@property
def targetType(self): # To support the schema naming convention
return self._node.type
+class DirectoryEntryNode(BaseDirectoryEntryNode):
+ """
+ Node resolver for a directory entry requested with a
+ directory SWHID and a relative path
+ """
+
+ def _get_node_data(self):
+ # STORAGE-TODO, archive is returning a dict
+ # return DirectoryEntry object instead
+ return self.archive.get_directory_entry_by_path(
+ directory_id=self.kwargs.get("swhid").object_id,
+ path=self.kwargs.get("path"),
+ )
+
+
class DirectoryEntryConnection(BaseConnection):
"""
Connection resolver for entries in a directory
"""
from .directory import BaseDirectoryNode
obj: BaseDirectoryNode
- _node_class = DirectoryEntryNode
+ _node_class = BaseDirectoryEntryNode
def _get_paged_result(self) -> PagedResult:
# FIXME, using dummy(local) pagination, move pagination to backend
# To remove localpagination, just drop the paginated call
# STORAGE-TODO
entries = self.archive.get_directory_entries(self.obj.swhid.object_id).results
return utils.paginated(entries, self._get_first_arg(), self._get_after_arg())
diff --git a/swh/graphql/resolvers/resolver_factory.py b/swh/graphql/resolvers/resolver_factory.py
index 438bc5b..fb85b22 100644
--- a/swh/graphql/resolvers/resolver_factory.py
+++ b/swh/graphql/resolvers/resolver_factory.py
@@ -1,84 +1,85 @@
# 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 .content import ContentNode, HashContentNode, TargetContentNode
from .directory import DirectoryNode, RevisionDirectoryNode, TargetDirectoryNode
-from .directory_entry import DirectoryEntryConnection
+from .directory_entry import DirectoryEntryConnection, DirectoryEntryNode
from .origin import OriginConnection, OriginNode, TargetOriginNode
from .release import ReleaseNode, TargetReleaseNode
from .revision import (
LogRevisionConnection,
ParentRevisionConnection,
RevisionNode,
TargetRevisionNode,
)
from .search import ResolveSwhidConnection, SearchConnection
from .snapshot import (
OriginSnapshotConnection,
SnapshotNode,
TargetSnapshotNode,
VisitSnapshotNode,
)
from .snapshot_branch import AliasSnapshotBranchNode, SnapshotBranchConnection
from .visit import LatestVisitNode, OriginVisitConnection, OriginVisitNode
from .visit_status import LatestVisitStatusNode, VisitStatusConnection
def get_node_resolver(resolver_type):
# FIXME, replace with a proper factory method
mapping = {
"origin": OriginNode,
"visit": OriginVisitNode,
"latest-visit": LatestVisitNode,
"latest-status": LatestVisitStatusNode,
"visit-snapshot": VisitSnapshotNode,
"snapshot": SnapshotNode,
"branch-alias": AliasSnapshotBranchNode,
"branch-revision": TargetRevisionNode,
"branch-release": TargetReleaseNode,
"branch-directory": TargetDirectoryNode,
"branch-content": TargetContentNode,
"branch-snapshot": TargetSnapshotNode,
"revision": RevisionNode,
"revision-directory": RevisionDirectoryNode,
"release": ReleaseNode,
"release-revision": TargetRevisionNode,
"release-release": TargetReleaseNode,
"release-directory": TargetDirectoryNode,
"release-content": TargetContentNode,
"directory": DirectoryNode,
+ "directory-entry": DirectoryEntryNode,
"content": ContentNode,
"content-by-hash": HashContentNode,
"dir-entry-dir": TargetDirectoryNode,
"dir-entry-file": TargetContentNode,
"search-result-origin": TargetOriginNode,
"search-result-snapshot": TargetSnapshotNode,
"search-result-revision": TargetRevisionNode,
"search-result-release": TargetReleaseNode,
"search-result-directory": TargetDirectoryNode,
"search-result-content": TargetContentNode,
}
if resolver_type not in mapping:
raise AttributeError(f"Invalid node type: {resolver_type}")
return mapping[resolver_type]
def get_connection_resolver(resolver_type):
# FIXME, replace with a proper factory method
mapping = {
"origins": OriginConnection,
"origin-visits": OriginVisitConnection,
"origin-snapshots": OriginSnapshotConnection,
"visit-status": VisitStatusConnection,
"snapshot-branches": SnapshotBranchConnection,
"revision-parents": ParentRevisionConnection,
"revision-log": LogRevisionConnection,
"directory-entries": DirectoryEntryConnection,
"resolve-swhid": ResolveSwhidConnection,
"search": SearchConnection,
}
if resolver_type not in mapping:
raise AttributeError(f"Invalid connection type: {resolver_type}")
return mapping[resolver_type]
diff --git a/swh/graphql/resolvers/resolvers.py b/swh/graphql/resolvers/resolvers.py
index 2be89f2..60992bb 100644
--- a/swh/graphql/resolvers/resolvers.py
+++ b/swh/graphql/resolvers/resolvers.py
@@ -1,299 +1,307 @@
# 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 a decorator (eg: @visitstatus.field("snapshot")
# Every object (type) is expected to resolve this way as they can accept arguments
# eg: origin.visits takes arguments to paginate
# - As a property in the Node object (eg: resolvers.visit.BaseVisitNode.id)
# Every scalar is expected to resolve this way
# - As an attribute/item in the object/dict returned by a 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")
search_result: ObjectType = ObjectType("SearchResult")
binary_string: ObjectType = ObjectType("BinaryString")
branch_target: UnionType = UnionType("BranchTarget")
release_target: UnionType = UnionType("ReleaseTarget")
directory_entry_target: UnionType = UnionType("DirectoryEntryTarget")
search_result_target: UnionType = UnionType("SearchResultTarget")
# 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.BaseSnapshotBranchNode, info: GraphQLResolveInfo, **kw
):
"""
Snapshot branch target can be a revision, release, directory,
content, snapshot or a branch itself (alias type)
"""
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)
+@query.field("directoryEntry")
+def directory_entry_resolver(
+ obj: None, info: GraphQLResolveInfo, **kw
+) -> rs.directory.DirectoryNode:
+ resolver = get_node_resolver("directory-entry")
+ return resolver(obj, info, **kw)
+
+
@directory_entry.field("target")
def directory_entry_target_resolver(
- obj: rs.directory_entry.DirectoryEntryNode, info: GraphQLResolveInfo, **kw
+ obj: rs.directory_entry.BaseDirectoryEntryNode, 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)
@search_result.field("target")
def search_result_target_resolver(
obj: rs.search.SearchResultNode, info: GraphQLResolveInfo, **kw
):
resolver_type = f"search-result-{obj.type}"
resolver = get_node_resolver(resolver_type)
return resolver(obj, info, **kw)
@query.field("contentByHash")
def content_by_hash_resolver(
obj: None, info: GraphQLResolveInfo, **kw
) -> rs.content.ContentNode:
resolver = get_node_resolver("content-by-hash")
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(
+def directory_entries_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.directory_entry.DirectoryEntryConnection:
resolver = get_connection_resolver("directory-entries")
return resolver(obj, info, **kw)
@query.field("resolveSwhid")
def search_swhid_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.search.ResolveSwhidConnection:
resolver = get_connection_resolver("resolve-swhid")
return resolver(obj, info, **kw)
@query.field("search")
def search_resolver(
obj, info: GraphQLResolveInfo, **kw
) -> rs.search.ResolveSwhidConnection:
resolver = get_connection_resolver("search")
return resolver(obj, info, **kw)
# Any other type of resolver
@release_target.type_resolver
@directory_entry_target.type_resolver
@branch_target.type_resolver
@search_result_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/schema/schema.graphql b/swh/graphql/schema/schema.graphql
index 0d61a85..8ae62f1 100644
--- a/swh/graphql/schema/schema.graphql
+++ b/swh/graphql/schema/schema.graphql
@@ -1,1063 +1,1078 @@
"""
SoftWare Heritage persistent Identifier
"""
scalar SWHID
"""
ISO-8601 encoded date string
"""
scalar DateTime
"""
Content identifier in the form hash-type:hash-value
"""
scalar ContentHash
"""
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: BinaryString
"""
User's name
"""
name: BinaryString
"""
User's full name
"""
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: BinaryString
"""
Type of Branch target
"""
targetType: 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: 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: BinaryString
"""
The message associated to the release
"""
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 DirectoryEntryTargetType {
dir
file
rev
}
"""
A directory entry object
"""
type DirectoryEntry {
"""
The directory entry name
"""
name: BinaryString
"""
Directory entry object type; can be file, dir or rev
"""
targetType: DirectoryEntryTargetType
"""
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 content checksums
"""
type ContentChecksum {
blake2s256: String
sha1: String
sha1_git: String
sha256: String
}
"""
Object with different content data representations
"""
type ContentData {
"""
URL to download the file data
"""
url: String
}
type ContentFileType {
"""
Detected content encoding
"""
encoding: String
"""
Detected MIME type of the content
"""
mimetype: String
}
type ContentLanguage {
"""
Detected programming language if any
"""
lang: String
}
type ContentLicense {
"""
Array of strings containing the detected license names
"""
licenses: [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
"""
File content
"""
data: ContentData
"""
Information about the content MIME type
"""
fileType: ContentFileType
"""
Information about the programming language used in the content
"""
language: ContentLanguage
"""
Information about the license of the content
"""
license: ContentLicense
}
"""
Connection to SearchResults
"""
type SearchResultConnection {
"""
List of SearchResult edges
"""
edges: [SearchResultEdge]
"""
List of SearchResult objects
"""
nodes: [SearchResult]
"""
Information for pagination
"""
pageInfo: PageInfo!
"""
Total number of result objects in the connection
"""
totalCount: Int
}
"""
Edge in SearchResult connection
"""
type SearchResultEdge {
"""
Cursor to request the next page after the item
"""
cursor: String!
"""
SearchResult object
"""
node: SearchResult
}
union SearchResultTarget = Origin | Revision | Release | Content | Directory | Snapshot
enum SearchResultTargetType {
origin
revision
release
content
directory
snapshot
}
"""
A SearchResult object
"""
type SearchResult {
"""
Result target type
"""
targetType: SearchResultTargetType
"""
Result target object
"""
target: SearchResultTarget
}
"""
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 a directory entry with directory SWHID and a path
+ """
+ directoryEntry(
+ """
+ SWHID of the directory object
+ """
+ swhid: SWHID!
+
+ """
+ Relative path to the requested object
+ """
+ path: String!
+ ): DirectoryEntry
+
"""
Get the content with a SWHID
"""
content(
"""
SWHID of the content object
"""
swhid: SWHID!
): Content
"""
Get the content by one or more hashes
Use multiple hashes for an accurate result
"""
contentByHash(
"""
List of hashType:hashValue strings
"""
checksums: [ContentHash]!
): Content
"""
Resolve the given SWHID to an object
"""
resolveSwhid(
"""
SWHID to look for
"""
swhid: SWHID!
): SearchResultConnection!
"""
Search in SWH
"""
search(
"""
String to search for
"""
query: String!
"""
Returns the first _n_ elements from the list
"""
first: Int!
"""
Returns the page after the cursor
"""
after: String
): SearchResultConnection!
}
diff --git a/swh/graphql/tests/data.py b/swh/graphql/tests/data.py
index beed8b6..1c9f983 100644
--- a/swh/graphql/tests/data.py
+++ b/swh/graphql/tests/data.py
@@ -1,107 +1,130 @@
# 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.model.model import ObjectType, Release, Revision, RevisionType
+from swh.model.model import (
+ Directory,
+ DirectoryEntry,
+ ObjectType,
+ Release,
+ Revision,
+ RevisionType,
+)
from swh.model.tests import swh_model_data
def populate_search_data(search):
search.origin_update({"url": origin.url} for origin in get_origins())
def get_origins():
return swh_model_data.ORIGINS
def get_snapshots():
return swh_model_data.SNAPSHOTS
def get_releases():
return swh_model_data.RELEASES
def get_revisions():
return swh_model_data.REVISIONS
def get_contents():
return swh_model_data.CONTENTS
def get_directories():
return swh_model_data.DIRECTORIES
def get_releases_with_target():
"""
GraphQL will not return a target object unless the target id
is present in the DB.
Return release objects with real targets instead of dummy
targets in swh.model.tests.swh_model_data
"""
with_revision = Release(
name=b"v0.0.1",
target_type=ObjectType.REVISION,
target=get_revisions()[0].id,
message=b"foo",
synthetic=False,
)
with_release = Release(
name=b"v0.0.1",
target_type=ObjectType.RELEASE,
target=get_releases()[0].id,
message=b"foo",
synthetic=False,
)
with_directory = Release(
name=b"v0.0.1",
target_type=ObjectType.DIRECTORY,
target=get_directories()[0].id,
message=b"foo",
synthetic=False,
)
with_content = Release(
name=b"v0.0.1",
target_type=ObjectType.CONTENT,
target=get_contents()[0].sha1_git,
message=b"foo",
synthetic=False,
)
return [with_revision, with_release, with_directory, with_content]
def get_revisions_with_parents():
"""
Revisions with real revisions as parents
"""
return [
Revision(
message=b"hello",
date=swh_model_data.DATES[0],
committer=swh_model_data.COMMITTERS[0],
author=swh_model_data.COMMITTERS[0],
committer_date=swh_model_data.DATES[0],
type=RevisionType.GIT,
directory=b"\x01" * 20,
synthetic=False,
parents=(get_revisions()[0].id, get_revisions()[1].id),
)
]
+def get_directories_with_nested_path():
+ return [
+ Directory(
+ entries=(
+ DirectoryEntry(
+ name=b"sub-dir",
+ perms=0o644,
+ type="dir",
+ target=get_directories()[1].id,
+ ),
+ )
+ )
+ ]
+
+
GRAPHQL_EXTRA_TEST_OBJECTS = {
"release": get_releases_with_target(),
"revision": get_revisions_with_parents(),
+ "directory": get_directories_with_nested_path(),
}
def populate_dummy_data(storage):
for object_type, objects in swh_model_data.TEST_OBJECTS.items():
method = getattr(storage, object_type + "_add")
method(objects)
for object_type, objects in GRAPHQL_EXTRA_TEST_OBJECTS.items():
method = getattr(storage, object_type + "_add")
method(objects)
diff --git a/swh/graphql/tests/functional/test_directory_entry.py b/swh/graphql/tests/functional/test_directory_entry.py
index 411435a..05c286d 100644
--- a/swh/graphql/tests/functional/test_directory_entry.py
+++ b/swh/graphql/tests/functional/test_directory_entry.py
@@ -1,37 +1,115 @@
# 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 server
+from swh.model.swhids import CoreSWHID, ObjectType
+
from . import utils
-from ..data import get_directories
+from ..data import get_directories, get_directories_with_nested_path
+
+
+def test_get_directory_entry_missing_path(client):
+ directory = get_directories()[0]
+ path = "missing"
+ query_str = """
+ {
+ directoryEntry(swhid: "%s", path: "%s") {
+ name {
+ text
+ }
+ targetType
+ target {
+ ...on Content {
+ swhid
+ }
+ }
+ }
+ }
+ """ % (
+ directory.swhid(),
+ path,
+ )
+ utils.assert_missing_object(client, query_str, "directoryEntry")
+
+
+@pytest.mark.parametrize(
+ "directory", get_directories() + get_directories_with_nested_path()
+)
+def test_get_directory_entry(client, directory):
+ storage = server.get_storage()
+ query_str = """
+ {
+ directoryEntry(swhid: "%s", path: "%s") {
+ name {
+ text
+ }
+ targetType
+ target {
+ ...on Content {
+ swhid
+ }
+ ...on Directory {
+ swhid
+ }
+ }
+ }
+ }
+ """
+ for entry in storage.directory_ls(directory.id, recursive=True):
+ if entry["type"] == "rev":
+ # FIXME, Revision is not supported as a directory entry target yet
+ continue
+ query = query_str % (
+ directory.swhid(),
+ entry["name"].decode(),
+ )
+ data, _ = utils.get_query_response(
+ client,
+ query,
+ )
+ swhid = None
+ if entry["type"] == "file" and entry["sha1_git"] is not None:
+ swhid = CoreSWHID(
+ object_type=ObjectType.CONTENT, object_id=entry["sha1_git"]
+ )
+ elif entry["type"] == "dir" and entry["target"] is not None:
+ swhid = CoreSWHID(
+ object_type=ObjectType.DIRECTORY, object_id=entry["target"]
+ )
+ assert data["directoryEntry"] == {
+ "name": {"text": entry["name"].decode()},
+ "target": {"swhid": str(swhid)} if swhid else None,
+ "targetType": entry["type"],
+ }
@pytest.mark.parametrize("directory", get_directories())
def test_get_directory_entry_connection(client, directory):
query_str = """
{
directory(swhid: "%s") {
swhid
entries {
nodes {
targetType
name {
text
}
}
}
}
}
"""
data, _ = utils.get_query_response(client, query_str % directory.swhid())
directory_entries = data["directory"]["entries"]["nodes"]
assert len(directory_entries) == len(directory.entries)
output = [
{"name": {"text": de.name.decode()}, "targetType": de.type}
for de in directory.entries
]
for each_entry in output:
assert each_entry in directory_entries
diff --git a/swh/graphql/tests/unit/resolvers/test_resolvers.py b/swh/graphql/tests/unit/resolvers/test_resolvers.py
index 1093cef..99fc127 100644
--- a/swh/graphql/tests/unit/resolvers/test_resolvers.py
+++ b/swh/graphql/tests/unit/resolvers/test_resolvers.py
@@ -1,131 +1,131 @@
# 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,
+ rs.directory_entries_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),
("directory", resolvers.directory.TargetDirectoryNode),
("content", resolvers.content.TargetContentNode),
("snapshot", resolvers.snapshot.TargetSnapshotNode),
],
)
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_union_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=="

File Metadata

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

Event Timeline