mirror of
https://github.com/koxudaxi/datamodel-code-generator.git
synced 2024-03-18 14:54:37 +03:00
Beta version graphql (#1707)
* Add scalar data type and template for this * Add union data type and template for this * Updater union template * Add typing.TypeAlias to default imports * Add graphql parser * Add first test * Formatted code style * Fix for test_main_simple_star_wars * Use poetry run ./scripts/format.sh * Fix `typename__` field for graphql object Set default literal value for `typename__` field * Add graphql docs * Add test: Check all GraphQL field types; * Add test: Check custom scalar py type; * Add test: Check graphql field aliases; * Run poetry run ./scripts/format.sh * Update graphql docs: Add section `Custom scalar types`; * poetry run ./scripts/format.sh
This commit is contained in:
@@ -268,6 +268,7 @@ These OSS projects use datamodel-code-generator to generate many models. See the
|
||||
- JSON Schema ([JSON Schema Core](http://json-schema.org/draft/2019-09/json-schema-validation.html)/[JSON Schema Validation](http://json-schema.org/draft/2019-09/json-schema-validation.html))
|
||||
- JSON/YAML/CSV Data (it will be converted to JSON Schema)
|
||||
- Python dictionary (it will be converted to JSON Schema)
|
||||
- GraphQL schema ([GraphQL Schemas and Types](https://graphql.org/learn/schema/))
|
||||
|
||||
## Supported output types
|
||||
- [pydantic](https://docs.pydantic.dev/1.10/).BaseModel
|
||||
|
||||
@@ -178,6 +178,7 @@ class InputFileType(Enum):
|
||||
Yaml = 'yaml'
|
||||
Dict = 'dict'
|
||||
CSV = 'csv'
|
||||
GraphQL = 'graphql'
|
||||
|
||||
|
||||
RAW_DATA_TYPES: List[InputFileType] = [
|
||||
@@ -185,6 +186,7 @@ RAW_DATA_TYPES: List[InputFileType] = [
|
||||
InputFileType.Yaml,
|
||||
InputFileType.Dict,
|
||||
InputFileType.CSV,
|
||||
InputFileType.GraphQL,
|
||||
]
|
||||
|
||||
|
||||
@@ -203,6 +205,10 @@ class OpenAPIScope(Enum):
|
||||
Parameters = 'parameters'
|
||||
|
||||
|
||||
class GraphQLScope(Enum):
|
||||
Schema = 'schema'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
def __init__(self, message: str) -> None:
|
||||
self.message: str = message
|
||||
@@ -273,6 +279,7 @@ def generate(
|
||||
field_include_all_keys: bool = False,
|
||||
field_extra_keys_without_x_prefix: Optional[Set[str]] = None,
|
||||
openapi_scopes: Optional[List[OpenAPIScope]] = None,
|
||||
graphql_scopes: Optional[List[GraphQLScope]] = None,
|
||||
wrap_string_literal: Optional[bool] = None,
|
||||
use_title_as_name: bool = False,
|
||||
use_operation_id_as_name: bool = False,
|
||||
@@ -329,6 +336,10 @@ def generate(
|
||||
|
||||
parser_class: Type[Parser] = OpenAPIParser
|
||||
kwargs['openapi_scopes'] = openapi_scopes
|
||||
elif input_file_type == InputFileType.GraphQL:
|
||||
from datamodel_code_generator.parser.graphql import GraphQLParser
|
||||
|
||||
parser_class: Type[Parser] = GraphQLParser
|
||||
else:
|
||||
from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ IMPORT_SET = Import.from_full_path('typing.Set')
|
||||
IMPORT_UNION = Import.from_full_path('typing.Union')
|
||||
IMPORT_OPTIONAL = Import.from_full_path('typing.Optional')
|
||||
IMPORT_LITERAL = Import.from_full_path('typing.Literal')
|
||||
IMPORT_TYPE_ALIAS = Import.from_full_path('typing.TypeAlias')
|
||||
IMPORT_LITERAL_BACKPORT = Import.from_full_path('typing_extensions.Literal')
|
||||
IMPORT_SEQUENCE = Import.from_full_path('typing.Sequence')
|
||||
IMPORT_FROZEN_SET = Import.from_full_path('typing.FrozenSet')
|
||||
|
||||
78
datamodel_code_generator/model/scalar.py
Normal file
78
datamodel_code_generator/model/scalar.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, DefaultDict, Dict, List, Optional, Tuple
|
||||
|
||||
from datamodel_code_generator.imports import IMPORT_TYPE_ALIAS, Import
|
||||
from datamodel_code_generator.model import DataModel, DataModelFieldBase
|
||||
from datamodel_code_generator.model.base import UNDEFINED
|
||||
from datamodel_code_generator.reference import Reference
|
||||
|
||||
_INT: str = 'int'
|
||||
_FLOAT: str = 'float'
|
||||
_BOOLEAN: str = 'bool'
|
||||
_STR: str = 'str'
|
||||
|
||||
# default graphql scalar types
|
||||
DEFAULT_GRAPHQL_SCALAR_TYPE = _STR
|
||||
|
||||
DEFAULT_GRAPHQL_SCALAR_TYPES: Dict[str, str] = {
|
||||
'Boolean': _BOOLEAN,
|
||||
'String': _STR,
|
||||
'ID': _STR,
|
||||
'Int': _INT,
|
||||
'Float': _FLOAT,
|
||||
}
|
||||
|
||||
|
||||
class DataTypeScalar(DataModel):
|
||||
TEMPLATE_FILE_PATH: ClassVar[str] = 'Scalar.jinja2'
|
||||
BASE_CLASS: ClassVar[str] = ''
|
||||
DEFAULT_IMPORTS: ClassVar[Tuple[Import, ...]] = (IMPORT_TYPE_ALIAS,)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
reference: Reference,
|
||||
fields: List[DataModelFieldBase],
|
||||
decorators: Optional[List[str]] = None,
|
||||
base_classes: Optional[List[Reference]] = None,
|
||||
custom_base_class: Optional[str] = None,
|
||||
custom_template_dir: Optional[Path] = None,
|
||||
extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
path: Optional[Path] = None,
|
||||
description: Optional[str] = None,
|
||||
default: Any = UNDEFINED,
|
||||
nullable: bool = False,
|
||||
):
|
||||
extra_template_data = extra_template_data or defaultdict(dict)
|
||||
|
||||
scalar_name = reference.name
|
||||
if scalar_name not in extra_template_data:
|
||||
extra_template_data[scalar_name] = defaultdict(dict)
|
||||
|
||||
# py_type
|
||||
py_type = extra_template_data[scalar_name].get(
|
||||
'py_type',
|
||||
DEFAULT_GRAPHQL_SCALAR_TYPES.get(
|
||||
reference.name, DEFAULT_GRAPHQL_SCALAR_TYPE
|
||||
),
|
||||
)
|
||||
extra_template_data[scalar_name]['py_type'] = py_type
|
||||
|
||||
super().__init__(
|
||||
reference=reference,
|
||||
fields=fields,
|
||||
decorators=decorators,
|
||||
base_classes=base_classes,
|
||||
custom_base_class=custom_base_class,
|
||||
custom_template_dir=custom_template_dir,
|
||||
extra_template_data=extra_template_data,
|
||||
methods=methods,
|
||||
path=path,
|
||||
description=description,
|
||||
default=default,
|
||||
nullable=nullable,
|
||||
)
|
||||
4
datamodel_code_generator/model/template/Scalar.jinja2
Normal file
4
datamodel_code_generator/model/template/Scalar.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
{%- if description %}
|
||||
# {{ description }}
|
||||
{%- endif %}
|
||||
{{ class_name }}: TypeAlias = {{ py_type }}
|
||||
10
datamodel_code_generator/model/template/Union.jinja2
Normal file
10
datamodel_code_generator/model/template/Union.jinja2
Normal file
@@ -0,0 +1,10 @@
|
||||
{%- if description %}
|
||||
# {{ description }}
|
||||
{%- endif %}
|
||||
{%- if fields|length > 1 %}
|
||||
{{ class_name }}: TypeAlias = Union[
|
||||
{%- for field in fields %}
|
||||
'{{ field.name }}',
|
||||
{%- endfor %}
|
||||
]{% else %}
|
||||
{{ class_name }}: TypeAlias = {{ fields[0].name }}{% endif %}
|
||||
49
datamodel_code_generator/model/union.py
Normal file
49
datamodel_code_generator/model/union.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, DefaultDict, Dict, List, Optional, Tuple
|
||||
|
||||
from datamodel_code_generator.imports import IMPORT_TYPE_ALIAS, IMPORT_UNION, Import
|
||||
from datamodel_code_generator.model import DataModel, DataModelFieldBase
|
||||
from datamodel_code_generator.model.base import UNDEFINED
|
||||
from datamodel_code_generator.reference import Reference
|
||||
|
||||
|
||||
class DataTypeUnion(DataModel):
|
||||
TEMPLATE_FILE_PATH: ClassVar[str] = 'Union.jinja2'
|
||||
BASE_CLASS: ClassVar[str] = ''
|
||||
DEFAULT_IMPORTS: ClassVar[Tuple[Import, ...]] = (
|
||||
IMPORT_TYPE_ALIAS,
|
||||
IMPORT_UNION,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
reference: Reference,
|
||||
fields: List[DataModelFieldBase],
|
||||
decorators: Optional[List[str]] = None,
|
||||
base_classes: Optional[List[Reference]] = None,
|
||||
custom_base_class: Optional[str] = None,
|
||||
custom_template_dir: Optional[Path] = None,
|
||||
extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
path: Optional[Path] = None,
|
||||
description: Optional[str] = None,
|
||||
default: Any = UNDEFINED,
|
||||
nullable: bool = False,
|
||||
):
|
||||
super().__init__(
|
||||
reference=reference,
|
||||
fields=fields,
|
||||
decorators=decorators,
|
||||
base_classes=base_classes,
|
||||
custom_base_class=custom_base_class,
|
||||
custom_template_dir=custom_template_dir,
|
||||
extra_template_data=extra_template_data,
|
||||
methods=methods,
|
||||
path=path,
|
||||
description=description,
|
||||
default=default,
|
||||
nullable=nullable,
|
||||
)
|
||||
495
datamodel_code_generator/parser/graphql.py
Normal file
495
datamodel_code_generator/parser/graphql.py
Normal file
@@ -0,0 +1,495 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
DefaultDict,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from urllib.parse import ParseResult
|
||||
|
||||
from datamodel_code_generator import (
|
||||
DefaultPutDict,
|
||||
LiteralType,
|
||||
PythonVersion,
|
||||
snooper_to_methods,
|
||||
)
|
||||
from datamodel_code_generator.model import DataModel, DataModelFieldBase
|
||||
from datamodel_code_generator.model import pydantic as pydantic_model
|
||||
from datamodel_code_generator.model.enum import Enum
|
||||
from datamodel_code_generator.model.scalar import DataTypeScalar
|
||||
from datamodel_code_generator.model.union import DataTypeUnion
|
||||
from datamodel_code_generator.parser.base import (
|
||||
DataType,
|
||||
Parser,
|
||||
Source,
|
||||
escape_characters,
|
||||
)
|
||||
from datamodel_code_generator.reference import ModelType, Reference
|
||||
from datamodel_code_generator.types import (
|
||||
DataTypeManager,
|
||||
StrictTypes,
|
||||
Types,
|
||||
)
|
||||
|
||||
try:
|
||||
import graphql
|
||||
except ImportError: # pragma: no cover
|
||||
raise Exception(
|
||||
"Please run `$pip install 'datamodel-code-generator[graphql]`' to generate data-model from a GraphQL schema."
|
||||
)
|
||||
|
||||
|
||||
graphql_resolver = graphql.type.introspection.TypeResolvers()
|
||||
|
||||
|
||||
def build_graphql_schema(schema_str: str) -> graphql.GraphQLSchema:
|
||||
"""Build a graphql schema from a string."""
|
||||
schema = graphql.build_schema(schema_str)
|
||||
return graphql.lexicographic_sort_schema(schema)
|
||||
|
||||
|
||||
@snooper_to_methods(max_variable_length=None)
|
||||
class GraphQLParser(Parser):
|
||||
# raw graphql schema as `graphql-core` object
|
||||
raw_obj: graphql.GraphQLSchema
|
||||
# all processed graphql objects
|
||||
# mapper from an object name (unique) to an object
|
||||
all_graphql_objects: Dict[str, graphql.GraphQLNamedType]
|
||||
# a reference for each object
|
||||
# mapper from an object name to his reference
|
||||
references: Dict[str, Reference] = {}
|
||||
# mapper from graphql type to all objects with this type
|
||||
# `graphql.type.introspection.TypeKind` -- an enum with all supported types
|
||||
# `graphql.GraphQLNamedType` -- base type for each graphql object
|
||||
# see `graphql-core` for more details
|
||||
support_graphql_types: Dict[
|
||||
graphql.type.introspection.TypeKind, List[graphql.GraphQLNamedType]
|
||||
]
|
||||
# graphql types order for render
|
||||
# may be as a parameter in the future
|
||||
parse_order: List[graphql.type.introspection.TypeKind] = [
|
||||
graphql.type.introspection.TypeKind.SCALAR,
|
||||
graphql.type.introspection.TypeKind.ENUM,
|
||||
graphql.type.introspection.TypeKind.INTERFACE,
|
||||
graphql.type.introspection.TypeKind.OBJECT,
|
||||
graphql.type.introspection.TypeKind.INPUT_OBJECT,
|
||||
graphql.type.introspection.TypeKind.UNION,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source: Union[str, Path, ParseResult],
|
||||
*,
|
||||
data_model_type: Type[DataModel] = pydantic_model.BaseModel,
|
||||
data_model_root_type: Type[DataModel] = pydantic_model.CustomRootType,
|
||||
data_model_scalar_type: Type[DataModel] = DataTypeScalar,
|
||||
data_model_union_type: Type[DataModel] = DataTypeUnion,
|
||||
data_type_manager_type: Type[DataTypeManager] = pydantic_model.DataTypeManager,
|
||||
data_model_field_type: Type[DataModelFieldBase] = pydantic_model.DataModelField,
|
||||
base_class: Optional[str] = None,
|
||||
additional_imports: Optional[List[str]] = None,
|
||||
custom_template_dir: Optional[Path] = None,
|
||||
extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]] = None,
|
||||
target_python_version: PythonVersion = PythonVersion.PY_37,
|
||||
dump_resolve_reference_action: Optional[Callable[[Iterable[str]], str]] = None,
|
||||
validation: bool = False,
|
||||
field_constraints: bool = False,
|
||||
snake_case_field: bool = False,
|
||||
strip_default_none: bool = False,
|
||||
aliases: Optional[Mapping[str, str]] = None,
|
||||
allow_population_by_field_name: bool = False,
|
||||
apply_default_values_for_required_fields: bool = False,
|
||||
allow_extra_fields: bool = False,
|
||||
force_optional_for_required_fields: bool = False,
|
||||
class_name: Optional[str] = None,
|
||||
use_standard_collections: bool = False,
|
||||
base_path: Optional[Path] = None,
|
||||
use_schema_description: bool = False,
|
||||
use_field_description: bool = False,
|
||||
use_default_kwarg: bool = False,
|
||||
reuse_model: bool = False,
|
||||
encoding: str = 'utf-8',
|
||||
enum_field_as_literal: Optional[LiteralType] = None,
|
||||
set_default_enum_member: bool = False,
|
||||
use_subclass_enum: bool = False,
|
||||
strict_nullable: bool = False,
|
||||
use_generic_container_types: bool = False,
|
||||
enable_faux_immutability: bool = False,
|
||||
remote_text_cache: Optional[DefaultPutDict[str, str]] = None,
|
||||
disable_appending_item_suffix: bool = False,
|
||||
strict_types: Optional[Sequence[StrictTypes]] = None,
|
||||
empty_enum_field_name: Optional[str] = None,
|
||||
custom_class_name_generator: Optional[Callable[[str], str]] = None,
|
||||
field_extra_keys: Optional[Set[str]] = None,
|
||||
field_include_all_keys: bool = False,
|
||||
field_extra_keys_without_x_prefix: Optional[Set[str]] = None,
|
||||
wrap_string_literal: Optional[bool] = None,
|
||||
use_title_as_name: bool = False,
|
||||
use_operation_id_as_name: bool = False,
|
||||
use_unique_items_as_set: bool = False,
|
||||
http_headers: Optional[Sequence[Tuple[str, str]]] = None,
|
||||
http_ignore_tls: bool = False,
|
||||
use_annotated: bool = False,
|
||||
use_non_positive_negative_number_constrained_types: bool = False,
|
||||
original_field_name_delimiter: Optional[str] = None,
|
||||
use_double_quotes: bool = False,
|
||||
use_union_operator: bool = False,
|
||||
allow_responses_without_content: bool = False,
|
||||
collapse_root_models: bool = False,
|
||||
special_field_name_prefix: Optional[str] = None,
|
||||
remove_special_field_name_prefix: bool = False,
|
||||
capitalise_enum_members: bool = False,
|
||||
keep_model_order: bool = False,
|
||||
use_one_literal_as_default: bool = False,
|
||||
known_third_party: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
source=source,
|
||||
data_model_type=data_model_type,
|
||||
data_model_root_type=data_model_root_type,
|
||||
data_type_manager_type=data_type_manager_type,
|
||||
data_model_field_type=data_model_field_type,
|
||||
base_class=base_class,
|
||||
additional_imports=additional_imports,
|
||||
custom_template_dir=custom_template_dir,
|
||||
extra_template_data=extra_template_data,
|
||||
target_python_version=target_python_version,
|
||||
dump_resolve_reference_action=dump_resolve_reference_action,
|
||||
validation=validation,
|
||||
field_constraints=field_constraints,
|
||||
snake_case_field=snake_case_field,
|
||||
strip_default_none=strip_default_none,
|
||||
aliases=aliases,
|
||||
allow_population_by_field_name=allow_population_by_field_name,
|
||||
allow_extra_fields=allow_extra_fields,
|
||||
apply_default_values_for_required_fields=apply_default_values_for_required_fields,
|
||||
force_optional_for_required_fields=force_optional_for_required_fields,
|
||||
class_name=class_name,
|
||||
use_standard_collections=use_standard_collections,
|
||||
base_path=base_path,
|
||||
use_schema_description=use_schema_description,
|
||||
use_field_description=use_field_description,
|
||||
use_default_kwarg=use_default_kwarg,
|
||||
reuse_model=reuse_model,
|
||||
encoding=encoding,
|
||||
enum_field_as_literal=enum_field_as_literal,
|
||||
use_one_literal_as_default=use_one_literal_as_default,
|
||||
set_default_enum_member=set_default_enum_member,
|
||||
use_subclass_enum=use_subclass_enum,
|
||||
strict_nullable=strict_nullable,
|
||||
use_generic_container_types=use_generic_container_types,
|
||||
enable_faux_immutability=enable_faux_immutability,
|
||||
remote_text_cache=remote_text_cache,
|
||||
disable_appending_item_suffix=disable_appending_item_suffix,
|
||||
strict_types=strict_types,
|
||||
empty_enum_field_name=empty_enum_field_name,
|
||||
custom_class_name_generator=custom_class_name_generator,
|
||||
field_extra_keys=field_extra_keys,
|
||||
field_include_all_keys=field_include_all_keys,
|
||||
field_extra_keys_without_x_prefix=field_extra_keys_without_x_prefix,
|
||||
wrap_string_literal=wrap_string_literal,
|
||||
use_title_as_name=use_title_as_name,
|
||||
use_operation_id_as_name=use_operation_id_as_name,
|
||||
use_unique_items_as_set=use_unique_items_as_set,
|
||||
http_headers=http_headers,
|
||||
http_ignore_tls=http_ignore_tls,
|
||||
use_annotated=use_annotated,
|
||||
use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types,
|
||||
original_field_name_delimiter=original_field_name_delimiter,
|
||||
use_double_quotes=use_double_quotes,
|
||||
use_union_operator=use_union_operator,
|
||||
allow_responses_without_content=allow_responses_without_content,
|
||||
collapse_root_models=collapse_root_models,
|
||||
special_field_name_prefix=special_field_name_prefix,
|
||||
remove_special_field_name_prefix=remove_special_field_name_prefix,
|
||||
capitalise_enum_members=capitalise_enum_members,
|
||||
keep_model_order=keep_model_order,
|
||||
known_third_party=known_third_party,
|
||||
)
|
||||
|
||||
self.data_model_scalar_type = data_model_scalar_type
|
||||
self.data_model_union_type = data_model_union_type
|
||||
|
||||
def _get_context_source_path_parts(self) -> Iterator[Tuple[Source, List[str]]]:
|
||||
# TODO (denisart): Temporarily this method duplicates
|
||||
# the method `datamodel_code_generator.parser.jsonschema.JsonSchemaParser._get_context_source_path_parts`.
|
||||
|
||||
if isinstance(self.source, list) or (
|
||||
isinstance(self.source, Path) and self.source.is_dir()
|
||||
):
|
||||
self.current_source_path = Path()
|
||||
self.model_resolver.after_load_files = {
|
||||
self.base_path.joinpath(s.path).resolve().as_posix()
|
||||
for s in self.iter_source
|
||||
}
|
||||
|
||||
for source in self.iter_source:
|
||||
if isinstance(self.source, ParseResult):
|
||||
path_parts = self.get_url_path_parts(self.source)
|
||||
else:
|
||||
path_parts = list(source.path.parts)
|
||||
if self.current_source_path is not None:
|
||||
self.current_source_path = source.path
|
||||
with self.model_resolver.current_base_path_context(
|
||||
source.path.parent
|
||||
), self.model_resolver.current_root_context(path_parts):
|
||||
yield source, path_parts
|
||||
|
||||
def _resolve_types(self, paths: List[str], schema: graphql.GraphQLSchema) -> None:
|
||||
for type_name, type_ in schema.type_map.items():
|
||||
if type_name.startswith('__'):
|
||||
continue
|
||||
|
||||
if type_name in ['Query', 'Mutation']:
|
||||
continue
|
||||
|
||||
resolved_type = graphql_resolver.kind(type_, None)
|
||||
|
||||
if resolved_type in self.support_graphql_types:
|
||||
self.all_graphql_objects[type_.name] = type_
|
||||
# TODO: need a special method for each graph type
|
||||
self.references[type_.name] = Reference(
|
||||
path=f'{str(*paths)}/{resolved_type.value}/{type_.name}',
|
||||
name=type_.name,
|
||||
original_name=type_.name,
|
||||
)
|
||||
|
||||
self.support_graphql_types[resolved_type].append(type_)
|
||||
|
||||
def _typename_field(self, name: str) -> DataModelFieldBase:
|
||||
return self.data_model_field_type(
|
||||
name='typename__',
|
||||
data_type=DataType(literals=[name]),
|
||||
default=name,
|
||||
required=False,
|
||||
alias='__typename',
|
||||
use_one_literal_as_default=True,
|
||||
has_default=True,
|
||||
)
|
||||
|
||||
def parse_scalar(self, scalar_graphql_object: graphql.GraphQLScalarType) -> None:
|
||||
self.results.append(
|
||||
self.data_model_scalar_type(
|
||||
reference=self.references[scalar_graphql_object.name],
|
||||
fields=[],
|
||||
custom_template_dir=self.custom_template_dir,
|
||||
extra_template_data=self.extra_template_data,
|
||||
description=scalar_graphql_object.description,
|
||||
)
|
||||
)
|
||||
|
||||
def parse_enum(self, enum_object: graphql.GraphQLEnumType) -> None:
|
||||
enum_fields: List[DataModelFieldBase] = []
|
||||
exclude_field_names: Set[str] = set()
|
||||
|
||||
for value_name, value in enum_object.values.items():
|
||||
default = (
|
||||
f"'{value_name.translate(escape_characters)}'"
|
||||
if isinstance(value_name, str)
|
||||
else value_name
|
||||
)
|
||||
|
||||
field_name = self.model_resolver.get_valid_field_name(
|
||||
value_name, excludes=exclude_field_names, model_type=ModelType.ENUM
|
||||
)
|
||||
exclude_field_names.add(field_name)
|
||||
|
||||
enum_fields.append(
|
||||
self.data_model_field_type(
|
||||
name=field_name,
|
||||
data_type=self.data_type_manager.get_data_type(
|
||||
Types.string,
|
||||
),
|
||||
default=default,
|
||||
required=True,
|
||||
strip_default_none=self.strip_default_none,
|
||||
has_default=True,
|
||||
use_field_description=value.description is not None,
|
||||
original_name=None,
|
||||
)
|
||||
)
|
||||
|
||||
enum = Enum(
|
||||
reference=self.references[enum_object.name],
|
||||
fields=enum_fields,
|
||||
path=self.current_source_path,
|
||||
description=enum_object.description,
|
||||
custom_template_dir=self.custom_template_dir,
|
||||
)
|
||||
self.results.append(enum)
|
||||
|
||||
def parse_field(
|
||||
self,
|
||||
field_name: str,
|
||||
alias: str,
|
||||
field: Union[graphql.GraphQLField, graphql.GraphQLInputField],
|
||||
) -> DataModelFieldBase:
|
||||
final_data_type = DataType(is_optional=True)
|
||||
data_type = final_data_type
|
||||
obj = field.type
|
||||
|
||||
while graphql.is_list_type(obj) or graphql.is_non_null_type(obj):
|
||||
if graphql.is_list_type(obj):
|
||||
data_type.is_list = True
|
||||
|
||||
new_data_type = DataType(is_optional=True)
|
||||
data_type.data_types = [new_data_type]
|
||||
|
||||
data_type = new_data_type
|
||||
elif graphql.is_non_null_type(obj):
|
||||
data_type.is_optional = False
|
||||
|
||||
obj = obj.of_type
|
||||
|
||||
data_type.type = obj.name
|
||||
|
||||
required = (not self.force_optional_for_required_fields) and (
|
||||
not final_data_type.is_optional
|
||||
)
|
||||
extras = {}
|
||||
|
||||
if hasattr(field, 'default_value'):
|
||||
if field.default_value == graphql.pyutils.Undefined:
|
||||
default = None
|
||||
else:
|
||||
default = field.default_value
|
||||
else:
|
||||
if required is False:
|
||||
if final_data_type.is_list:
|
||||
default = 'list'
|
||||
extras = {'default_factory': 'list'}
|
||||
else:
|
||||
default = None
|
||||
else:
|
||||
default = None
|
||||
|
||||
return self.data_model_field_type(
|
||||
name=field_name,
|
||||
default=default,
|
||||
data_type=final_data_type,
|
||||
required=required,
|
||||
extras=extras,
|
||||
alias=alias,
|
||||
strip_default_none=self.strip_default_none,
|
||||
use_annotated=self.use_annotated,
|
||||
use_field_description=field.description is not None,
|
||||
use_default_kwarg=self.use_default_kwarg,
|
||||
original_name=field_name,
|
||||
has_default=default is not None,
|
||||
)
|
||||
|
||||
def parse_object_like(
|
||||
self,
|
||||
obj: Union[
|
||||
graphql.GraphQLInterfaceType,
|
||||
graphql.GraphQLObjectType,
|
||||
graphql.GraphQLInputObjectType,
|
||||
],
|
||||
) -> None:
|
||||
fields = []
|
||||
exclude_field_names: Set[str] = set()
|
||||
|
||||
for field_name, field in obj.fields.items():
|
||||
field_name_, alias = self.model_resolver.get_valid_field_name_and_alias(
|
||||
field_name, excludes=exclude_field_names
|
||||
)
|
||||
exclude_field_names.add(field_name_)
|
||||
|
||||
data_model_field_type = self.parse_field(field_name_, alias, field)
|
||||
fields.append(data_model_field_type)
|
||||
|
||||
fields.append(self._typename_field(obj.name))
|
||||
|
||||
base_classes = []
|
||||
if hasattr(obj, 'interfaces'):
|
||||
base_classes = [self.references[i.name] for i in obj.interfaces]
|
||||
|
||||
data_model_type = self.data_model_type(
|
||||
reference=self.references[obj.name],
|
||||
fields=fields,
|
||||
base_classes=base_classes,
|
||||
custom_base_class=self.base_class,
|
||||
custom_template_dir=self.custom_template_dir,
|
||||
extra_template_data=self.extra_template_data,
|
||||
path=self.current_source_path,
|
||||
description=obj.description,
|
||||
)
|
||||
self.results.append(data_model_type)
|
||||
|
||||
def parse_interface(
|
||||
self, interface_graphql_object: graphql.GraphQLInterfaceType
|
||||
) -> None:
|
||||
self.parse_object_like(interface_graphql_object)
|
||||
|
||||
def parse_object(self, graphql_object: graphql.GraphQLObjectType) -> None:
|
||||
self.parse_object_like(graphql_object)
|
||||
|
||||
def parse_input_object(
|
||||
self, input_graphql_object: graphql.GraphQLInputObjectType
|
||||
) -> None:
|
||||
self.parse_object_like(input_graphql_object)
|
||||
|
||||
def parse_union(self, union_object: graphql.GraphQLUnionType) -> None:
|
||||
fields = []
|
||||
|
||||
for type_ in union_object.types:
|
||||
fields.append(
|
||||
self.data_model_field_type(name=type_.name, data_type=DataType())
|
||||
)
|
||||
|
||||
data_model_type = self.data_model_union_type(
|
||||
reference=self.references[union_object.name],
|
||||
fields=fields,
|
||||
custom_base_class=self.base_class,
|
||||
custom_template_dir=self.custom_template_dir,
|
||||
extra_template_data=self.extra_template_data,
|
||||
path=self.current_source_path,
|
||||
description=union_object.description,
|
||||
)
|
||||
self.results.append(data_model_type)
|
||||
|
||||
def parse_raw(self) -> None:
|
||||
self.all_graphql_objects = {}
|
||||
self.references: Dict[str, Reference] = {}
|
||||
|
||||
self.support_graphql_types = {
|
||||
graphql.type.introspection.TypeKind.SCALAR: [],
|
||||
graphql.type.introspection.TypeKind.ENUM: [],
|
||||
graphql.type.introspection.TypeKind.UNION: [],
|
||||
graphql.type.introspection.TypeKind.INTERFACE: [],
|
||||
graphql.type.introspection.TypeKind.OBJECT: [],
|
||||
graphql.type.introspection.TypeKind.INPUT_OBJECT: [],
|
||||
}
|
||||
|
||||
# may be as a parameter in the future (??)
|
||||
_mapper_from_graphql_type_to_parser_method = {
|
||||
graphql.type.introspection.TypeKind.SCALAR: self.parse_scalar,
|
||||
graphql.type.introspection.TypeKind.ENUM: self.parse_enum,
|
||||
graphql.type.introspection.TypeKind.INTERFACE: self.parse_interface,
|
||||
graphql.type.introspection.TypeKind.OBJECT: self.parse_object,
|
||||
graphql.type.introspection.TypeKind.INPUT_OBJECT: self.parse_input_object,
|
||||
graphql.type.introspection.TypeKind.UNION: self.parse_union,
|
||||
}
|
||||
|
||||
for source, path_parts in self._get_context_source_path_parts():
|
||||
schema: graphql.GraphQLSchema = build_graphql_schema(source.text)
|
||||
self.raw_obj = schema
|
||||
|
||||
self._resolve_types(path_parts, schema)
|
||||
|
||||
for next_type in self.parse_order:
|
||||
for obj in self.support_graphql_types[next_type]:
|
||||
parser_ = _mapper_from_graphql_type_to_parser_method[next_type]
|
||||
parser_(obj) # type: ignore
|
||||
195
docs/graphql.md
Normal file
195
docs/graphql.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Generate from GraphQL
|
||||
|
||||
The code generator can create pydantic models from GraphQL schema definitions.
|
||||
|
||||
## Simple example
|
||||
|
||||
```bash
|
||||
$ datamodel-codegen --input schema.graphql --input-file-type graphql --output model.py
|
||||
```
|
||||
|
||||
Let's consider a simple GraphQL schema
|
||||
(more details in https://graphql.org/learn/schema/).
|
||||
|
||||
**schema.graphql**
|
||||
```graphql
|
||||
type Book {
|
||||
id: ID!
|
||||
title: String
|
||||
author: Author
|
||||
}
|
||||
|
||||
type Author {
|
||||
id: ID!
|
||||
name: String
|
||||
books: [Book]
|
||||
}
|
||||
|
||||
input BooksInput {
|
||||
ids: [ID!]!
|
||||
}
|
||||
|
||||
input AuthorBooksInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
getBooks(input: BooksInput): [Book]
|
||||
getAuthorBooks(input: AuthorBooksInput): [Book]
|
||||
}
|
||||
```
|
||||
|
||||
**model.py**
|
||||
```python
|
||||
# generated by datamodel-codegen:
|
||||
# filename: schema.graphql
|
||||
# timestamp: 2023-11-20T17:04:42+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
# The `Boolean` scalar type represents `true` or `false`.
|
||||
Boolean: TypeAlias = bool
|
||||
|
||||
|
||||
# The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
|
||||
ID: TypeAlias = str
|
||||
|
||||
|
||||
# The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
|
||||
String: TypeAlias = str
|
||||
|
||||
|
||||
class Author(BaseModel):
|
||||
books: Optional[List[Optional[Book]]] = Field(default_factory=list)
|
||||
id: ID
|
||||
name: Optional[String] = None
|
||||
typename__: Optional[Literal['Author']] = Field('Author', alias='__typename')
|
||||
|
||||
|
||||
class Book(BaseModel):
|
||||
author: Optional[Author] = None
|
||||
id: ID
|
||||
title: Optional[String] = None
|
||||
typename__: Optional[Literal['Book']] = Field('Book', alias='__typename')
|
||||
|
||||
|
||||
class AuthorBooksInput(BaseModel):
|
||||
id: ID
|
||||
typename__: Optional[Literal['AuthorBooksInput']] = Field(
|
||||
'AuthorBooksInput', alias='__typename'
|
||||
)
|
||||
|
||||
|
||||
class BooksInput(BaseModel):
|
||||
ids: List[ID]
|
||||
typename__: Optional[Literal['BooksInput']] = Field(
|
||||
'BooksInput', alias='__typename'
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
## Response deserialization
|
||||
|
||||
For the following response of `getAuthorBooks` GraphQL query
|
||||
|
||||
**response.json**
|
||||
```json
|
||||
{
|
||||
"getAuthorBooks": [
|
||||
{
|
||||
"author": {
|
||||
"id": "51341cdscwef14r13",
|
||||
"name": "J. K. Rowling"
|
||||
},
|
||||
"id": "1321dfvrt211wdw",
|
||||
"title": "Harry Potter and the Prisoner of Azkaban"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"id": "51341cdscwef14r13",
|
||||
"name": "J. K. Rowling"
|
||||
},
|
||||
"id": "dvsmu12e19xmqacqw9",
|
||||
"title": "Fantastic Beasts: The Crimes of Grindelwald"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**main.py**
|
||||
```python
|
||||
from model import Book
|
||||
|
||||
response = {...}
|
||||
|
||||
books = [
|
||||
Book.parse_obj(book_raw) for book_raw in response["getAuthorBooks"]
|
||||
]
|
||||
print(books)
|
||||
# [Book(author=Author(books=[], id='51341cdscwef14r13', name='J. K. Rowling', typename__='Author'), id='1321dfvrt211wdw', title='Harry Potter and the Prisoner of Azkaban', typename__='Book'), Book(author=Author(books=[], id='51341cdscwef14r13', name='J. K. Rowling', typename__='Author'), id='dvsmu12e19xmqacqw9', title='Fantastic Beasts: The Crimes of Grindelwald', typename__='Book')]
|
||||
```
|
||||
|
||||
## Custom scalar types
|
||||
|
||||
```bash
|
||||
$ datamodel-codegen --input schema.graphql --input-file-type graphql --output model.py --extra-template-data data.json
|
||||
```
|
||||
|
||||
**schema.graphql**
|
||||
```graphql
|
||||
scalar Long
|
||||
|
||||
type A {
|
||||
id: ID!
|
||||
duration: Long!
|
||||
}
|
||||
```
|
||||
|
||||
**data.json**
|
||||
```json
|
||||
{
|
||||
"Long": {
|
||||
"py_type": "int"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**model.py**
|
||||
```python
|
||||
# generated by datamodel-codegen:
|
||||
# filename: custom-scalar-types.graphql
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
# The `Boolean` scalar type represents `true` or `false`.
|
||||
Boolean: TypeAlias = bool
|
||||
|
||||
|
||||
# The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
|
||||
ID: TypeAlias = str
|
||||
|
||||
|
||||
Long: TypeAlias = int
|
||||
|
||||
|
||||
# The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
|
||||
String: TypeAlias = str
|
||||
|
||||
|
||||
class A(BaseModel):
|
||||
duration: Long
|
||||
id: ID
|
||||
typename__: Optional[Literal['A']] = Field('A', alias='__typename')
|
||||
|
||||
```
|
||||
@@ -265,6 +265,7 @@ These OSS projects use datamodel-code-generator to generate many models. See the
|
||||
- JSON Schema ([JSON Schema Core](http://json-schema.org/draft/2019-09/json-schema-validation.html)/[JSON Schema Validation](http://json-schema.org/draft/2019-09/json-schema-validation.html))
|
||||
- JSON/YAML/CSV Data (it will be converted to JSON Schema)
|
||||
- Python dictionary (it will be converted to JSON Schema)
|
||||
- GraphQL schema ([GraphQL Schemas and Types](https://graphql.org/learn/schema/))
|
||||
|
||||
## Supported output types
|
||||
- [pydantic](https://docs.pydantic.dev/1.10/).BaseModel
|
||||
|
||||
@@ -24,6 +24,7 @@ nav:
|
||||
- Generate from OpenAPI: openapi.md
|
||||
- Generate from JSON Schema: jsonschema.md
|
||||
- Generate from JSON Data: jsondata.md
|
||||
- Generate from GraphQL Schema: graphql.md
|
||||
- Custom template: custom_template.md
|
||||
- Using as module: using_as_module.md
|
||||
- Formatting: formatting.md
|
||||
|
||||
18
poetry.lock
generated
18
poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -537,6 +537,20 @@ files = [
|
||||
{file = "genson-1.2.2.tar.gz", hash = "sha256:8caf69aa10af7aee0e1a1351d1d06801f4696e005f06cedef438635384346a16"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.3"
|
||||
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4"
|
||||
files = [
|
||||
{file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"},
|
||||
{file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.2,<5", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
@@ -1819,4 +1833,4 @@ validation = ["openapi-spec-validator", "prance"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "b5330018b8d8a41cba8a623a307edbccc0df0144c842c7b56e01ba39f9eb3f39"
|
||||
content-hash = "746dac266eae35e424f52926437687d05b59bb241e49a7d5b18cf09a86850f8e"
|
||||
|
||||
@@ -84,6 +84,10 @@ pytest-xdist = "^3.3.1"
|
||||
prance = "*"
|
||||
openapi-spec-validator = "*"
|
||||
|
||||
|
||||
[tool.poetry.group.graphql.dependencies]
|
||||
graphql-core = "^3.2.3"
|
||||
|
||||
[tool.poetry.extras]
|
||||
http = ["httpx"]
|
||||
debug = ["PySnooper"]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: custom-scalar-types.graphql
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
# The `Boolean` scalar type represents `true` or `false`.
|
||||
Boolean: TypeAlias = bool
|
||||
|
||||
|
||||
# The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
|
||||
ID: TypeAlias = str
|
||||
|
||||
|
||||
Long: TypeAlias = int
|
||||
|
||||
|
||||
# The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
|
||||
String: TypeAlias = str
|
||||
|
||||
|
||||
class A(BaseModel):
|
||||
duration: Long
|
||||
id: ID
|
||||
typename__: Optional[Literal['A']] = Field('A', alias='__typename')
|
||||
@@ -0,0 +1,42 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: different-types-of-fields.graphql
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
# The `Boolean` scalar type represents `true` or `false`.
|
||||
Boolean: TypeAlias = bool
|
||||
|
||||
|
||||
# The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
|
||||
String: TypeAlias = str
|
||||
|
||||
|
||||
class A(BaseModel):
|
||||
field: String
|
||||
listField: List[String]
|
||||
listListField: List[List[String]]
|
||||
listListOptionalField: List[List[Optional[String]]]
|
||||
listOptionalField: List[Optional[String]]
|
||||
listOptionalListField: List[Optional[List[String]]]
|
||||
listOptionalListOptionalField: List[Optional[List[Optional[String]]]]
|
||||
optionalField: Optional[String] = None
|
||||
optionalListListField: Optional[List[List[String]]] = Field(default_factory=list)
|
||||
optionalListListOptionalField: Optional[List[List[Optional[String]]]] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
optionalListOptionalField: Optional[List[Optional[String]]] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
optionalListOptionalListField: Optional[List[Optional[List[String]]]] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
optionalListOptionalListOptionalField: Optional[
|
||||
List[Optional[List[Optional[String]]]]
|
||||
] = Field(default_factory=list)
|
||||
typename__: Optional[Literal['A']] = Field('A', alias='__typename')
|
||||
@@ -0,0 +1,28 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: field-aliases.graphql
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
# The `Boolean` scalar type represents `true` or `false`.
|
||||
Boolean: TypeAlias = bool
|
||||
|
||||
|
||||
DateTime: TypeAlias = str
|
||||
|
||||
|
||||
# The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
|
||||
String: TypeAlias = str
|
||||
|
||||
|
||||
class DateTimePeriod(BaseModel):
|
||||
periodFrom: DateTime = Field(..., alias='from')
|
||||
periodTo: DateTime = Field(..., alias='to')
|
||||
typename__: Optional[Literal['DateTimePeriod']] = Field(
|
||||
'DateTimePeriod', alias='__typename'
|
||||
)
|
||||
147
tests/data/expected/main/main_graphql_simple_star_wars/output.py
Normal file
147
tests/data/expected/main/main_graphql_simple_star_wars/output.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: simple-star-wars.graphql
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
# The `Boolean` scalar type represents `true` or `false`.
|
||||
Boolean: TypeAlias = bool
|
||||
|
||||
|
||||
# The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
|
||||
ID: TypeAlias = str
|
||||
|
||||
|
||||
# The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
|
||||
Int: TypeAlias = int
|
||||
|
||||
|
||||
# The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
|
||||
String: TypeAlias = str
|
||||
|
||||
|
||||
class Film(BaseModel):
|
||||
characters: List[Person]
|
||||
characters_ids: List[ID]
|
||||
director: String
|
||||
episode_id: Int
|
||||
id: ID
|
||||
opening_crawl: String
|
||||
planets: List[Planet]
|
||||
planets_ids: List[ID]
|
||||
producer: Optional[String] = None
|
||||
release_date: String
|
||||
species: List[Species]
|
||||
species_ids: List[ID]
|
||||
starships: List[Starship]
|
||||
starships_ids: List[ID]
|
||||
title: String
|
||||
vehicles: List[Vehicle]
|
||||
vehicles_ids: List[ID]
|
||||
typename__: Optional[Literal['Film']] = Field('Film', alias='__typename')
|
||||
|
||||
|
||||
class Person(BaseModel):
|
||||
birth_year: Optional[String] = None
|
||||
eye_color: Optional[String] = None
|
||||
films: List[Film]
|
||||
films_ids: List[ID]
|
||||
gender: Optional[String] = None
|
||||
hair_color: Optional[String] = None
|
||||
height: Optional[Int] = None
|
||||
homeworld: Optional[Planet] = None
|
||||
homeworld_id: Optional[ID] = None
|
||||
id: ID
|
||||
mass: Optional[Int] = None
|
||||
name: String
|
||||
skin_color: Optional[String] = None
|
||||
species: List[Species]
|
||||
species_ids: List[ID]
|
||||
starships: List[Starship]
|
||||
starships_ids: List[ID]
|
||||
vehicles: List[Vehicle]
|
||||
vehicles_ids: List[ID]
|
||||
typename__: Optional[Literal['Person']] = Field('Person', alias='__typename')
|
||||
|
||||
|
||||
class Planet(BaseModel):
|
||||
climate: Optional[String] = None
|
||||
diameter: Optional[String] = None
|
||||
films: List[Film]
|
||||
films_ids: List[ID]
|
||||
gravity: Optional[String] = None
|
||||
id: ID
|
||||
name: String
|
||||
orbital_period: Optional[String] = None
|
||||
population: Optional[String] = None
|
||||
residents: List[Person]
|
||||
residents_ids: List[ID]
|
||||
rotation_period: Optional[String] = None
|
||||
surface_water: Optional[String] = None
|
||||
terrain: Optional[String] = None
|
||||
typename__: Optional[Literal['Planet']] = Field('Planet', alias='__typename')
|
||||
|
||||
|
||||
class Species(BaseModel):
|
||||
average_height: Optional[String] = None
|
||||
average_lifespan: Optional[String] = None
|
||||
classification: Optional[String] = None
|
||||
designation: Optional[String] = None
|
||||
eye_colors: Optional[String] = None
|
||||
films: List[Film]
|
||||
films_ids: List[ID]
|
||||
hair_colors: Optional[String] = None
|
||||
id: ID
|
||||
language: Optional[String] = None
|
||||
name: String
|
||||
people: List[Person]
|
||||
people_ids: List[ID]
|
||||
skin_colors: Optional[String] = None
|
||||
typename__: Optional[Literal['Species']] = Field('Species', alias='__typename')
|
||||
|
||||
|
||||
class Starship(BaseModel):
|
||||
MGLT: Optional[String] = None
|
||||
cargo_capacity: Optional[String] = None
|
||||
consumables: Optional[String] = None
|
||||
cost_in_credits: Optional[String] = None
|
||||
crew: Optional[String] = None
|
||||
films: List[Film]
|
||||
films_ids: List[ID]
|
||||
hyperdrive_rating: Optional[String] = None
|
||||
id: ID
|
||||
length: Optional[String] = None
|
||||
manufacturer: Optional[String] = None
|
||||
max_atmosphering_speed: Optional[String] = None
|
||||
model: Optional[String] = None
|
||||
name: String
|
||||
passengers: Optional[String] = None
|
||||
pilots: List[Person]
|
||||
pilots_ids: List[ID]
|
||||
starship_class: Optional[String] = None
|
||||
typename__: Optional[Literal['Starship']] = Field('Starship', alias='__typename')
|
||||
|
||||
|
||||
class Vehicle(BaseModel):
|
||||
cargo_capacity: Optional[String] = None
|
||||
consumables: Optional[String] = None
|
||||
cost_in_credits: Optional[String] = None
|
||||
crew: Optional[String] = None
|
||||
films: List[Film]
|
||||
films_ids: List[ID]
|
||||
id: ID
|
||||
length: Optional[String] = None
|
||||
manufacturer: Optional[String] = None
|
||||
max_atmosphering_speed: Optional[String] = None
|
||||
model: Optional[String] = None
|
||||
name: String
|
||||
passengers: Optional[String] = None
|
||||
pilots: List[Person]
|
||||
pilots_ids: List[ID]
|
||||
vehicle_class: Optional[String] = None
|
||||
typename__: Optional[Literal['Vehicle']] = Field('Vehicle', alias='__typename')
|
||||
6
tests/data/graphql/custom-scalar-types.graphql
Normal file
6
tests/data/graphql/custom-scalar-types.graphql
Normal file
@@ -0,0 +1,6 @@
|
||||
scalar Long
|
||||
|
||||
type A {
|
||||
id: ID!
|
||||
duration: Long!
|
||||
}
|
||||
5
tests/data/graphql/custom-scalar-types.json
Normal file
5
tests/data/graphql/custom-scalar-types.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"Long": {
|
||||
"py_type": "int"
|
||||
}
|
||||
}
|
||||
15
tests/data/graphql/different-types-of-fields.graphql
Normal file
15
tests/data/graphql/different-types-of-fields.graphql
Normal file
@@ -0,0 +1,15 @@
|
||||
type A {
|
||||
field: String!
|
||||
optionalField: String
|
||||
optionalListOptionalField: [String]
|
||||
listOptionalField: [String]!
|
||||
listField: [String!]!
|
||||
optionalListOptionalListOptionalField:[[String]]
|
||||
optionalListListOptionalField:[[String]!]
|
||||
listListOptionalField:[[String]!]!
|
||||
listOptionalListOptionalField:[[String]]!
|
||||
optionalListOptionalListField:[[String!]]
|
||||
optionalListListField:[[String!]!]
|
||||
listListField:[[String!]!]!
|
||||
listOptionalListField:[[String!]]!
|
||||
}
|
||||
6
tests/data/graphql/field-aliases.graphql
Normal file
6
tests/data/graphql/field-aliases.graphql
Normal file
@@ -0,0 +1,6 @@
|
||||
scalar DateTime
|
||||
|
||||
type DateTimePeriod {
|
||||
from: DateTime!
|
||||
to: DateTime!
|
||||
}
|
||||
4
tests/data/graphql/field-aliases.json
Normal file
4
tests/data/graphql/field-aliases.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"to": "periodTo",
|
||||
"from": "periodFrom"
|
||||
}
|
||||
142
tests/data/graphql/simple-star-wars.graphql
Normal file
142
tests/data/graphql/simple-star-wars.graphql
Normal file
@@ -0,0 +1,142 @@
|
||||
type Person {
|
||||
id: ID!
|
||||
name: String!
|
||||
height: Int
|
||||
mass: Int
|
||||
hair_color: String
|
||||
skin_color: String
|
||||
eye_color: String
|
||||
birth_year: String
|
||||
gender: String
|
||||
|
||||
# Relationships
|
||||
homeworld_id: ID
|
||||
homeworld: Planet
|
||||
species: [Species!]!
|
||||
species_ids: [ID!]!
|
||||
films: [Film!]!
|
||||
films_ids: [ID!]!
|
||||
starships: [Starship!]!
|
||||
starships_ids: [ID!]!
|
||||
vehicles: [Vehicle!]!
|
||||
vehicles_ids: [ID!]!
|
||||
}
|
||||
|
||||
type Planet {
|
||||
id: ID!
|
||||
name: String!
|
||||
rotation_period: String
|
||||
orbital_period: String
|
||||
diameter: String
|
||||
climate: String
|
||||
gravity: String
|
||||
terrain: String
|
||||
surface_water: String
|
||||
population: String
|
||||
|
||||
# Relationships
|
||||
residents: [Person!]!
|
||||
residents_ids: [ID!]!
|
||||
films: [Film!]!
|
||||
films_ids: [ID!]!
|
||||
}
|
||||
|
||||
type Species {
|
||||
id: ID!
|
||||
name: String!
|
||||
classification: String
|
||||
designation: String
|
||||
average_height: String
|
||||
skin_colors: String
|
||||
hair_colors: String
|
||||
eye_colors: String
|
||||
average_lifespan: String
|
||||
language: String
|
||||
|
||||
# Relationships
|
||||
people: [Person!]!
|
||||
people_ids: [ID!]!
|
||||
films: [Film!]!
|
||||
films_ids: [ID!]!
|
||||
}
|
||||
|
||||
type Vehicle {
|
||||
id: ID!
|
||||
name: String!
|
||||
model: String
|
||||
manufacturer: String
|
||||
cost_in_credits: String
|
||||
length: String
|
||||
max_atmosphering_speed: String
|
||||
crew: String
|
||||
passengers: String
|
||||
cargo_capacity: String
|
||||
consumables: String
|
||||
vehicle_class: String
|
||||
|
||||
# Relationships
|
||||
pilots: [Person!]!
|
||||
pilots_ids: [ID!]!
|
||||
films: [Film!]!
|
||||
films_ids: [ID!]!
|
||||
}
|
||||
|
||||
type Starship {
|
||||
id: ID!
|
||||
name: String!
|
||||
model: String
|
||||
manufacturer: String
|
||||
cost_in_credits: String
|
||||
length: String
|
||||
max_atmosphering_speed: String
|
||||
crew: String
|
||||
passengers: String
|
||||
cargo_capacity: String
|
||||
consumables: String
|
||||
hyperdrive_rating: String
|
||||
MGLT: String
|
||||
starship_class: String
|
||||
|
||||
# Relationships
|
||||
pilots: [Person!]!
|
||||
pilots_ids: [ID!]!
|
||||
films: [Film!]!
|
||||
films_ids: [ID!]!
|
||||
}
|
||||
|
||||
type Film {
|
||||
id: ID!
|
||||
title: String!
|
||||
episode_id: Int!
|
||||
opening_crawl: String!
|
||||
director: String!
|
||||
producer: String
|
||||
release_date: String!
|
||||
|
||||
# Relationships
|
||||
characters: [Person!]!
|
||||
characters_ids: [ID!]!
|
||||
planets: [Planet!]!
|
||||
planets_ids: [ID!]!
|
||||
starships: [Starship!]!
|
||||
starships_ids: [ID!]!
|
||||
vehicles: [Vehicle!]!
|
||||
vehicles_ids: [ID!]!
|
||||
species: [Species!]!
|
||||
species_ids: [ID!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
planet(id: ID!): Planet
|
||||
listPlanets(page: Int): [Planet!]!
|
||||
person(id: ID!): Person
|
||||
listPeople(page: Int): [Person!]!
|
||||
species(id: ID!): Species
|
||||
listSpecies(page: Int): [Species!]!
|
||||
film(id: ID!): Film
|
||||
listFilms(page: Int): [Film!]!
|
||||
starship(id: ID!): Starship
|
||||
listStarships(page: Int): [Starship!]!
|
||||
vehicle(id: ID!): Vehicle
|
||||
listVehicles(page: Int): [Vehicle!]!
|
||||
}
|
||||
@@ -38,6 +38,7 @@ JSON_DATA_PATH: Path = DATA_PATH / 'json'
|
||||
YAML_DATA_PATH: Path = DATA_PATH / 'yaml'
|
||||
PYTHON_DATA_PATH: Path = DATA_PATH / 'python'
|
||||
CSV_DATA_PATH: Path = DATA_PATH / 'csv'
|
||||
GRAPHQL_DATA_PATH: Path = DATA_PATH / 'graphql'
|
||||
EXPECTED_MAIN_PATH = DATA_PATH / 'expected' / 'main'
|
||||
|
||||
TIMESTAMP = '1985-10-26T01:21:00-07:00'
|
||||
@@ -6086,3 +6087,101 @@ def test_all_of_use_default():
|
||||
EXPECTED_MAIN_PATH / 'main_all_of_use_default' / 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_graphql_simple_star_wars():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(GRAPHQL_DATA_PATH / 'simple-star-wars.graphql'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--input-file-type',
|
||||
'graphql',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH / 'main_graphql_simple_star_wars' / 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_graphql_different_types_of_fields():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(GRAPHQL_DATA_PATH / 'different-types-of-fields.graphql'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--input-file-type',
|
||||
'graphql',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH
|
||||
/ 'main_graphql_different_types_of_fields'
|
||||
/ 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_graphql_custom_scalar_types():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(GRAPHQL_DATA_PATH / 'custom-scalar-types.graphql'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--input-file-type',
|
||||
'graphql',
|
||||
'--extra-template-data',
|
||||
str(GRAPHQL_DATA_PATH / 'custom-scalar-types.json'),
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH / 'main_graphql_custom_scalar_types' / 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_graphql_field_aliases():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(GRAPHQL_DATA_PATH / 'field-aliases.graphql'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--input-file-type',
|
||||
'graphql',
|
||||
'--aliases',
|
||||
str(GRAPHQL_DATA_PATH / 'field-aliases.json'),
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH / 'main_graphql_field_aliases' / 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user