diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ce613342..69bc72a7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v1 with: - python-version: '3.8' + python-version: '3.9' architecture: 'x64' - uses: actions/cache@v1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c6e638f3..aab2cff3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,10 +20,10 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-pip- - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.9 - name: Build publish run: | python -m pip install poetry poetry-dynamic-versioning diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f643e2f7..cd4f8553 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/.gitignore b/.gitignore index 7225e1d1..d75b96db 100644 --- a/.gitignore +++ b/.gitignore @@ -176,4 +176,6 @@ fabric.properties .idea +.vscode/ + version.py diff --git a/README.md b/README.md index 956d3449..2d4c7d05 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Options: --specify-tags Use along with --generate-routers to generate specific routers from given list of tags. -c, --custom-visitors PATH - A custom visitor that adds variables to the template. -d, --output-model-type Specify a Pydantic base model to use (see [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator); default is `pydantic.BaseModel`). - -p, --python-version Specify a Python version to target (default is `3.8`). + -p, --python-version Specify a Python version to target (default is `3.9`). --install-completion Install completion for the current shell. --show-completion Show completion for the current shell, to copy it or customize the installation. diff --git a/fastapi_code_generator/__init__.py b/fastapi_code_generator/__init__.py index e69de29b..c998a3db 100644 --- a/fastapi_code_generator/__init__.py +++ b/fastapi_code_generator/__init__.py @@ -0,0 +1,3 @@ +from .patches import patch_parse + +patch_parse() diff --git a/fastapi_code_generator/__main__.py b/fastapi_code_generator/__main__.py index 196c7f44..4d3e6610 100644 --- a/fastapi_code_generator/__main__.py +++ b/fastapi_code_generator/__main__.py @@ -28,7 +28,7 @@ BUILTIN_VISITOR_DIR = Path(__file__).parent / "visitors" -MODEL_PATH: Path = Path("models.py") +MODEL_PATH: Path = Path("models") def dynamic_load_module(module_path: Path) -> Any: @@ -63,7 +63,7 @@ def main( DataModelType.PydanticBaseModel.value, "--output-model-type", "-d" ), python_version: PythonVersion = typer.Option( - PythonVersion.PY_38.value, "--python-version", "-p" + PythonVersion.PY_39.value, "--python-version", "-p" ), ) -> None: input_name: str = input_file @@ -72,10 +72,7 @@ def main( with open(input_file, encoding=encoding) as f: input_text = f.read() - if model_file: - model_path = Path(model_file).with_suffix('.py') - else: - model_path = MODEL_PATH + model_path = Path(model_file) if model_file else MODEL_PATH # pragma: no cover return generate_code( input_name, @@ -119,7 +116,7 @@ def generate_code( generate_routers: Optional[bool] = None, specify_tags: Optional[str] = None, output_model_type: DataModelType = DataModelType.PydanticBaseModel, - python_version: PythonVersion = PythonVersion.PY_38, + python_version: PythonVersion = PythonVersion.PY_39, ) -> None: if not model_path: model_path = MODEL_PATH @@ -149,14 +146,16 @@ def generate_code( with chdir(output_dir): models = parser.parse() - output = output_dir / model_path if not models: # if no models (schemas), just generate an empty model file. - modules = {output: ("", input_name)} + modules = {output_dir / model_path.with_suffix('.py'): ("", input_name)} elif isinstance(models, str): - modules = {output: (models, input_name)} + modules = {output_dir / model_path.with_suffix('.py'): (models, input_name)} else: - raise Exception('Modular references are not supported in this version') + modules = { + output_dir / model_path / module_name[0]: (model.body, input_name) + for module_name, model in models.items() + } environment: Environment = Environment( loader=FileSystemLoader( diff --git a/fastapi_code_generator/parser.py b/fastapi_code_generator/parser.py index 579f6014..f9b06780 100644 --- a/fastapi_code_generator/parser.py +++ b/fastapi_code_generator/parser.py @@ -2,6 +2,7 @@ import pathlib import re +from functools import cached_property from typing import ( Any, Callable, @@ -42,7 +43,6 @@ ResponseObject, ) from datamodel_code_generator.types import DataType, DataTypeManager, StrictTypes -from datamodel_code_generator.util import cached_property from pydantic import BaseModel, ValidationInfo RE_APPLICATION_JSON_PATTERN: Pattern[str] = re.compile(r'^application/.*json$') @@ -93,16 +93,43 @@ class Argument(CachedPropertyModel): type_hint: UsefulStr default: Optional[UsefulStr] = None default_value: Optional[UsefulStr] = None + field: Union[DataModelField, list[DataModelField], None] = None required: bool def __str__(self) -> str: return self.argument - @cached_property + @property def argument(self) -> str: + if self.field is None: + type_hint = self.type_hint + else: + type_hint = ( + UsefulStr(self.field.type_hint) + if not isinstance(self.field, list) + else UsefulStr( + f"Union[{', '.join(field.type_hint for field in self.field)}]" + ) + ) + if self.default is None and self.required: + return f'{self.name}: {type_hint}' + return f'{self.name}: {type_hint} = {self.default}' + + @property + def snakecase(self) -> str: + if self.field is None: + type_hint = self.type_hint + else: + type_hint = ( + UsefulStr(self.field.type_hint) + if not isinstance(self.field, list) + else UsefulStr( + f"Union[{', '.join(field.type_hint for field in self.field)}]" + ) + ) if self.default is None and self.required: - return f'{self.name}: {self.type_hint}' - return f'{self.name}: {self.type_hint} = {self.default}' + return f'{stringcase.snakecase(self.name)}: {type_hint}' + return f'{stringcase.snakecase(self.name)}: {type_hint} = {self.default}' class Operation(CachedPropertyModel): @@ -114,16 +141,39 @@ class Operation(CachedPropertyModel): parameters: List[Dict[str, Any]] = [] responses: Dict[UsefulStr, Any] = {} deprecated: bool = False - imports: List[Import] = [] security: Optional[List[Dict[str, List[str]]]] = None tags: Optional[List[str]] = [] - arguments: str = '' - snake_case_arguments: str = '' request: Optional[Argument] = None response: str = '' additional_responses: Dict[Union[str, int], Dict[str, str]] = {} return_type: str = '' callbacks: Dict[UsefulStr, List["Operation"]] = {} + arguments_list: List[Argument] = [] + + @classmethod + def merge_arguments_with_union(cls, arguments: List[Argument]) -> List[Argument]: + grouped_arguments: DefaultDict[str, List[Argument]] = DefaultDict(list) + for argument in arguments: + grouped_arguments[argument.name].append(argument) + + merged_arguments = [] + for argument_list in grouped_arguments.values(): + if len(argument_list) == 1: + merged_arguments.append(argument_list[0]) + else: + argument = argument_list[0] + fields = [ + item + for arg in argument_list + if arg.field is not None + for item in ( + arg.field if isinstance(arg.field, list) else [arg.field] + ) + if item is not None + ] + argument.field = fields + merged_arguments.append(argument) + return merged_arguments @cached_property def type(self) -> UsefulStr: @@ -132,6 +182,27 @@ def type(self) -> UsefulStr: """ return self.method + @property + def arguments(self) -> str: + sorted_arguments = Operation.merge_arguments_with_union(self.arguments_list) + return ", ".join(argument.argument for argument in sorted_arguments) + + @property + def snake_case_arguments(self) -> str: + sorted_arguments = Operation.merge_arguments_with_union(self.arguments_list) + return ", ".join(argument.snakecase for argument in sorted_arguments) + + @property + def imports(self) -> Imports: + imports = Imports() + for argument in self.arguments_list: + if isinstance(argument.field, list): + for field in argument.field: + imports.append(field.data_type.import_) + elif argument.field: + imports.append(argument.field.data_type.import_) + return imports + @cached_property def root_path(self) -> UsefulStr: paths = self.path.split("/") @@ -153,7 +224,7 @@ def function_name(self) -> str: return stringcase.snakecase(name) -@snooper_to_methods(max_variable_length=None) +@snooper_to_methods() class OpenAPIParser(OpenAPIModelParser): def __init__( self, @@ -166,7 +237,7 @@ def __init__( base_class: Optional[str] = None, custom_template_dir: Optional[pathlib.Path] = None, extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]] = None, - target_python_version: PythonVersion = PythonVersion.PY_37, + target_python_version: PythonVersion = PythonVersion.PY_39, dump_resolve_reference_action: Optional[Callable[[Iterable[str]], str]] = None, validation: bool = False, field_constraints: bool = False, @@ -314,6 +385,7 @@ def get_parameter_type( default=default, # type: ignore default_value=schema.default, required=field.required, + field=field, ) def get_arguments(self, snake_case: bool, path: List[str]) -> str: @@ -347,6 +419,10 @@ def get_argument_list(self, snake_case: bool, path: List[str]) -> List[Argument] or argument.type_hint.startswith('Optional[') ) + # check if there are duplicate argument.name + argument_names = [argument.name for argument in arguments] + if len(argument_names) != len(set(argument_names)): + self.imports_for_fastapi.append(Import(from_='typing', import_="Union")) return arguments def parse_request_body( @@ -466,10 +542,7 @@ def parse_operation( resolved_path = self.model_resolver.resolve_ref(path) path_name, method = path[-2:] - self._temporary_operation['arguments'] = self.get_arguments( - snake_case=False, path=path - ) - self._temporary_operation['snake_case_arguments'] = self.get_arguments( + self._temporary_operation['arguments_list'] = self.get_argument_list( snake_case=True, path=path ) main_operation = self._temporary_operation @@ -499,11 +572,8 @@ def parse_operation( self._temporary_operation = {'_parameters': []} cb_path = path + ['callbacks', key, route, method] super().parse_operation(cb_op, cb_path) - self._temporary_operation['arguments'] = self.get_arguments( - snake_case=False, path=cb_path - ) - self._temporary_operation['snake_case_arguments'] = ( - self.get_arguments(snake_case=True, path=cb_path) + self._temporary_operation['arguments_list'] = ( + self.get_argument_list(snake_case=True, path=cb_path) ) callbacks[key].append( @@ -527,13 +597,16 @@ def _collapse_root_model(self, data_type: DataType) -> DataType: reference = data_type.reference import functools - if not ( - reference - and ( - len(reference.children) == 1 - or functools.reduce(lambda a, b: a == b, reference.children) - ) - ): + try: + if not ( + reference + and ( + len(reference.children) == 0 + or functools.reduce(lambda a, b: a == b, reference.children) + ) + ): + return data_type + except RecursionError: return data_type source = reference.source if not isinstance(source, CustomRootType): diff --git a/fastapi_code_generator/patches.py b/fastapi_code_generator/patches.py new file mode 100644 index 00000000..f3f4dd37 --- /dev/null +++ b/fastapi_code_generator/patches.py @@ -0,0 +1,257 @@ +# todo: remove this file when the following PR is merged into datamodels-code-generator: +# +# https://github.com/koxudaxi/datamodel-code-generator/pull/2379 + +import logging +from itertools import groupby +from pathlib import Path +from typing import NamedTuple, Optional, Union + +from datamodel_code_generator.format import CodeFormatter +from datamodel_code_generator.imports import IMPORT_ANNOTATIONS, Import, Imports +from datamodel_code_generator.model.base import DataModel +from datamodel_code_generator.parser import base +from datamodel_code_generator.reference import ModelResolver + +logger = logging.getLogger(__name__) + +# Save the original method before patching +original_parse = base.Parser.parse + + +def patch_parse() -> None: # noqa: C901 + def __alias_shadowed_imports( + self: base.Parser, + models: list[DataModel], + all_model_field_names: set[str], + ) -> None: + for model in models: + for model_field in model.fields: + if model_field.data_type.type in all_model_field_names: + alias = model_field.data_type.type + "_aliased" + model_field.data_type.type = alias + if model_field.data_type.import_: + model_field.data_type.import_.alias = alias + + def _parse( # noqa: PLR0912, PLR0914, PLR0915 + self: base.Parser, + with_import: Optional[bool] = True, # noqa: FBT001, FBT002 + format_: Optional[bool] = True, # noqa: FBT001, FBT002 + settings_path: Optional[Path] = None, + ) -> Union[str, dict[tuple[str, ...], base.Result]]: + self.parse_raw() + + if with_import: + self.imports.append(IMPORT_ANNOTATIONS) + + if format_: + code_formatter: Optional[CodeFormatter] = CodeFormatter( + self.target_python_version, + settings_path, + self.wrap_string_literal, + skip_string_normalization=not self.use_double_quotes, + known_third_party=self.known_third_party, + custom_formatters=self.custom_formatter, + custom_formatters_kwargs=self.custom_formatters_kwargs, + encoding=self.encoding, + formatters=self.formatters, + ) + else: + code_formatter = None + + _, sorted_data_models, require_update_action_models = base.sort_data_models( + self.results + ) + + results: dict[tuple[str, ...], base.Result] = {} + + def module_key(data_model: DataModel) -> tuple[str, ...]: + return tuple(data_model.module_path) + + def sort_key(data_model: DataModel) -> tuple[int, tuple[str, ...]]: + return (len(data_model.module_path), tuple(data_model.module_path)) + + # process in reverse order to correctly establish module levels + grouped_models = groupby( + sorted(sorted_data_models.values(), key=sort_key, reverse=True), + key=module_key, + ) + + module_models: list[tuple[tuple[str, ...], list[DataModel]]] = [] + unused_models: list[DataModel] = [] + model_to_module_models: dict[ + DataModel, tuple[tuple[str, ...], list[DataModel]] + ] = {} + module_to_import: dict[tuple[str, ...], Imports] = {} + + previous_module: tuple[str, ...] = () + for module, models in ((k, [*v]) for k, v in grouped_models): + for model in models: + model_to_module_models[model] = module, models + self._Parser__delete_duplicate_models(models) # type: ignore[attr-defined] + self._Parser__replace_duplicate_name_in_module(models) # type: ignore[attr-defined] + if len(previous_module) - len(module) > 1: + module_models.extend( + ( + previous_module[:parts], + [], + ) + for parts in range(len(previous_module) - 1, len(module), -1) + ) + module_models.append( + ( + module, + models, + ) + ) + previous_module = module + + class Processed(NamedTuple): + module: tuple[str, ...] + models: list[DataModel] + init: bool + imports: Imports + scoped_model_resolver: ModelResolver + + processed_models: list[Processed] = [] + + for module_, models in module_models: + imports = module_to_import[module_] = Imports(self.use_exact_imports) + init = False + if module_: + parent = (*module_[:-1], "__init__.py") + if parent not in results: + results[parent] = base.Result(body="") + if (*module_, "__init__.py") in results: + module = (*module_, "__init__.py") + init = True + else: + module = tuple( + part.replace("-", "_") + for part in (*module_[:-1], f"{module_[-1]}.py") + ) + else: + module = ("__init__.py",) + + all_module_fields = { + field.name + for model in models + for field in model.fields + if field.name is not None + } + scoped_model_resolver = ModelResolver(exclude_names=all_module_fields) + + self.__alias_shadowed_imports(models, all_module_fields) # type: ignore[attr-defined] + self._Parser__override_required_field(models) # type: ignore[attr-defined] + self._Parser__replace_unique_list_to_set(models) # type: ignore[attr-defined] + self._Parser__change_from_import( # type: ignore[attr-defined] + models, imports, scoped_model_resolver, init + ) + self._Parser__extract_inherited_enum(models) # type: ignore[attr-defined] + self._Parser__set_reference_default_value_to_field(models) # type: ignore[attr-defined] + self._Parser__reuse_model(models, require_update_action_models) # type: ignore[attr-defined] + self._Parser__collapse_root_models( # type: ignore[attr-defined] + models, unused_models, imports, scoped_model_resolver + ) + self._Parser__set_default_enum_member(models) # type: ignore[attr-defined] + self._Parser__sort_models(models, imports) # type: ignore[attr-defined] + self._Parser__change_field_name(models) # type: ignore[attr-defined] + self._Parser__apply_discriminator_type(models, imports) # type: ignore[attr-defined] + self._Parser__set_one_literal_on_default(models) # type: ignore[attr-defined] + + processed_models.append( + Processed(module, models, init, imports, scoped_model_resolver) + ) + + for processed_model in processed_models: + for model in processed_model.models: + processed_model.imports.append(model.imports) + + for unused_model in unused_models: + module, models = model_to_module_models[unused_model] + if unused_model in models: # pragma: no cover + imports = module_to_import[module] + imports.remove(unused_model.imports) + models.remove(unused_model) + + for processed_model in processed_models: + # postprocess imports to remove unused imports. + model_code = str("\n".join([str(m) for m in processed_model.models])) + unused_imports = [ + (from_, import_) + for from_, imports_ in processed_model.imports.items() + for import_ in imports_ + if import_ not in model_code + ] + for from_, import_ in unused_imports: + processed_model.imports.remove(Import(from_=from_, import_=import_)) + + for ( + module, + models, + init, + imports, + scoped_model_resolver, + ) in processed_models: # noqa: B007 + # process after removing unused models + self._Parser__change_imported_model_name( # type: ignore[attr-defined] + models, imports, scoped_model_resolver + ) + + for ( + module, + models, + init, + imports, + scoped_model_resolver, + ) in processed_models: # noqa: B007 + result: list[str] = [] + if models: + if with_import: + result += [str(self.imports), str(imports), "\n"] + + code = base.dump_templates(models) + result += [code] + + if self.dump_resolve_reference_action is not None: + result += [ + "\n", + self.dump_resolve_reference_action( + m.reference.short_name + for m in models + if m.path in require_update_action_models + ), + ] + if not result and not init: + continue + body = "\n".join(result) + if code_formatter: + body = code_formatter.format_code(body) + + results[module] = base.Result( + body=body, source=models[0].file_path if models else None + ) + + # retain existing behaviour + if [*results] == [("__init__.py",)]: + return results["__init__.py",].body + + results = {tuple(i.replace("-", "_") for i in k): v for k, v in results.items()} + return ( + self._Parser__postprocess_result_modules(results) # type: ignore[attr-defined] + if self.treat_dot_as_module + else { + tuple( + ( + part[: part.rfind(".")].replace(".", "_") + + part[part.rfind(".") :] + ) + for part in k + ): v + for k, v in results.items() + } + ) + + base.Parser.parse = _parse # type: ignore[method-assign] + base.Parser.__alias_shadowed_imports = __alias_shadowed_imports # type: ignore[attr-defined] + logger.info("Patched Parser.parse method.") diff --git a/fastapi_code_generator/visitors/imports.py b/fastapi_code_generator/visitors/imports.py index e7ea2225..b1b37be6 100644 --- a/fastapi_code_generator/visitors/imports.py +++ b/fastapi_code_generator/visitors/imports.py @@ -32,6 +32,9 @@ def get_imports(parser: OpenAPIParser, model_path: Path) -> Dict[str, object]: ) for from_, imports_ in parser.imports_for_fastapi.items(): imports[from_].update(imports_) + for operation in parser.operations.values(): + if operation.imports: + imports.alias.update(operation.imports.alias) return {'imports': imports} diff --git a/poetry.lock b/poetry.lock index 98e1a4fe..e8b87169 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,20 +6,19 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} - [[package]] name = "anyio" version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, @@ -33,7 +32,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.23)"] [[package]] @@ -42,6 +41,7 @@ version = "3.4.0" description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "argcomplete-3.4.0-py3-none-any.whl", hash = "sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5"}, {file = "argcomplete-3.4.0.tar.gz", hash = "sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f"}, @@ -56,6 +56,7 @@ version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, @@ -92,7 +93,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -102,6 +103,7 @@ version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, @@ -113,6 +115,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -127,10 +130,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "coverage" @@ -138,6 +143,7 @@ version = "7.5.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, @@ -197,77 +203,40 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "datamodel-code-generator" -version = "0.25.6" +version = "0.28.5" description = "Datamodel Code Generator" optional = false -python-versions = "<4.0,>=3.7" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "datamodel_code_generator-0.25.6-py3-none-any.whl", hash = "sha256:adbdb485a713a7035d7260c28d3f280e598b0eb3170d2361cb92431a44c4d154"}, - {file = "datamodel_code_generator-0.25.6.tar.gz", hash = "sha256:466f56de876947b73d4a4cd0c947ce37f63f68f2f6c0ce1477045d1e6e495da5"}, + {file = "datamodel_code_generator-0.28.5-py3-none-any.whl", hash = "sha256:f899c1da5af04b5d5b6e3edbd718c1bf3a00fc4b2fe8210cef609d93a9983e9e"}, + {file = "datamodel_code_generator-0.28.5.tar.gz", hash = "sha256:20e8b817d301d2d0bb15f436e81c97b25ad1c2ef922c99249c2444141ae15a6a"}, ] [package.dependencies] -argcomplete = ">=1.10,<4.0" +argcomplete = ">=2.10.1,<4" black = ">=19.10b0" -genson = ">=1.2.1,<2.0" -httpx = {version = "*", optional = true, markers = "extra == \"http\""} -inflect = ">=4.1.0,<6.0" -isort = ">=4.3.21,<6.0" -jinja2 = ">=2.10.1,<4.0" +genson = ">=1.2.1,<2" +httpx = {version = ">=0.24.1", optional = true, markers = "extra == \"http\""} +inflect = ">=4.1,<6" +isort = ">=4.3.21,<7" +jinja2 = ">=2.10.1,<4" packaging = "*" -pydantic = [ - {version = ">=1.10.0,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, - {version = ">=1.10.0,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.5.1,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version < \"3.10\""}, - {version = ">=1.9.0,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, -] +pydantic = ">=1.5" pyyaml = ">=6.0.1" -toml = {version = ">=0.10.0,<1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -debug = ["PySnooper (>=0.4.1,<2.0.0)"] -graphql = ["graphql-core (>=3.2.3,<4.0.0)"] -http = ["httpx"] -validation = ["openapi-spec-validator (>=0.2.8,<0.7.0)", "prance (>=0.18.2)"] - -[[package]] -name = "dnspython" -version = "2.6.1" -description = "DNS toolkit" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, - {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, -] +tomli = {version = ">=2.2.1,<3", markers = "python_version <= \"3.11\""} [package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=41)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=0.9.25)"] -idna = ["idna (>=3.6)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "email-validator" -version = "2.1.2" -description = "A robust email address syntax and deliverability validation library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "email_validator-2.1.2-py3-none-any.whl", hash = "sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115"}, - {file = "email_validator-2.1.2.tar.gz", hash = "sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c"}, -] - -[package.dependencies] -dnspython = ">=2.0.0" -idna = ">=2.0.0" +all = ["graphql-core (>=3.2.3)", "httpx (>=0.24.1)", "openapi-spec-validator (>=0.2.8,<0.7)", "prance (>=0.18.2)", "pysnooper (>=0.4.1,<2)", "ruff (>=0.9.10)"] +debug = ["pysnooper (>=0.4.1,<2)"] +graphql = ["graphql-core (>=3.2.3)"] +http = ["httpx (>=0.24.1)"] +ruff = ["ruff (>=0.9.10)"] +validation = ["openapi-spec-validator (>=0.2.8,<0.7)", "prance (>=0.18.2)"] [[package]] name = "exceptiongroup" @@ -275,6 +244,8 @@ version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, @@ -289,6 +260,7 @@ version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, @@ -303,6 +275,7 @@ version = "1.3.0" description = "GenSON is a powerful, user-friendly JSON Schema generator." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7"}, {file = "genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37"}, @@ -314,6 +287,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -325,6 +299,7 @@ version = "1.0.5" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, @@ -346,6 +321,7 @@ version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, @@ -359,7 +335,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -370,6 +346,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -381,6 +358,7 @@ version = "5.6.2" description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "inflect-5.6.2-py3-none-any.whl", hash = "sha256:b45d91a4a28a4e617ff1821117439b06eaa86e2a4573154af0149e9be6687238"}, {file = "inflect-5.6.2.tar.gz", hash = "sha256:aadc7ed73928f5e014129794bbac03058cca35d0a973a5fc4eb45c7fa26005f9"}, @@ -388,7 +366,7 @@ files = [ [package.extras] docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pygments", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +testing = ["pygments", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "iniconfig" @@ -396,6 +374,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -407,6 +386,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["main", "dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -421,6 +401,7 @@ version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, @@ -438,6 +419,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -462,6 +444,7 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -531,6 +514,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -542,6 +526,7 @@ version = "1.10.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, @@ -589,6 +574,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["main", "dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -600,6 +586,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -611,6 +598,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -622,6 +610,7 @@ version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, @@ -638,6 +627,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -653,6 +643,7 @@ version = "2.8.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, @@ -660,12 +651,8 @@ files = [ [package.dependencies] annotated-types = ">=0.4.0" -email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} pydantic-core = "2.20.1" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} [package.extras] email = ["email-validator (>=2.0.0)"] @@ -676,6 +663,7 @@ version = "2.20.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, @@ -777,6 +765,7 @@ version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, @@ -791,6 +780,7 @@ version = "1.1.1" description = "A poor man's debugger for Python." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "PySnooper-1.1.1-py2.py3-none-any.whl", hash = "sha256:378f13d731a3e04d3d0350e5f295bdd0f1b49fc8a8b8bf2067fe1e5290bd20be"}, {file = "PySnooper-1.1.1.tar.gz", hash = "sha256:d17dc91cca1593c10230dce45e46b1d3ff0f8910f0c38e941edf6ba1260b3820"}, @@ -805,6 +795,7 @@ version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, @@ -827,6 +818,7 @@ version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -845,6 +837,7 @@ version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, @@ -862,6 +855,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -876,6 +870,7 @@ version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, @@ -936,6 +931,7 @@ version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" +groups = ["main"] files = [ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, @@ -944,7 +940,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -955,6 +950,7 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, @@ -966,6 +962,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -977,6 +974,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -988,31 +986,53 @@ version = "1.2.0" description = "String case converter." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, ] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main", "dev"] files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {main = "python_version <= \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""} [[package]] name = "typed-ast" @@ -1020,6 +1040,7 @@ version = "1.5.5" description = "a fork of Python 2 and 3 ast modules with type comment support" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, @@ -1070,6 +1091,7 @@ version = "0.12.3" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, @@ -1087,12 +1109,13 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [metadata] -lock-version = "2.0" -python-versions = "^3.8.0" -content-hash = "fc9627faa9f8f59f49a6a7e94e5f94576d2df77e39108d7ac39066814fafc933" +lock-version = "2.1" +python-versions = ">=3.9,<3.13" +content-hash = "4c877a8538b055a678c7b0389bbe91cb862a0ecbc41d8fbd843b43be7999b23d" diff --git a/pyproject.toml b/pyproject.toml index c6de822b..d3726ecc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ "Natural Language :: English", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -30,9 +29,9 @@ pattern = "^(?P\\d+\\.\\d+\\.\\d+)(-?((?P[a-zA-Z]+)\\.?(?P Union[str, HTTPValidationError]: + """ + Get Sheet + """ + pass + + +@app.post( + '/update-sheet', + response_model=str, + responses={'422': {'model': HTTPValidationError}}, +) +def update_sheet_update_sheet_post( + spreadsheet_id: Optional[SpreadsheetId] = 'none', +) -> Union[str, HTTPValidationError]: + """ + Update Sheet + """ + pass diff --git a/tests/data/expected/openapi/default_template/duplicate_anonymus_parameter/models.py b/tests/data/expected/openapi/default_template/duplicate_anonymus_parameter/models.py new file mode 100644 index 00000000..22d8094e --- /dev/null +++ b/tests/data/expected/openapi/default_template/duplicate_anonymus_parameter/models.py @@ -0,0 +1,25 @@ +# generated by fastapi-codegen: +# filename: duplicate_anonymus_parameter.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import List, Optional, Union + +from pydantic import BaseModel, Field + + +class ValidationError(BaseModel): + loc: List[Union[str, int]] = Field(..., title='Location') + msg: str = Field(..., title='Message') + type: str = Field(..., title='Error Type') + + +class SpreadsheetId(Enum): + cards = 'cards' + none = 'none' + + +class HTTPValidationError(BaseModel): + detail: Optional[List[ValidationError]] = Field(None, title='Detail') diff --git a/tests/data/expected/openapi/default_template/duplicate_param/main.py b/tests/data/expected/openapi/default_template/duplicate_param/main.py new file mode 100644 index 00000000..a40a5466 --- /dev/null +++ b/tests/data/expected/openapi/default_template/duplicate_param/main.py @@ -0,0 +1,24 @@ +# generated by fastapi-codegen: +# filename: duplicate_param.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from typing import Union + +from fastapi import FastAPI +from pydantic import constr + +app = FastAPI( + title='REST API', + version='0.0.1', + servers=[{'url': 'https://api.something.com/1'}], +) + + +@app.delete('/boards/{id}', response_model=None) +def delete_boards_id(id: Union[str, constr(regex=r'^[0-9a-fA-F]{24}$')]) -> None: + """ + Delete a Board + """ + pass diff --git a/tests/data/expected/openapi/default_template/duplicate_param/models.py b/tests/data/expected/openapi/default_template/duplicate_param/models.py new file mode 100644 index 00000000..ecb52027 --- /dev/null +++ b/tests/data/expected/openapi/default_template/duplicate_param/models.py @@ -0,0 +1,3 @@ +# generated by fastapi-codegen: +# filename: duplicate_param.yaml +# timestamp: 2020-06-19T00:00:00+00:00 diff --git a/tests/data/expected/openapi/default_template/modular_reference/main.py b/tests/data/expected/openapi/default_template/modular_reference/main.py new file mode 100644 index 00000000..f73fde10 --- /dev/null +++ b/tests/data/expected/openapi/default_template/modular_reference/main.py @@ -0,0 +1,24 @@ +# generated by fastapi-codegen: +# filename: modular_reference.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import FastAPI, Path + +from .models.pets import Pet + +app = FastAPI( + version='1.0.0', + title='modular_reference_template', + license={'name': 'MIT'}, + servers=[{'url': 'http://petstore.swagger.io/v1'}], +) + + +@app.get('/pets/{petId}', response_model=Pet, tags=['pets']) +def show_pet_by_id(pet_id: str = Path(..., alias='petId')) -> Pet: + """ + Info for a specific pet + """ + pass diff --git a/tests/data/expected/openapi/default_template/modular_reference/models/__init__.py b/tests/data/expected/openapi/default_template/modular_reference/models/__init__.py new file mode 100644 index 00000000..66a71bb0 --- /dev/null +++ b/tests/data/expected/openapi/default_template/modular_reference/models/__init__.py @@ -0,0 +1,3 @@ +# generated by fastapi-codegen: +# filename: modular_reference.yaml +# timestamp: 2020-06-19T00:00:00+00:00 diff --git a/tests/data/expected/openapi/default_template/modular_reference/models/pets.py b/tests/data/expected/openapi/default_template/modular_reference/models/pets.py new file mode 100644 index 00000000..9fcfc5a3 --- /dev/null +++ b/tests/data/expected/openapi/default_template/modular_reference/models/pets.py @@ -0,0 +1,15 @@ +# generated by fastapi-codegen: +# filename: modular_reference.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel + + +class Pet(BaseModel): + id: int + name: str + tag: Optional[str] = None diff --git a/tests/data/expected/openapi/default_template/recursion/main.py b/tests/data/expected/openapi/default_template/recursion/main.py new file mode 100644 index 00000000..a33633bf --- /dev/null +++ b/tests/data/expected/openapi/default_template/recursion/main.py @@ -0,0 +1,20 @@ +# generated by fastapi-codegen: +# filename: recursion.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import FastAPI + +from .models import ID + +app = FastAPI( + title='REST API', + version='0.0.1', + servers=[{'url': 'https://api.something.com/1'}], +) + + +@app.get('/actions/{id}', response_model=None) +def get_actions_id(id: ID) -> None: + pass diff --git a/tests/data/expected/openapi/default_template/recursion/models.py b/tests/data/expected/openapi/default_template/recursion/models.py new file mode 100644 index 00000000..f6005120 --- /dev/null +++ b/tests/data/expected/openapi/default_template/recursion/models.py @@ -0,0 +1,32 @@ +# generated by fastapi-codegen: +# filename: recursion.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, Field, constr + + +class ID(BaseModel): + __root__: constr(regex=r'^[0-9a-fA-F]{24}$') = Field( + ..., example='5abbe4b7ddc1b351ef961414' + ) + + +class Card(BaseModel): + id: Optional[ID] = None + + +class Board(BaseModel): + id: Optional[ID] = None + + +class Data(BaseModel): + card: Optional[Card] = None + board: Optional[Board] = None + + +class Action(BaseModel): + data: Optional[Data] = None diff --git a/tests/data/expected/openapi/default_template/shadowed_imports/main.py b/tests/data/expected/openapi/default_template/shadowed_imports/main.py new file mode 100644 index 00000000..fb97c255 --- /dev/null +++ b/tests/data/expected/openapi/default_template/shadowed_imports/main.py @@ -0,0 +1,21 @@ +# generated by fastapi-codegen: +# filename: shadowed_imports.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from datetime import date as date_aliased +from typing import Optional + +from fastapi import FastAPI + +app = FastAPI( + title='REST API', + version='0.0.1', + servers=[{'url': 'https://api.something.com/1'}], +) + + +@app.get('/actions/', response_model=None) +def get_actions_(due: Optional[date_aliased] = None) -> None: + pass diff --git a/tests/data/expected/openapi/default_template/shadowed_imports/models.py b/tests/data/expected/openapi/default_template/shadowed_imports/models.py new file mode 100644 index 00000000..65108846 --- /dev/null +++ b/tests/data/expected/openapi/default_template/shadowed_imports/models.py @@ -0,0 +1,15 @@ +# generated by fastapi-codegen: +# filename: shadowed_imports.yaml +# timestamp: 2020-06-19T00:00:00+00:00 + +from __future__ import annotations + +from datetime import date as date_aliased +from typing import Optional + +from pydantic import BaseModel, Field + + +class MarketingOptIn(BaseModel): + optedIn: Optional[bool] = Field(None, example=False) + date: Optional[date_aliased] = Field(None, example='2018-04-26T17:03:25.155Z') diff --git a/tests/data/openapi/default_template/discriminator_in_root_with_properties.yaml b/tests/data/openapi/default_template/discriminator_in_root_with_properties.yaml new file mode 100644 index 00000000..1b9d7b1f --- /dev/null +++ b/tests/data/openapi/default_template/discriminator_in_root_with_properties.yaml @@ -0,0 +1,54 @@ +info: + title: discriminator_in_root_with_properties + version: 1.0.0 +openapi: 3.0.1 +paths: {} +servers: + - url: https://your-domain.atlassian.net +tags: [] +components: + schemas: + CustomContextVariable: + additionalProperties: false + oneOf: + - $ref: '#/components/schemas/UserContextVariable' + - $ref: '#/components/schemas/IssueContextVariable' + properties: + type: + description: Type of custom context variable. + type: string + discriminator: + mapping: + issue: '#/components/schemas/IssueContextVariable' + user: '#/components/schemas/UserContextVariable' + propertyName: type + required: + - type + type: object + IssueContextVariable: + properties: + id: + description: The issue ID. + format: int64 + type: integer + key: + description: The issue key. + type: string + type: + description: Type of custom context variable. + type: string + required: + - type + type: object + UserContextVariable: + properties: + accountId: + description: The account ID of the user. + type: string + type: + description: Type of custom context variable. + type: string + required: + - accountId + - type + type: object \ No newline at end of file diff --git a/tests/data/openapi/default_template/duplicate_anonymus_parameter.yaml b/tests/data/openapi/default_template/duplicate_anonymus_parameter.yaml new file mode 100644 index 00000000..6ea436c9 --- /dev/null +++ b/tests/data/openapi/default_template/duplicate_anonymus_parameter.yaml @@ -0,0 +1,100 @@ +openapi: 3.1.0 +info: + title: test + version: 0.1.0 +servers: + - url: http://locahost:8000 + description: Local development server +paths: + /get-sheet: + get: + summary: Get Sheet + description: Get data from a Google Sheet + operationId: get_sheet_get_sheet_get + parameters: + - name: spreadsheet_id + in: query + required: false + schema: + type: string + default: none + enum: + - cards + - none + title: Spreadsheet Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: string + title: Response Get Sheet Get Sheet Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /update-sheet: + post: + summary: Update Sheet + description: Update data in a Google Sheet within the existing spreadsheet + operationId: update_sheet_update_sheet_post + parameters: + - name: spreadsheet_id + in: query + required: false + schema: + type: string + default: none + enum: + - cards + - none + title: Spreadsheet Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: string + title: Response Update Sheet Update Sheet Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError diff --git a/tests/data/openapi/default_template/duplicate_param.yaml b/tests/data/openapi/default_template/duplicate_param.yaml new file mode 100644 index 00000000..ae948ec2 --- /dev/null +++ b/tests/data/openapi/default_template/duplicate_param.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.0 +info: + title: REST API + version: 0.0.1 +servers: + - url: https://api.something.com/1 +components: + securitySchemes: {} + schemas: + ID: + type: string + pattern: ^[0-9a-fA-F]{24}$ + example: 5abbe4b7ddc1b351ef961414 + responses: {} +paths: + /boards/{id}: + parameters: + - name: id + in: path + description: '' + required: true + schema: + $ref: '#/components/schemas/ID' + delete: + tags: [] + summary: Delete a Board + description: Delete a board. + operationId: delete-boards-id + parameters: + - name: id + in: path + description: The id of the board to delete + required: true + schema: + type: string + deprecated: false + responses: + '200': + description: Success diff --git a/tests/data/openapi/default_template/modular_reference.yaml b/tests/data/openapi/default_template/modular_reference.yaml new file mode 100644 index 00000000..61a4ed62 --- /dev/null +++ b/tests/data/openapi/default_template/modular_reference.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: modular_reference_template + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/pets.Pet' +components: + schemas: + pets.Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string diff --git a/tests/data/openapi/default_template/recursion.yaml b/tests/data/openapi/default_template/recursion.yaml new file mode 100644 index 00000000..be88f92e --- /dev/null +++ b/tests/data/openapi/default_template/recursion.yaml @@ -0,0 +1,43 @@ +openapi: 3.0.0 +info: + title: REST API + version: 0.0.1 +servers: + - url: https://api.something.com/1 +components: + schemas: + Action: + type: object + properties: + data: + type: object + properties: + card: + type: object + properties: + id: + type: string + $ref: '#/components/schemas/ID' + board: + type: object + properties: + id: + type: string + $ref: '#/components/schemas/ID' + ID: + type: string + pattern: ^[0-9a-fA-F]{24}$ + example: 5abbe4b7ddc1b351ef961414 +paths: + /actions/{id}: + parameters: + - name: id + in: path + description: The ID of the Action + required: true + schema: + $ref: '#/components/schemas/ID' + get: + responses: + '200': + description: Success diff --git a/tests/data/openapi/default_template/shadowed_imports.yaml b/tests/data/openapi/default_template/shadowed_imports.yaml new file mode 100644 index 00000000..1478f728 --- /dev/null +++ b/tests/data/openapi/default_template/shadowed_imports.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + title: REST API + version: 0.0.1 +servers: + - url: https://api.something.com/1 +components: + schemas: + marketingOptIn: + type: object + properties: + optedIn: + type: boolean + example: false + date: + type: string + format: date + example: '2018-04-26T17:03:25.155Z' +paths: + /actions/: + get: + parameters: + - name: due + in: query + description: A due date for the card + required: false + schema: + type: string + format: date + responses: + '200': + description: Success + diff --git a/tests/test_generate.py b/tests/test_generate.py index c88cac61..0ef13f3b 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -26,7 +26,11 @@ @pytest.mark.parametrize( - "oas_file", (DATA_DIR / OPEN_API_DEFAULT_TEMPLATE_DIR_NAME).glob("*.yaml") + "oas_file", + (DATA_DIR / OPEN_API_DEFAULT_TEMPLATE_DIR_NAME).glob("*.yaml"), + ids=[ + id.stem for id in (DATA_DIR / OPEN_API_DEFAULT_TEMPLATE_DIR_NAME).glob("*.yaml") + ], ) @freeze_time("2020-06-19") def test_generate_default_template(oas_file): @@ -40,8 +44,8 @@ def test_generate_default_template(oas_file): template_dir=None, ) expected_dir = EXPECTED_DIR / OPEN_API_DEFAULT_TEMPLATE_DIR_NAME / oas_file.stem - output_files = sorted(list(output_dir.glob('*'))) - expected_files = sorted(list(expected_dir.glob('*'))) + output_files = sorted(list(output_dir.glob('**/*.py'))) + expected_files = sorted(list(expected_dir.glob('**/*.py'))) assert [f.name for f in output_files] == [f.name for f in expected_files] for output_file, expected_file in zip(output_files, expected_files): assert output_file.read_text() == expected_file.read_text(), oas_file