432 lines
17 KiB
Python
432 lines
17 KiB
Python
from itertools import chain
|
|
from typing import cast, Callable, Collection, Dict, List, Union
|
|
|
|
from ..language import DirectiveLocation, parse_value
|
|
from ..pyutils import inspect, Undefined
|
|
from ..type import (
|
|
GraphQLArgument,
|
|
GraphQLDirective,
|
|
GraphQLEnumType,
|
|
GraphQLEnumValue,
|
|
GraphQLField,
|
|
GraphQLInputField,
|
|
GraphQLInputObjectType,
|
|
GraphQLInputType,
|
|
GraphQLInterfaceType,
|
|
GraphQLList,
|
|
GraphQLNamedType,
|
|
GraphQLNonNull,
|
|
GraphQLObjectType,
|
|
GraphQLOutputType,
|
|
GraphQLScalarType,
|
|
GraphQLSchema,
|
|
GraphQLType,
|
|
GraphQLUnionType,
|
|
TypeKind,
|
|
assert_interface_type,
|
|
assert_nullable_type,
|
|
assert_object_type,
|
|
introspection_types,
|
|
is_input_type,
|
|
is_output_type,
|
|
specified_scalar_types,
|
|
)
|
|
from .get_introspection_query import (
|
|
IntrospectionDirective,
|
|
IntrospectionEnumType,
|
|
IntrospectionField,
|
|
IntrospectionInterfaceType,
|
|
IntrospectionInputObjectType,
|
|
IntrospectionInputValue,
|
|
IntrospectionObjectType,
|
|
IntrospectionQuery,
|
|
IntrospectionScalarType,
|
|
IntrospectionType,
|
|
IntrospectionTypeRef,
|
|
IntrospectionUnionType,
|
|
)
|
|
from .value_from_ast import value_from_ast
|
|
|
|
__all__ = ["build_client_schema"]
|
|
|
|
|
|
def build_client_schema(
|
|
introspection: IntrospectionQuery, assume_valid: bool = False
|
|
) -> GraphQLSchema:
|
|
"""Build a GraphQLSchema for use by client tools.
|
|
|
|
Given the result of a client running the introspection query, creates and returns
|
|
a GraphQLSchema instance which can be then used with all GraphQL-core 3 tools,
|
|
but cannot be used to execute a query, as introspection does not represent the
|
|
"resolver", "parse" or "serialize" functions or any other server-internal
|
|
mechanisms.
|
|
|
|
This function expects a complete introspection result. Don't forget to check the
|
|
"errors" field of a server response before calling this function.
|
|
"""
|
|
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)}."
|
|
)
|
|
|
|
# Get the schema from the introspection result.
|
|
schema_introspection = introspection["__schema"]
|
|
|
|
# Given a type reference in introspection, return the GraphQLType instance,
|
|
# preferring cached instances before building new instances.
|
|
def get_type(type_ref: IntrospectionTypeRef) -> GraphQLType:
|
|
kind = type_ref.get("kind")
|
|
if kind == TypeKind.LIST.name:
|
|
item_ref = type_ref.get("ofType")
|
|
if not item_ref:
|
|
raise TypeError("Decorated type deeper than introspection query.")
|
|
item_ref = cast(IntrospectionTypeRef, item_ref)
|
|
return GraphQLList(get_type(item_ref))
|
|
if kind == TypeKind.NON_NULL.name:
|
|
nullable_ref = type_ref.get("ofType")
|
|
if not nullable_ref:
|
|
raise TypeError("Decorated type deeper than introspection query.")
|
|
nullable_ref = cast(IntrospectionTypeRef, nullable_ref)
|
|
nullable_type = get_type(nullable_ref)
|
|
return GraphQLNonNull(assert_nullable_type(nullable_type))
|
|
type_ref = cast(IntrospectionType, type_ref)
|
|
return get_named_type(type_ref)
|
|
|
|
def get_named_type(type_ref: IntrospectionType) -> GraphQLNamedType:
|
|
type_name = type_ref.get("name")
|
|
if not type_name:
|
|
raise TypeError(f"Unknown type reference: {inspect(type_ref)}.")
|
|
|
|
type_ = type_map.get(type_name)
|
|
if not type_:
|
|
raise TypeError(
|
|
f"Invalid or incomplete schema, unknown type: {type_name}."
|
|
" Ensure that a full introspection query is used in order"
|
|
" to build a client schema."
|
|
)
|
|
return type_
|
|
|
|
def get_object_type(type_ref: IntrospectionObjectType) -> GraphQLObjectType:
|
|
return assert_object_type(get_type(type_ref))
|
|
|
|
def get_interface_type(
|
|
type_ref: IntrospectionInterfaceType,
|
|
) -> GraphQLInterfaceType:
|
|
return assert_interface_type(get_type(type_ref))
|
|
|
|
# Given a type's introspection result, construct the correct GraphQLType instance.
|
|
def build_type(type_: IntrospectionType) -> GraphQLNamedType:
|
|
if type_ and "name" in type_ and "kind" in type_:
|
|
builder = type_builders.get(type_["kind"])
|
|
if builder: # pragma: no cover else
|
|
return builder(type_)
|
|
raise TypeError(
|
|
"Invalid or incomplete introspection result."
|
|
" Ensure that a full introspection query is used in order"
|
|
f" to build a client schema: {inspect(type_)}."
|
|
)
|
|
|
|
def build_scalar_def(
|
|
scalar_introspection: IntrospectionScalarType,
|
|
) -> GraphQLScalarType:
|
|
name = scalar_introspection["name"]
|
|
try:
|
|
return cast(GraphQLScalarType, GraphQLScalarType.reserved_types[name])
|
|
except KeyError:
|
|
return GraphQLScalarType(
|
|
name=name,
|
|
description=scalar_introspection.get("description"),
|
|
specified_by_url=scalar_introspection.get("specifiedByURL"),
|
|
)
|
|
|
|
def build_implementations_list(
|
|
implementing_introspection: Union[
|
|
IntrospectionObjectType, IntrospectionInterfaceType
|
|
],
|
|
) -> List[GraphQLInterfaceType]:
|
|
maybe_interfaces = implementing_introspection.get("interfaces")
|
|
if maybe_interfaces is None:
|
|
# Temporary workaround until GraphQL ecosystem will fully support
|
|
# 'interfaces' on interface types
|
|
if implementing_introspection["kind"] == TypeKind.INTERFACE.name:
|
|
return []
|
|
raise TypeError(
|
|
"Introspection result missing interfaces:"
|
|
f" {inspect(implementing_introspection)}."
|
|
)
|
|
interfaces = cast(Collection[IntrospectionInterfaceType], maybe_interfaces)
|
|
return [get_interface_type(interface) for interface in interfaces]
|
|
|
|
def build_object_def(
|
|
object_introspection: IntrospectionObjectType,
|
|
) -> GraphQLObjectType:
|
|
name = object_introspection["name"]
|
|
try:
|
|
return cast(GraphQLObjectType, GraphQLObjectType.reserved_types[name])
|
|
except KeyError:
|
|
return GraphQLObjectType(
|
|
name=name,
|
|
description=object_introspection.get("description"),
|
|
interfaces=lambda: build_implementations_list(object_introspection),
|
|
fields=lambda: build_field_def_map(object_introspection),
|
|
)
|
|
|
|
def build_interface_def(
|
|
interface_introspection: IntrospectionInterfaceType,
|
|
) -> GraphQLInterfaceType:
|
|
return GraphQLInterfaceType(
|
|
name=interface_introspection["name"],
|
|
description=interface_introspection.get("description"),
|
|
interfaces=lambda: build_implementations_list(interface_introspection),
|
|
fields=lambda: build_field_def_map(interface_introspection),
|
|
)
|
|
|
|
def build_union_def(
|
|
union_introspection: IntrospectionUnionType,
|
|
) -> GraphQLUnionType:
|
|
maybe_possible_types = union_introspection.get("possibleTypes")
|
|
if maybe_possible_types is None:
|
|
raise TypeError(
|
|
"Introspection result missing possibleTypes:"
|
|
f" {inspect(union_introspection)}."
|
|
)
|
|
possible_types = cast(Collection[IntrospectionObjectType], maybe_possible_types)
|
|
return GraphQLUnionType(
|
|
name=union_introspection["name"],
|
|
description=union_introspection.get("description"),
|
|
types=lambda: [get_object_type(type_) for type_ in possible_types],
|
|
)
|
|
|
|
def build_enum_def(enum_introspection: IntrospectionEnumType) -> GraphQLEnumType:
|
|
if enum_introspection.get("enumValues") is None:
|
|
raise TypeError(
|
|
"Introspection result missing enumValues:"
|
|
f" {inspect(enum_introspection)}."
|
|
)
|
|
name = enum_introspection["name"]
|
|
try:
|
|
return cast(GraphQLEnumType, GraphQLEnumType.reserved_types[name])
|
|
except KeyError:
|
|
return GraphQLEnumType(
|
|
name=name,
|
|
description=enum_introspection.get("description"),
|
|
values={
|
|
value_introspect["name"]: GraphQLEnumValue(
|
|
value=value_introspect["name"],
|
|
description=value_introspect.get("description"),
|
|
deprecation_reason=value_introspect.get("deprecationReason"),
|
|
)
|
|
for value_introspect in enum_introspection["enumValues"]
|
|
},
|
|
)
|
|
|
|
def build_input_object_def(
|
|
input_object_introspection: IntrospectionInputObjectType,
|
|
) -> GraphQLInputObjectType:
|
|
if input_object_introspection.get("inputFields") is None:
|
|
raise TypeError(
|
|
"Introspection result missing inputFields:"
|
|
f" {inspect(input_object_introspection)}."
|
|
)
|
|
return GraphQLInputObjectType(
|
|
name=input_object_introspection["name"],
|
|
description=input_object_introspection.get("description"),
|
|
fields=lambda: build_input_value_def_map(
|
|
input_object_introspection["inputFields"]
|
|
),
|
|
is_one_of=input_object_introspection.get("isOneOf", False),
|
|
)
|
|
|
|
type_builders: Dict[str, Callable[[IntrospectionType], GraphQLNamedType]] = {
|
|
TypeKind.SCALAR.name: build_scalar_def, # type: ignore
|
|
TypeKind.OBJECT.name: build_object_def, # type: ignore
|
|
TypeKind.INTERFACE.name: build_interface_def, # type: ignore
|
|
TypeKind.UNION.name: build_union_def, # type: ignore
|
|
TypeKind.ENUM.name: build_enum_def, # type: ignore
|
|
TypeKind.INPUT_OBJECT.name: build_input_object_def, # type: ignore
|
|
}
|
|
|
|
def build_field_def_map(
|
|
type_introspection: Union[IntrospectionObjectType, IntrospectionInterfaceType],
|
|
) -> Dict[str, GraphQLField]:
|
|
if type_introspection.get("fields") is None:
|
|
raise TypeError(
|
|
f"Introspection result missing fields: {type_introspection}."
|
|
)
|
|
return {
|
|
field_introspection["name"]: build_field(field_introspection)
|
|
for field_introspection in type_introspection["fields"]
|
|
}
|
|
|
|
def build_field(field_introspection: IntrospectionField) -> GraphQLField:
|
|
type_introspection = cast(IntrospectionType, field_introspection["type"])
|
|
type_ = get_type(type_introspection)
|
|
if not is_output_type(type_):
|
|
raise TypeError(
|
|
"Introspection must provide output type for fields,"
|
|
f" but received: {inspect(type_)}."
|
|
)
|
|
type_ = cast(GraphQLOutputType, type_)
|
|
|
|
args_introspection = field_introspection.get("args")
|
|
if args_introspection is None:
|
|
raise TypeError(
|
|
"Introspection result missing field args:"
|
|
f" {inspect(field_introspection)}."
|
|
)
|
|
|
|
return GraphQLField(
|
|
type_,
|
|
args=build_argument_def_map(args_introspection),
|
|
description=field_introspection.get("description"),
|
|
deprecation_reason=field_introspection.get("deprecationReason"),
|
|
)
|
|
|
|
def build_argument_def_map(
|
|
argument_value_introspections: Collection[IntrospectionInputValue],
|
|
) -> Dict[str, GraphQLArgument]:
|
|
return {
|
|
argument_introspection["name"]: build_argument(argument_introspection)
|
|
for argument_introspection in argument_value_introspections
|
|
}
|
|
|
|
def build_argument(
|
|
argument_introspection: IntrospectionInputValue,
|
|
) -> GraphQLArgument:
|
|
type_introspection = cast(IntrospectionType, argument_introspection["type"])
|
|
type_ = get_type(type_introspection)
|
|
if not is_input_type(type_):
|
|
raise TypeError(
|
|
"Introspection must provide input type for arguments,"
|
|
f" but received: {inspect(type_)}."
|
|
)
|
|
type_ = cast(GraphQLInputType, type_)
|
|
|
|
default_value_introspection = argument_introspection.get("defaultValue")
|
|
default_value = (
|
|
Undefined
|
|
if default_value_introspection is None
|
|
else value_from_ast(parse_value(default_value_introspection), type_)
|
|
)
|
|
return GraphQLArgument(
|
|
type_,
|
|
default_value=default_value,
|
|
description=argument_introspection.get("description"),
|
|
deprecation_reason=argument_introspection.get("deprecationReason"),
|
|
)
|
|
|
|
def build_input_value_def_map(
|
|
input_value_introspections: Collection[IntrospectionInputValue],
|
|
) -> Dict[str, GraphQLInputField]:
|
|
return {
|
|
input_value_introspection["name"]: build_input_value(
|
|
input_value_introspection
|
|
)
|
|
for input_value_introspection in input_value_introspections
|
|
}
|
|
|
|
def build_input_value(
|
|
input_value_introspection: IntrospectionInputValue,
|
|
) -> GraphQLInputField:
|
|
type_introspection = cast(IntrospectionType, input_value_introspection["type"])
|
|
type_ = get_type(type_introspection)
|
|
if not is_input_type(type_):
|
|
raise TypeError(
|
|
"Introspection must provide input type for input fields,"
|
|
f" but received: {inspect(type_)}."
|
|
)
|
|
type_ = cast(GraphQLInputType, type_)
|
|
|
|
default_value_introspection = input_value_introspection.get("defaultValue")
|
|
default_value = (
|
|
Undefined
|
|
if default_value_introspection is None
|
|
else value_from_ast(parse_value(default_value_introspection), type_)
|
|
)
|
|
return GraphQLInputField(
|
|
type_,
|
|
default_value=default_value,
|
|
description=input_value_introspection.get("description"),
|
|
deprecation_reason=input_value_introspection.get("deprecationReason"),
|
|
)
|
|
|
|
def build_directive(
|
|
directive_introspection: IntrospectionDirective,
|
|
) -> GraphQLDirective:
|
|
if directive_introspection.get("args") is None:
|
|
raise TypeError(
|
|
"Introspection result missing directive args:"
|
|
f" {inspect(directive_introspection)}."
|
|
)
|
|
if directive_introspection.get("locations") is None:
|
|
raise TypeError(
|
|
"Introspection result missing directive locations:"
|
|
f" {inspect(directive_introspection)}."
|
|
)
|
|
return GraphQLDirective(
|
|
name=directive_introspection["name"],
|
|
description=directive_introspection.get("description"),
|
|
is_repeatable=directive_introspection.get("isRepeatable", False),
|
|
locations=list(
|
|
cast(
|
|
Collection[DirectiveLocation],
|
|
directive_introspection.get("locations"),
|
|
)
|
|
),
|
|
args=build_argument_def_map(directive_introspection["args"]),
|
|
)
|
|
|
|
# Iterate through all types, getting the type definition for each.
|
|
type_map: Dict[str, GraphQLNamedType] = {
|
|
type_introspection["name"]: build_type(type_introspection)
|
|
for type_introspection in schema_introspection["types"]
|
|
}
|
|
|
|
# Include standard types only if they are used.
|
|
for std_type_name, std_type in chain(
|
|
specified_scalar_types.items(), introspection_types.items()
|
|
):
|
|
if std_type_name in type_map:
|
|
type_map[std_type_name] = std_type
|
|
|
|
# Get the root Query, Mutation, and Subscription types.
|
|
query_type_ref = schema_introspection.get("queryType")
|
|
query_type = None if query_type_ref is None else get_object_type(query_type_ref)
|
|
mutation_type_ref = schema_introspection.get("mutationType")
|
|
mutation_type = (
|
|
None if mutation_type_ref is None else get_object_type(mutation_type_ref)
|
|
)
|
|
subscription_type_ref = schema_introspection.get("subscriptionType")
|
|
subscription_type = (
|
|
None
|
|
if subscription_type_ref is None
|
|
else get_object_type(subscription_type_ref)
|
|
)
|
|
|
|
# Get the directives supported by Introspection, assuming empty-set if directives
|
|
# were not queried for.
|
|
directive_introspections = schema_introspection.get("directives")
|
|
directives = (
|
|
[
|
|
build_directive(directive_introspection)
|
|
for directive_introspection in directive_introspections
|
|
]
|
|
if directive_introspections
|
|
else []
|
|
)
|
|
|
|
# Then produce and return a Schema with these types.
|
|
return GraphQLSchema(
|
|
query=query_type,
|
|
mutation=mutation_type,
|
|
subscription=subscription_type,
|
|
types=list(type_map.values()),
|
|
directives=directives,
|
|
description=schema_introspection.get("description"),
|
|
assume_valid=assume_valid,
|
|
)
|