diff --git a/turms/cli/main.py b/turms/cli/main.py index 0de05b0..3eb130d 100644 --- a/turms/cli/main.py +++ b/turms/cli/main.py @@ -11,11 +11,12 @@ scan_folder_for_single_config, load_projects_from_configpath, build_schema_from_schema_type, + build_introspection_from_schema_type ) from .watch import stream_changes from graphql import print_schema from functools import wraps - +import json click.rich_click.USE_RICH_MARKUP = True directory = os.getcwd() @@ -259,7 +260,7 @@ def watch(projects): # pragma: no cover @with_projects @click.option( "--out", - default=".schema.graphql", + default=None, help="The output file extension (will be appended to the project name)", ) @click.option( @@ -267,9 +268,24 @@ def watch(projects): # pragma: no cover default=None, help="The output directory for the schema files (will default to the current working directory)", ) -def download(projects, out, dir): +@click.option( + "--format", + default="sdl", + help="The output format for the schema files (sdl or json)", + type=click.Choice(["sdl", "introspection"]), +) +def download(projects, out, dir, format): """Download the graphql projects schema as a sdl file""" + + + default_extensions = { + "sdl": ".schema.graphql", + "introspection": ".introspection.json", + } + + out = out or default_extensions.get(format, ".schema.graphql") + try: app_directory = dir or os.getcwd() for key, project in projects.items(): @@ -277,14 +293,24 @@ def download(projects, out, dir): get_console().print( f"Downloading schema for project {key} to {app_directory}/{filename}" ) - schema = build_schema_from_schema_type( - project.schema_url, allow_introspection=True - ) + if format == "sdl": + schema = build_schema_from_schema_type( + project.schema_url, allow_introspection=True + ) + output = print_schema(schema) + + else: + introspection = build_introspection_from_schema_type( + project.schema_url + ) + output = json.dumps(introspection, indent=2) + + with open(os.path.join(app_directory, filename), "w") as f: - f.write(print_schema(schema)) + f.write(output) + except Exception as e: raise click.ClickException(str(e)) from e - if __name__ == "__main__": cli() diff --git a/turms/plugins/inputs.py b/turms/plugins/inputs.py index c55e53d..5179761 100644 --- a/turms/plugins/inputs.py +++ b/turms/plugins/inputs.py @@ -30,11 +30,145 @@ class InputsPluginConfig(PluginConfig): inputtype_bases: List[str] = ["pydantic.BaseModel"] skip_underscore: bool = True skip_unreferenced: bool = True + arguments_allow_population_by_field_name: bool = True class Config: env_prefix = "TURMS_PLUGINS_INPUTS_" +def generate_input_config_class( + graphQLType: GraphQLTypes, + config: GeneratorConfig, + plugin_config: InputsPluginConfig, + typename: str = None, +): + """Generates the config class for a specific type + + It will append the config class to the registry, and set the frozen + attribute for the class to True, if the freeze config is enabled and + the type appears in the freeze list. + + It will also add config attributes to the class, if the type appears in + 'additional_config' in the config file. + + """ + + config_fields = [] + + if config.freeze.enabled: + if graphQLType in config.freeze.types: + if config.freeze.exclude and typename in config.freeze.exclude: + pass + elif config.freeze.include and typename not in config.freeze.include: + pass + else: + config_fields.append( + ast.Assign( + targets=[ast.Name(id="frozen", ctx=ast.Store())], + value=ast.Constant(value=True), + ) + ) + + if config.options.enabled: + if graphQLType in config.options.types: + if config.options.exclude and typename in config.options.exclude: + pass + elif config.options.include and typename not in config.options.include: + pass + else: + if config.options.allow_mutation is not None: + config_fields.append( + ast.Assign( + targets=[ast.Name(id="allow_mutation", ctx=ast.Store())], + value=ast.Constant(value=config.options.allow_mutation), + ) + ) + + if config.options.extra is not None: + config_fields.append( + ast.Assign( + targets=[ast.Name(id="extra", ctx=ast.Store())], + value=ast.Constant(value=config.options.extra), + ) + ) + + if config.options.validate_assignment is not None: + config_fields.append( + ast.Assign( + targets=[ + ast.Name(id="validate_assignment", ctx=ast.Store()) + ], + value=ast.Constant( + value=config.options.validate_assignment + ), + ) + ) + + if ( + config.options.allow_population_by_field_name is not None + or plugin_config.arguments_allow_population_by_field_name + ): + config_fields.append( + ast.Assign( + targets=[ + ast.Name( + id="allow_population_by_field_name", ctx=ast.Store() + ) + ], + value=ast.Constant( + value=config.options.allow_population_by_field_name + or plugin_config.arguments_allow_population_by_field_name + ), + ) + ) + + if config.options.orm_mode is not None: + config_fields.append( + ast.Assign( + targets=[ast.Name(id="orm_mode", ctx=ast.Store())], + value=ast.Constant(value=config.options.orm_mode), + ) + ) + + if config.options.use_enum_values is not None: + config_fields.append( + ast.Assign( + targets=[ast.Name(id="use_enum_values", ctx=ast.Store())], + value=ast.Constant(value=config.options.use_enum_values), + ) + ) + + if typename: + if typename in config.additional_config: + for key, value in config.additional_config[typename].items(): + config_fields.append( + ast.Assign( + targets=[ast.Name(id=key, ctx=ast.Store())], + value=ast.Constant(value=value), + ) + ) + + if len(config_fields) > 0: + config_fields.insert( + 0, + ast.Expr( + value=ast.Str(s="A config class"), + ), + ) + if len(config_fields) > 0: + return [ + ast.ClassDef( + name="Config", + bases=[], + keywords=[], + body=config_fields, + decorator_list=[], + ) + ] + else: + return [] + + def generate_input_annotation( type: GraphQLInputType, parent: str, @@ -236,7 +370,9 @@ def generate_inputs( decorator_list=[], keywords=[], body=fields - + generate_config_class(GraphQLTypes.INPUT, config, typename=key), + + generate_input_config_class( + GraphQLTypes.INPUT, config, plugin_config, typename=key + ), ) ) diff --git a/turms/plugins/operations.py b/turms/plugins/operations.py index 62df5f7..1b9fc5c 100644 --- a/turms/plugins/operations.py +++ b/turms/plugins/operations.py @@ -42,7 +42,7 @@ class OperationsPluginConfig(PluginConfig): operations_glob: Optional[str] create_arguments: bool = True extract_documentation: bool = True - arguments_allow_population_by_field_name: bool = False + arguments_allow_population_by_field_name: bool = True class Config: env_prefix = "TURMS_PLUGINS_OPERATIONS_" diff --git a/turms/run.py b/turms/run.py index 758abb2..d4a2f9a 100644 --- a/turms/run.py +++ b/turms/run.py @@ -230,6 +230,45 @@ def instantiate(module_path: str, **kwargs): """ return import_string(module_path)(**kwargs) +def build_introspection_from_schema_type( + schema: SchemaType +) -> GraphQLSchema: + """Builds a schema from a project + + Args: + project (GraphQLProject): The project + + Returns: + GraphQLSchema: The schema + """ + if isinstance(schema, dict): + if len(schema.values()) == 1: + key, value = list(schema.items())[0] + return load_introspection_from_url(key, value.headers) + + else: + # Multiple schemas, now we only support dsl + raise GenerationError("Multiple schemas not supported for introspection") + + if isinstance(schema, list): + if len(schema) == 1: + # Only one schema, probably because of aesthetic reasons + return build_introspection_from_schema_type( + schema[0] + ) + + else: + raise GenerationError("Multiple schemas not supported for introspection") + + if isinstance(schema, AnyHttpUrl): + return load_introspection_from_url(schema) + + if isinstance(schema, str): + return load_introspection_from_url(schema) + + + raise GenerationError("Could not build introspection with type " + str(type(schema))) + def build_schema_from_schema_type( schema: SchemaType, allow_introspection: bool = False diff --git a/turms/utils.py b/turms/utils.py index e9ab37a..1282fbe 100644 --- a/turms/utils.py +++ b/turms/utils.py @@ -119,7 +119,9 @@ def generate_typename_field( def generate_config_class( - graphQLType: GraphQLTypes, config: GeneratorConfig, typename: str = None + graphQLType: GraphQLTypes, + config: GeneratorConfig, + typename: str = None, ): """Generates the config class for a specific type