2025-12-01
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
from .build_client_schema import build_client_schema
|
||||
from .get_introspection_query_ast import get_introspection_query_ast
|
||||
from .node_tree import node_tree
|
||||
from .parse_result import parse_result
|
||||
from .serialize_variable_values import serialize_value, serialize_variable_values
|
||||
from .update_schema_enum import update_schema_enum
|
||||
from .update_schema_scalars import update_schema_scalar, update_schema_scalars
|
||||
|
||||
__all__ = [
|
||||
"build_client_schema",
|
||||
"node_tree",
|
||||
"parse_result",
|
||||
"get_introspection_query_ast",
|
||||
"serialize_variable_values",
|
||||
"serialize_value",
|
||||
"update_schema_enum",
|
||||
"update_schema_scalars",
|
||||
"update_schema_scalar",
|
||||
]
|
||||
@@ -0,0 +1,98 @@
|
||||
from graphql import GraphQLSchema, IntrospectionQuery
|
||||
from graphql import build_client_schema as build_client_schema_orig
|
||||
from graphql.pyutils import inspect
|
||||
from graphql.utilities.get_introspection_query import (
|
||||
DirectiveLocation,
|
||||
IntrospectionDirective,
|
||||
)
|
||||
|
||||
__all__ = ["build_client_schema"]
|
||||
|
||||
|
||||
INCLUDE_DIRECTIVE_JSON: IntrospectionDirective = {
|
||||
"name": "include",
|
||||
"description": (
|
||||
"Directs the executor to include this field or fragment "
|
||||
"only when the `if` argument is true."
|
||||
),
|
||||
"locations": [
|
||||
DirectiveLocation.FIELD,
|
||||
DirectiveLocation.FRAGMENT_SPREAD,
|
||||
DirectiveLocation.INLINE_FRAGMENT,
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "if",
|
||||
"description": "Included when true.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": "None",
|
||||
"ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": "None"},
|
||||
},
|
||||
"defaultValue": "None",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
SKIP_DIRECTIVE_JSON: IntrospectionDirective = {
|
||||
"name": "skip",
|
||||
"description": (
|
||||
"Directs the executor to skip this field or fragment "
|
||||
"when the `if` argument is true."
|
||||
),
|
||||
"locations": [
|
||||
DirectiveLocation.FIELD,
|
||||
DirectiveLocation.FRAGMENT_SPREAD,
|
||||
DirectiveLocation.INLINE_FRAGMENT,
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "if",
|
||||
"description": "Skipped when true.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": "None",
|
||||
"ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": "None"},
|
||||
},
|
||||
"defaultValue": "None",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_client_schema(introspection: IntrospectionQuery) -> GraphQLSchema:
|
||||
"""This is an alternative to the graphql-core function
|
||||
:code:`build_client_schema` but with default include and skip directives
|
||||
added to the schema to fix
|
||||
`issue #278 <https://github.com/graphql-python/gql/issues/278>`_
|
||||
|
||||
.. warning::
|
||||
This function will be removed once the issue
|
||||
`graphql-js#3419 <https://github.com/graphql/graphql-js/issues/3419>`_
|
||||
has been fixed and ported to graphql-core so don't use it
|
||||
outside gql.
|
||||
"""
|
||||
|
||||
if not isinstance(introspection, dict) or not isinstance(
|
||||
introspection.get("__schema"), dict
|
||||
):
|
||||
raise TypeError(
|
||||
"Invalid or incomplete introspection result. Ensure that you"
|
||||
" are passing the 'data' attribute of an introspection response"
|
||||
f" and no 'errors' were returned alongside: {inspect(introspection)}."
|
||||
)
|
||||
|
||||
schema_introspection = introspection["__schema"]
|
||||
|
||||
directives = schema_introspection.get("directives", None)
|
||||
|
||||
if directives is None:
|
||||
schema_introspection["directives"] = directives = []
|
||||
|
||||
if not any(directive["name"] == "skip" for directive in directives):
|
||||
directives.append(SKIP_DIRECTIVE_JSON)
|
||||
|
||||
if not any(directive["name"] == "include" for directive in directives):
|
||||
directives.append(INCLUDE_DIRECTIVE_JSON)
|
||||
|
||||
return build_client_schema_orig(introspection, assume_valid=False)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
from itertools import repeat
|
||||
|
||||
from graphql import DocumentNode, GraphQLSchema
|
||||
|
||||
from gql.dsl import DSLFragment, DSLMetaField, DSLQuery, DSLSchema, dsl_gql
|
||||
|
||||
|
||||
def get_introspection_query_ast(
|
||||
descriptions: bool = True,
|
||||
specified_by_url: bool = False,
|
||||
directive_is_repeatable: bool = False,
|
||||
schema_description: bool = False,
|
||||
input_value_deprecation: bool = False,
|
||||
type_recursion_level: int = 7,
|
||||
) -> DocumentNode:
|
||||
"""Get a query for introspection as a document using the DSL module.
|
||||
|
||||
Equivalent to the get_introspection_query function from graphql-core
|
||||
but using the DSL module and allowing to select the recursion level.
|
||||
|
||||
Optionally, you can exclude descriptions, include specification URLs,
|
||||
include repeatability of directives, and specify whether to include
|
||||
the schema description as well.
|
||||
"""
|
||||
|
||||
ds = DSLSchema(GraphQLSchema())
|
||||
|
||||
fragment_FullType = DSLFragment("FullType").on(ds.__Type)
|
||||
fragment_InputValue = DSLFragment("InputValue").on(ds.__InputValue)
|
||||
fragment_TypeRef = DSLFragment("TypeRef").on(ds.__Type)
|
||||
|
||||
schema = DSLMetaField("__schema")
|
||||
|
||||
if descriptions and schema_description:
|
||||
schema.select(ds.__Schema.description)
|
||||
|
||||
schema.select(
|
||||
ds.__Schema.queryType.select(ds.__Type.name),
|
||||
ds.__Schema.mutationType.select(ds.__Type.name),
|
||||
ds.__Schema.subscriptionType.select(ds.__Type.name),
|
||||
)
|
||||
|
||||
schema.select(ds.__Schema.types.select(fragment_FullType))
|
||||
|
||||
directives = ds.__Schema.directives.select(ds.__Directive.name)
|
||||
|
||||
deprecated_expand = {}
|
||||
|
||||
if input_value_deprecation:
|
||||
deprecated_expand = {
|
||||
"includeDeprecated": True,
|
||||
}
|
||||
|
||||
if descriptions:
|
||||
directives.select(ds.__Directive.description)
|
||||
if directive_is_repeatable:
|
||||
directives.select(ds.__Directive.isRepeatable)
|
||||
directives.select(
|
||||
ds.__Directive.locations,
|
||||
ds.__Directive.args(**deprecated_expand).select(fragment_InputValue),
|
||||
)
|
||||
|
||||
schema.select(directives)
|
||||
|
||||
fragment_FullType.select(
|
||||
ds.__Type.kind,
|
||||
ds.__Type.name,
|
||||
)
|
||||
if descriptions:
|
||||
fragment_FullType.select(ds.__Type.description)
|
||||
if specified_by_url:
|
||||
fragment_FullType.select(ds.__Type.specifiedByURL)
|
||||
|
||||
fields = ds.__Type.fields(includeDeprecated=True).select(ds.__Field.name)
|
||||
|
||||
if descriptions:
|
||||
fields.select(ds.__Field.description)
|
||||
|
||||
fields.select(
|
||||
ds.__Field.args(**deprecated_expand).select(fragment_InputValue),
|
||||
ds.__Field.type.select(fragment_TypeRef),
|
||||
ds.__Field.isDeprecated,
|
||||
ds.__Field.deprecationReason,
|
||||
)
|
||||
|
||||
enum_values = ds.__Type.enumValues(includeDeprecated=True).select(
|
||||
ds.__EnumValue.name
|
||||
)
|
||||
|
||||
if descriptions:
|
||||
enum_values.select(ds.__EnumValue.description)
|
||||
|
||||
enum_values.select(
|
||||
ds.__EnumValue.isDeprecated,
|
||||
ds.__EnumValue.deprecationReason,
|
||||
)
|
||||
|
||||
fragment_FullType.select(
|
||||
fields,
|
||||
ds.__Type.inputFields(**deprecated_expand).select(fragment_InputValue),
|
||||
ds.__Type.interfaces.select(fragment_TypeRef),
|
||||
enum_values,
|
||||
ds.__Type.possibleTypes.select(fragment_TypeRef),
|
||||
)
|
||||
|
||||
fragment_InputValue.select(ds.__InputValue.name)
|
||||
|
||||
if descriptions:
|
||||
fragment_InputValue.select(ds.__InputValue.description)
|
||||
|
||||
fragment_InputValue.select(
|
||||
ds.__InputValue.type.select(fragment_TypeRef),
|
||||
ds.__InputValue.defaultValue,
|
||||
)
|
||||
|
||||
if input_value_deprecation:
|
||||
fragment_InputValue.select(
|
||||
ds.__InputValue.isDeprecated,
|
||||
ds.__InputValue.deprecationReason,
|
||||
)
|
||||
|
||||
fragment_TypeRef.select(
|
||||
ds.__Type.kind,
|
||||
ds.__Type.name,
|
||||
)
|
||||
|
||||
if type_recursion_level >= 1:
|
||||
current_field = ds.__Type.ofType.select(ds.__Type.kind, ds.__Type.name)
|
||||
fragment_TypeRef.select(current_field)
|
||||
|
||||
for _ in repeat(None, type_recursion_level - 1):
|
||||
new_oftype = ds.__Type.ofType.select(ds.__Type.kind, ds.__Type.name)
|
||||
current_field.select(new_oftype)
|
||||
current_field = new_oftype
|
||||
|
||||
query = DSLQuery(schema)
|
||||
|
||||
query.name = "IntrospectionQuery"
|
||||
|
||||
dsl_query = dsl_gql(query, fragment_FullType, fragment_InputValue, fragment_TypeRef)
|
||||
|
||||
return dsl_query
|
||||
@@ -0,0 +1,92 @@
|
||||
from typing import Any, Iterable, List, Optional, Sized
|
||||
|
||||
from graphql import Node
|
||||
|
||||
|
||||
def _node_tree_recursive(
|
||||
obj: Any,
|
||||
*,
|
||||
indent: int = 0,
|
||||
ignored_keys: List,
|
||||
):
|
||||
|
||||
assert ignored_keys is not None
|
||||
|
||||
results = []
|
||||
|
||||
if hasattr(obj, "__slots__"):
|
||||
|
||||
results.append(" " * indent + f"{type(obj).__name__}")
|
||||
|
||||
try:
|
||||
keys = sorted(obj.keys)
|
||||
except AttributeError:
|
||||
# If the object has no keys attribute, print its repr and return.
|
||||
results.append(" " * (indent + 1) + repr(obj))
|
||||
else:
|
||||
for key in keys:
|
||||
if key in ignored_keys:
|
||||
continue
|
||||
attr_value = getattr(obj, key, None)
|
||||
results.append(" " * (indent + 1) + f"{key}:")
|
||||
if isinstance(attr_value, Iterable) and not isinstance(
|
||||
attr_value, (str, bytes)
|
||||
):
|
||||
if isinstance(attr_value, Sized) and len(attr_value) == 0:
|
||||
results.append(
|
||||
" " * (indent + 2) + f"empty {type(attr_value).__name__}"
|
||||
)
|
||||
else:
|
||||
for item in attr_value:
|
||||
results.append(
|
||||
_node_tree_recursive(
|
||||
item,
|
||||
indent=indent + 2,
|
||||
ignored_keys=ignored_keys,
|
||||
)
|
||||
)
|
||||
else:
|
||||
results.append(
|
||||
_node_tree_recursive(
|
||||
attr_value,
|
||||
indent=indent + 2,
|
||||
ignored_keys=ignored_keys,
|
||||
)
|
||||
)
|
||||
else:
|
||||
results.append(" " * indent + repr(obj))
|
||||
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
def node_tree(
|
||||
obj: Node,
|
||||
*,
|
||||
ignore_loc: bool = True,
|
||||
ignore_block: bool = True,
|
||||
ignored_keys: Optional[List] = None,
|
||||
):
|
||||
"""Method which returns a tree of Node elements as a String.
|
||||
|
||||
Useful to debug deep DocumentNode instances created by gql or dsl_gql.
|
||||
|
||||
NOTE: from gql version 3.6.0b4 the elements of each node are sorted to ignore
|
||||
small changes in graphql-core
|
||||
|
||||
WARNING: the output of this method is not guaranteed and may change without notice.
|
||||
"""
|
||||
|
||||
assert isinstance(obj, Node)
|
||||
|
||||
if ignored_keys is None:
|
||||
ignored_keys = []
|
||||
|
||||
if ignore_loc:
|
||||
# We are ignoring loc attributes by default
|
||||
ignored_keys.append("loc")
|
||||
|
||||
if ignore_block:
|
||||
# We are ignoring block attributes by default (in StringValueNode)
|
||||
ignored_keys.append("block")
|
||||
|
||||
return _node_tree_recursive(obj, ignored_keys=ignored_keys)
|
||||
@@ -0,0 +1,446 @@
|
||||
import logging
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, cast
|
||||
|
||||
from graphql import (
|
||||
IDLE,
|
||||
REMOVE,
|
||||
DocumentNode,
|
||||
FieldNode,
|
||||
FragmentDefinitionNode,
|
||||
FragmentSpreadNode,
|
||||
GraphQLError,
|
||||
GraphQLInterfaceType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
GraphQLSchema,
|
||||
GraphQLType,
|
||||
InlineFragmentNode,
|
||||
NameNode,
|
||||
Node,
|
||||
OperationDefinitionNode,
|
||||
SelectionSetNode,
|
||||
TypeInfo,
|
||||
TypeInfoVisitor,
|
||||
Visitor,
|
||||
is_leaf_type,
|
||||
print_ast,
|
||||
visit,
|
||||
)
|
||||
from graphql.language.visitor import VisitorActionEnum
|
||||
from graphql.pyutils import inspect
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Equivalent to QUERY_DOCUMENT_KEYS but only for fields interesting to
|
||||
# visit to parse the results
|
||||
RESULT_DOCUMENT_KEYS: Dict[str, Tuple[str, ...]] = {
|
||||
"document": ("definitions",),
|
||||
"operation_definition": ("selection_set",),
|
||||
"selection_set": ("selections",),
|
||||
"field": ("selection_set",),
|
||||
"inline_fragment": ("selection_set",),
|
||||
"fragment_definition": ("selection_set",),
|
||||
}
|
||||
|
||||
|
||||
def _ignore_non_null(type_: GraphQLType):
|
||||
"""Removes the GraphQLNonNull wrappings around types."""
|
||||
if isinstance(type_, GraphQLNonNull):
|
||||
return type_.of_type
|
||||
else:
|
||||
return type_
|
||||
|
||||
|
||||
def _get_fragment(document, fragment_name):
|
||||
"""Returns a fragment from the document."""
|
||||
for definition in document.definitions:
|
||||
if isinstance(definition, FragmentDefinitionNode):
|
||||
if definition.name.value == fragment_name:
|
||||
return definition
|
||||
|
||||
raise GraphQLError(f'Fragment "{fragment_name}" not found in document!')
|
||||
|
||||
|
||||
class ParseResultVisitor(Visitor):
|
||||
def __init__(
|
||||
self,
|
||||
schema: GraphQLSchema,
|
||||
document: DocumentNode,
|
||||
node: Node,
|
||||
result: Dict[str, Any],
|
||||
type_info: TypeInfo,
|
||||
visit_fragment: bool = False,
|
||||
inside_list_level: int = 0,
|
||||
operation_name: Optional[str] = None,
|
||||
):
|
||||
"""Recursive Implementation of a Visitor class to parse results
|
||||
correspondind to a schema and a document.
|
||||
|
||||
Using a TypeInfo class to get the node types during traversal.
|
||||
|
||||
If we reach a list in the results, then we parse each
|
||||
item of the list recursively, traversing the same nodes
|
||||
of the query again.
|
||||
|
||||
During traversal, we keep the current position in the result
|
||||
in the result_stack field.
|
||||
|
||||
Alongside the field type, we calculate the "result type"
|
||||
which is computed from the field type and the current
|
||||
recursive level we are for this field
|
||||
(:code:`inside_list_level` argument).
|
||||
"""
|
||||
self.schema: GraphQLSchema = schema
|
||||
self.document: DocumentNode = document
|
||||
self.node: Node = node
|
||||
self.result: Dict[str, Any] = result
|
||||
self.type_info: TypeInfo = type_info
|
||||
self.visit_fragment: bool = visit_fragment
|
||||
self.inside_list_level = inside_list_level
|
||||
self.operation_name = operation_name
|
||||
|
||||
self.result_stack: List[Any] = []
|
||||
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def current_result(self):
|
||||
try:
|
||||
return self.result_stack[-1]
|
||||
except IndexError:
|
||||
return self.result
|
||||
|
||||
@staticmethod
|
||||
def leave_document(node: DocumentNode, *_args: Any) -> Dict[str, Any]:
|
||||
results = cast(List[Dict[str, Any]], node.definitions)
|
||||
return {k: v for result in results for k, v in result.items()}
|
||||
|
||||
def enter_operation_definition(
|
||||
self, node: OperationDefinitionNode, *_args: Any
|
||||
) -> Union[None, VisitorActionEnum]:
|
||||
|
||||
if self.operation_name is not None:
|
||||
if not hasattr(node.name, "value"):
|
||||
return REMOVE # pragma: no cover
|
||||
|
||||
node.name = cast(NameNode, node.name)
|
||||
|
||||
if node.name.value != self.operation_name:
|
||||
log.debug(f"SKIPPING operation {node.name.value}")
|
||||
return REMOVE
|
||||
|
||||
return IDLE
|
||||
|
||||
@staticmethod
|
||||
def leave_operation_definition(
|
||||
node: OperationDefinitionNode, *_args: Any
|
||||
) -> Dict[str, Any]:
|
||||
selections = cast(List[Dict[str, Any]], node.selection_set)
|
||||
return {k: v for s in selections for k, v in s.items()}
|
||||
|
||||
@staticmethod
|
||||
def leave_selection_set(node: SelectionSetNode, *_args: Any) -> Dict[str, Any]:
|
||||
partial_results = cast(Dict[str, Any], node.selections)
|
||||
return partial_results
|
||||
|
||||
@staticmethod
|
||||
def in_first_field(path):
|
||||
return path.count("selections") <= 1
|
||||
|
||||
def get_current_result_type(self, path):
|
||||
field_type = self.type_info.get_type()
|
||||
|
||||
list_level = self.inside_list_level
|
||||
|
||||
result_type = _ignore_non_null(field_type)
|
||||
|
||||
if self.in_first_field(path):
|
||||
|
||||
while list_level > 0:
|
||||
assert isinstance(result_type, GraphQLList)
|
||||
result_type = _ignore_non_null(result_type.of_type)
|
||||
|
||||
list_level -= 1
|
||||
|
||||
return result_type
|
||||
|
||||
def enter_field(
|
||||
self,
|
||||
node: FieldNode,
|
||||
key: str,
|
||||
parent: Node,
|
||||
path: List[Node],
|
||||
ancestors: List[Node],
|
||||
) -> Union[None, VisitorActionEnum, Dict[str, Any]]:
|
||||
|
||||
name = node.alias.value if node.alias else node.name.value
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(f"Enter field {name}")
|
||||
log.debug(f" path={path!r}")
|
||||
log.debug(f" current_result={self.current_result!r}")
|
||||
|
||||
if self.current_result is None:
|
||||
# Result was null for this field -> remove
|
||||
return REMOVE
|
||||
|
||||
elif isinstance(self.current_result, Mapping):
|
||||
|
||||
try:
|
||||
result_value = self.current_result[name]
|
||||
except KeyError:
|
||||
# Key not found in result.
|
||||
# Should never happen in theory with a correct GraphQL backend
|
||||
# Silently ignoring this field
|
||||
log.debug(f" Key {name} not found in result --> REMOVE")
|
||||
return REMOVE
|
||||
|
||||
log.debug(f" result_value={result_value}")
|
||||
|
||||
# We get the field_type from type_info
|
||||
field_type = self.type_info.get_type()
|
||||
|
||||
# We calculate a virtual "result type" depending on our recursion level.
|
||||
result_type = self.get_current_result_type(path)
|
||||
|
||||
# If the result for this field is a list, then we need
|
||||
# to recursively visit the same node multiple times for each
|
||||
# item in the list.
|
||||
if (
|
||||
not isinstance(result_value, Mapping)
|
||||
and isinstance(result_value, Iterable)
|
||||
and not isinstance(result_value, str)
|
||||
and not is_leaf_type(result_type)
|
||||
):
|
||||
|
||||
# Finding out the inner type of the list
|
||||
inner_type = _ignore_non_null(result_type.of_type)
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(" List detected:")
|
||||
log.debug(f" field_type={inspect(field_type)}")
|
||||
log.debug(f" result_type={inspect(result_type)}")
|
||||
log.debug(f" inner_type={inspect(inner_type)}\n")
|
||||
|
||||
visits: List[Dict[str, Any]] = []
|
||||
|
||||
# Get parent type
|
||||
initial_type = self.type_info.get_parent_type()
|
||||
assert isinstance(
|
||||
initial_type, (GraphQLObjectType, GraphQLInterfaceType)
|
||||
)
|
||||
|
||||
# Get parent SelectionSet node
|
||||
selection_set_node = ancestors[-1]
|
||||
assert isinstance(selection_set_node, SelectionSetNode)
|
||||
|
||||
# Keep only the current node in a new selection set node
|
||||
new_node = SelectionSetNode(selections=[node])
|
||||
|
||||
for item in result_value:
|
||||
|
||||
new_result = {name: item}
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(f" recursive new_result={new_result}")
|
||||
log.debug(f" recursive ast={print_ast(node)}")
|
||||
log.debug(f" recursive path={path!r}")
|
||||
log.debug(f" recursive initial_type={initial_type!r}\n")
|
||||
|
||||
if self.in_first_field(path):
|
||||
inside_list_level = self.inside_list_level + 1
|
||||
else:
|
||||
inside_list_level = 1
|
||||
|
||||
inner_visit = parse_result_recursive(
|
||||
self.schema,
|
||||
self.document,
|
||||
new_node,
|
||||
new_result,
|
||||
initial_type=initial_type,
|
||||
inside_list_level=inside_list_level,
|
||||
)
|
||||
log.debug(f" recursive result={inner_visit}\n")
|
||||
|
||||
inner_visit = cast(List[Dict[str, Any]], inner_visit)
|
||||
visits.append(inner_visit[0][name])
|
||||
|
||||
result_value = {name: visits}
|
||||
log.debug(f" recursive visits final result = {result_value}\n")
|
||||
return result_value
|
||||
|
||||
# If the result for this field is not a list, then add it
|
||||
# to the result stack so that it becomes the current_value
|
||||
# for the next inner fields
|
||||
self.result_stack.append(result_value)
|
||||
|
||||
return IDLE
|
||||
|
||||
raise GraphQLError(
|
||||
f"Invalid result for container of field {name}: {self.current_result!r}"
|
||||
)
|
||||
|
||||
def leave_field(
|
||||
self,
|
||||
node: FieldNode,
|
||||
key: str,
|
||||
parent: Node,
|
||||
path: List[Node],
|
||||
ancestors: List[Node],
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
name = cast(str, node.alias.value if node.alias else node.name.value)
|
||||
|
||||
log.debug(f"Leave field {name}")
|
||||
|
||||
if self.current_result is None:
|
||||
|
||||
return_value = None
|
||||
|
||||
elif node.selection_set is None:
|
||||
|
||||
field_type = self.type_info.get_type()
|
||||
result_type = self.get_current_result_type(path)
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(f" field type of {name} is {inspect(field_type)}")
|
||||
log.debug(f" result type of {name} is {inspect(result_type)}")
|
||||
|
||||
assert is_leaf_type(result_type)
|
||||
|
||||
# Finally parsing a single scalar using the parse_value method
|
||||
return_value = result_type.parse_value(self.current_result)
|
||||
else:
|
||||
|
||||
partial_results = cast(List[Dict[str, Any]], node.selection_set)
|
||||
|
||||
return_value = {k: v for pr in partial_results for k, v in pr.items()}
|
||||
|
||||
# Go up a level in the result stack
|
||||
self.result_stack.pop()
|
||||
|
||||
log.debug(f"Leave field {name}: returning {return_value}")
|
||||
|
||||
return {name: return_value}
|
||||
|
||||
# Fragments
|
||||
|
||||
def enter_fragment_definition(
|
||||
self, node: FragmentDefinitionNode, *_args: Any
|
||||
) -> Union[None, VisitorActionEnum]:
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(f"Enter fragment definition {node.name.value}.")
|
||||
log.debug(f"visit_fragment={self.visit_fragment!s}")
|
||||
|
||||
if self.visit_fragment:
|
||||
return IDLE
|
||||
else:
|
||||
return REMOVE
|
||||
|
||||
@staticmethod
|
||||
def leave_fragment_definition(
|
||||
node: FragmentDefinitionNode, *_args: Any
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
selections = cast(List[Dict[str, Any]], node.selection_set)
|
||||
return {k: v for s in selections for k, v in s.items()}
|
||||
|
||||
def leave_fragment_spread(
|
||||
self, node: FragmentSpreadNode, *_args: Any
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
fragment_name = node.name.value
|
||||
|
||||
log.debug(f"Start recursive fragment visit {fragment_name}")
|
||||
|
||||
fragment_node = _get_fragment(self.document, fragment_name)
|
||||
|
||||
fragment_result = parse_result_recursive(
|
||||
self.schema,
|
||||
self.document,
|
||||
fragment_node,
|
||||
self.current_result,
|
||||
visit_fragment=True,
|
||||
)
|
||||
|
||||
log.debug(
|
||||
f"Result of recursive fragment visit {fragment_name}: {fragment_result}"
|
||||
)
|
||||
|
||||
return cast(Dict[str, Any], fragment_result)
|
||||
|
||||
@staticmethod
|
||||
def leave_inline_fragment(node: InlineFragmentNode, *_args: Any) -> Dict[str, Any]:
|
||||
|
||||
selections = cast(List[Dict[str, Any]], node.selection_set)
|
||||
return {k: v for s in selections for k, v in s.items()}
|
||||
|
||||
|
||||
def parse_result_recursive(
|
||||
schema: GraphQLSchema,
|
||||
document: DocumentNode,
|
||||
node: Node,
|
||||
result: Optional[Dict[str, Any]],
|
||||
initial_type: Optional[GraphQLType] = None,
|
||||
inside_list_level: int = 0,
|
||||
visit_fragment: bool = False,
|
||||
operation_name: Optional[str] = None,
|
||||
) -> Any:
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
type_info = TypeInfo(schema, initial_type=initial_type)
|
||||
|
||||
visited = visit(
|
||||
node,
|
||||
TypeInfoVisitor(
|
||||
type_info,
|
||||
ParseResultVisitor(
|
||||
schema,
|
||||
document,
|
||||
node,
|
||||
result,
|
||||
type_info=type_info,
|
||||
inside_list_level=inside_list_level,
|
||||
visit_fragment=visit_fragment,
|
||||
operation_name=operation_name,
|
||||
),
|
||||
),
|
||||
visitor_keys=RESULT_DOCUMENT_KEYS,
|
||||
)
|
||||
|
||||
return visited
|
||||
|
||||
|
||||
def parse_result(
|
||||
schema: GraphQLSchema,
|
||||
document: DocumentNode,
|
||||
result: Optional[Dict[str, Any]],
|
||||
operation_name: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Unserialize a result received from a GraphQL backend.
|
||||
|
||||
:param schema: the GraphQL schema
|
||||
:param document: the document representing the query sent to the backend
|
||||
:param result: the serialized result received from the backend
|
||||
:param operation_name: the optional operation name
|
||||
|
||||
:returns: a parsed result with scalars and enums parsed depending on
|
||||
their definition in the schema.
|
||||
|
||||
Given a schema, a query and a serialized result,
|
||||
provide a new result with parsed values.
|
||||
|
||||
If the result contains only built-in GraphQL scalars (String, Int, Float, ...)
|
||||
then the parsed result should be unchanged.
|
||||
|
||||
If the result contains custom scalars or enums, then those values
|
||||
will be parsed with the parse_value method of the custom scalar or enum
|
||||
definition in the schema."""
|
||||
|
||||
return parse_result_recursive(
|
||||
schema, document, document, result, operation_name=operation_name
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from graphql import (
|
||||
DocumentNode,
|
||||
GraphQLEnumType,
|
||||
GraphQLError,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLScalarType,
|
||||
GraphQLSchema,
|
||||
GraphQLType,
|
||||
GraphQLWrappingType,
|
||||
OperationDefinitionNode,
|
||||
type_from_ast,
|
||||
)
|
||||
from graphql.pyutils import inspect
|
||||
|
||||
|
||||
def _get_document_operation(
|
||||
document: DocumentNode, operation_name: Optional[str] = None
|
||||
) -> OperationDefinitionNode:
|
||||
"""Returns the operation which should be executed in the document.
|
||||
|
||||
Raises a GraphQLError if a single operation cannot be retrieved.
|
||||
"""
|
||||
|
||||
operation: Optional[OperationDefinitionNode] = None
|
||||
|
||||
for definition in document.definitions:
|
||||
if isinstance(definition, OperationDefinitionNode):
|
||||
if operation_name is None:
|
||||
if operation:
|
||||
raise GraphQLError(
|
||||
"Must provide operation name"
|
||||
" if query contains multiple operations."
|
||||
)
|
||||
operation = definition
|
||||
elif definition.name and definition.name.value == operation_name:
|
||||
operation = definition
|
||||
|
||||
if not operation:
|
||||
if operation_name is not None:
|
||||
raise GraphQLError(f"Unknown operation named '{operation_name}'.")
|
||||
|
||||
# The following line should never happen normally as the document is
|
||||
# already verified before calling this function.
|
||||
raise GraphQLError("Must provide an operation.") # pragma: no cover
|
||||
|
||||
return operation
|
||||
|
||||
|
||||
def serialize_value(type_: GraphQLType, value: Any) -> Any:
|
||||
"""Given a GraphQL type and a Python value, return the serialized value.
|
||||
|
||||
This method will serialize the value recursively, entering into
|
||||
lists and dicts.
|
||||
|
||||
Can be used to serialize Enums and/or Custom Scalars in variable values.
|
||||
|
||||
:param type_: the GraphQL type
|
||||
:param value: the provided value
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
if isinstance(type_, GraphQLNonNull):
|
||||
# raise GraphQLError(f"Type {type_.of_type.name} Cannot be None.")
|
||||
raise GraphQLError(f"Type {inspect(type_)} Cannot be None.")
|
||||
else:
|
||||
return None
|
||||
|
||||
if isinstance(type_, GraphQLWrappingType):
|
||||
inner_type = type_.of_type
|
||||
|
||||
if isinstance(type_, GraphQLNonNull):
|
||||
return serialize_value(inner_type, value)
|
||||
|
||||
elif isinstance(type_, GraphQLList):
|
||||
return [serialize_value(inner_type, v) for v in value]
|
||||
|
||||
elif isinstance(type_, (GraphQLScalarType, GraphQLEnumType)):
|
||||
return type_.serialize(value)
|
||||
|
||||
elif isinstance(type_, GraphQLInputObjectType):
|
||||
return {
|
||||
field_name: serialize_value(field.type, value[field_name])
|
||||
for field_name, field in type_.fields.items()
|
||||
if field_name in value
|
||||
}
|
||||
|
||||
raise GraphQLError(f"Impossible to serialize value with type: {inspect(type_)}.")
|
||||
|
||||
|
||||
def serialize_variable_values(
|
||||
schema: GraphQLSchema,
|
||||
document: DocumentNode,
|
||||
variable_values: Dict[str, Any],
|
||||
operation_name: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Given a GraphQL document and a schema, serialize the Dictionary of
|
||||
variable values.
|
||||
|
||||
Useful to serialize Enums and/or Custom Scalars in variable values.
|
||||
|
||||
:param schema: the GraphQL schema
|
||||
:param document: the document representing the query sent to the backend
|
||||
:param variable_values: the dictionnary of variable values which needs
|
||||
to be serialized.
|
||||
:param operation_name: the optional operation_name for the query.
|
||||
"""
|
||||
|
||||
parsed_variable_values: Dict[str, Any] = {}
|
||||
|
||||
# Find the operation in the document
|
||||
operation = _get_document_operation(document, operation_name=operation_name)
|
||||
|
||||
# Serialize every variable value defined for the operation
|
||||
for var_def_node in operation.variable_definitions:
|
||||
var_name = var_def_node.variable.name.value
|
||||
var_type = type_from_ast(schema, var_def_node.type)
|
||||
|
||||
if var_name in variable_values:
|
||||
|
||||
assert var_type is not None
|
||||
|
||||
var_value = variable_values[var_name]
|
||||
|
||||
parsed_variable_values[var_name] = serialize_value(var_type, var_value)
|
||||
|
||||
return parsed_variable_values
|
||||
@@ -0,0 +1,69 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Mapping, Type, Union, cast
|
||||
|
||||
from graphql import GraphQLEnumType, GraphQLSchema
|
||||
|
||||
|
||||
def update_schema_enum(
|
||||
schema: GraphQLSchema,
|
||||
name: str,
|
||||
values: Union[Dict[str, Any], Type[Enum]],
|
||||
use_enum_values: bool = False,
|
||||
):
|
||||
"""Update in the schema the GraphQLEnumType corresponding to the given name.
|
||||
|
||||
Example::
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 0
|
||||
GREEN = 1
|
||||
BLUE = 2
|
||||
|
||||
update_schema_enum(schema, 'Color', Color)
|
||||
|
||||
:param schema: a GraphQL Schema already containing the GraphQLEnumType type.
|
||||
:param name: the name of the enum in the GraphQL schema
|
||||
:param values: Either a Python Enum or a dict of values. The keys of the provided
|
||||
values should correspond to the keys of the existing enum in the schema.
|
||||
:param use_enum_values: By default, we configure the GraphQLEnumType to serialize
|
||||
to enum instances (ie: .parse_value() returns Color.RED).
|
||||
If use_enum_values is set to True, then .parse_value() returns 0.
|
||||
use_enum_values=True is the defaut behaviour when passing an Enum
|
||||
to a GraphQLEnumType.
|
||||
"""
|
||||
|
||||
# Convert Enum values to Dict
|
||||
if isinstance(values, type):
|
||||
if issubclass(values, Enum):
|
||||
values = cast(Type[Enum], values)
|
||||
if use_enum_values:
|
||||
values = {enum.name: enum.value for enum in values}
|
||||
else:
|
||||
values = {enum.name: enum for enum in values}
|
||||
|
||||
if not isinstance(values, Mapping):
|
||||
raise TypeError(f"Invalid type for enum values: {type(values)}")
|
||||
|
||||
# Find enum type in schema
|
||||
schema_enum = schema.get_type(name)
|
||||
|
||||
if schema_enum is None:
|
||||
raise KeyError(f"Enum {name} not found in schema!")
|
||||
|
||||
if not isinstance(schema_enum, GraphQLEnumType):
|
||||
raise TypeError(
|
||||
f'The type "{name}" is not a GraphQLEnumType, it is a {type(schema_enum)}'
|
||||
)
|
||||
|
||||
# Replace all enum values
|
||||
for enum_name, enum_value in schema_enum.values.items():
|
||||
try:
|
||||
enum_value.value = values[enum_name]
|
||||
except KeyError:
|
||||
raise KeyError(f'Enum key "{enum_name}" not found in provided values!')
|
||||
|
||||
# Delete the _value_lookup cached property
|
||||
if "_value_lookup" in schema_enum.__dict__:
|
||||
del schema_enum.__dict__["_value_lookup"]
|
||||
@@ -0,0 +1,60 @@
|
||||
from typing import Iterable, List
|
||||
|
||||
from graphql import GraphQLScalarType, GraphQLSchema
|
||||
|
||||
|
||||
def update_schema_scalar(schema: GraphQLSchema, name: str, scalar: GraphQLScalarType):
|
||||
"""Update the scalar in a schema with the scalar provided.
|
||||
|
||||
:param schema: the GraphQL schema
|
||||
:param name: the name of the custom scalar type in the schema
|
||||
:param scalar: a provided scalar type
|
||||
|
||||
This can be used to update the default Custom Scalar implementation
|
||||
when the schema has been provided from a text file or from introspection.
|
||||
"""
|
||||
|
||||
if not isinstance(scalar, GraphQLScalarType):
|
||||
raise TypeError("Scalars should be instances of GraphQLScalarType.")
|
||||
|
||||
schema_scalar = schema.get_type(name)
|
||||
|
||||
if schema_scalar is None:
|
||||
raise KeyError(f"Scalar '{name}' not found in schema.")
|
||||
|
||||
if not isinstance(schema_scalar, GraphQLScalarType):
|
||||
raise TypeError(
|
||||
f'The type "{name}" is not a GraphQLScalarType,'
|
||||
f" it is a {type(schema_scalar)}"
|
||||
)
|
||||
|
||||
# Update the conversion methods
|
||||
# Using setattr because mypy has a false positive
|
||||
# https://github.com/python/mypy/issues/2427
|
||||
setattr(schema_scalar, "serialize", scalar.serialize)
|
||||
setattr(schema_scalar, "parse_value", scalar.parse_value)
|
||||
setattr(schema_scalar, "parse_literal", scalar.parse_literal)
|
||||
|
||||
|
||||
def update_schema_scalars(schema: GraphQLSchema, scalars: List[GraphQLScalarType]):
|
||||
"""Update the scalars in a schema with the scalars provided.
|
||||
|
||||
:param schema: the GraphQL schema
|
||||
:param scalars: a list of provided scalar types
|
||||
|
||||
This can be used to update the default Custom Scalar implementation
|
||||
when the schema has been provided from a text file or from introspection.
|
||||
|
||||
If the name of the provided scalar is different than the name of
|
||||
the custom scalar, then you should use the
|
||||
:func:`update_schema_scalar <gql.utilities.update_schema_scalar>` method instead.
|
||||
"""
|
||||
|
||||
if not isinstance(scalars, Iterable):
|
||||
raise TypeError("Scalars argument should be a list of scalars.")
|
||||
|
||||
for scalar in scalars:
|
||||
if not isinstance(scalar, GraphQLScalarType):
|
||||
raise TypeError("Scalars should be instances of GraphQLScalarType.")
|
||||
|
||||
update_schema_scalar(schema, scalar.name, scalar)
|
||||
Reference in New Issue
Block a user