From 6b58d0bfbb8c6fd16d9b89bc7c0b317334155e01 Mon Sep 17 00:00:00 2001 From: Jordi Sanchez Date: Thu, 19 May 2022 13:29:15 -0700 Subject: [PATCH 1/5] feat: Adds support for parameter components and parameter references. --- openapi_python_client/parser/errors.py | 9 +- openapi_python_client/parser/openapi.py | 118 +++++-- .../parser/properties/__init__.py | 56 +++- .../parser/properties/schemas.py | 91 +++++- openapi_python_client/schema/__init__.py | 1 + pyproject.toml | 5 +- tests/conftest.py | 21 ++ tests/test_parser/test_openapi.py | 297 ++++++++++++++---- 8 files changed, 502 insertions(+), 96 deletions(-) diff --git a/openapi_python_client/parser/errors.py b/openapi_python_client/parser/errors.py index dfa2d54cb..d7111a7b8 100644 --- a/openapi_python_client/parser/errors.py +++ b/openapi_python_client/parser/errors.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Optional -__all__ = ["ErrorLevel", "GeneratorError", "ParseError", "PropertyError", "ValidationError"] +__all__ = ["ErrorLevel", "GeneratorError", "ParseError", "PropertyError", "ValidationError", "ParameterError"] from pydantic import BaseModel @@ -39,5 +39,12 @@ class PropertyError(ParseError): header = "Problem creating a Property: " +@dataclass +class ParameterError(ParseError): + """Error raised when there's a problem creating a Parameter.""" + + header = "Problem creating a Parameter: " + + class ValidationError(Exception): """Used internally to exit quickly from property parsing due to some internal exception.""" diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index a013551c5..cf0d4c2b9 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -12,7 +12,18 @@ from ..config import Config from ..utils import PythonIdentifier from .errors import GeneratorError, ParseError, PropertyError -from .properties import Class, EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data +from .properties import ( + Class, + EnumProperty, + ModelProperty, + Parameters, + Property, + Schemas, + build_parameters, + build_schemas, + property_from_data, +) +from .properties.schemas import parse_reference_path from .responses import Response, response_from_data _PATH_PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)}") @@ -33,8 +44,8 @@ class EndpointCollection: @staticmethod def from_data( - *, data: Dict[str, oai.PathItem], schemas: Schemas, config: Config - ) -> Tuple[Dict[utils.PythonIdentifier, "EndpointCollection"], Schemas]: + *, data: Dict[str, oai.PathItem], schemas: Schemas, parameters: Parameters, config: Config + ) -> Tuple[Dict[utils.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]: """Parse the openapi paths data to get EndpointCollections by tag""" endpoints_by_tag: Dict[utils.PythonIdentifier, EndpointCollection] = {} @@ -47,13 +58,19 @@ def from_data( continue tag = utils.PythonIdentifier(value=(operation.tags or ["default"])[0], prefix="tag") collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) - endpoint, schemas = Endpoint.from_data( - data=operation, path=path, method=method, tag=tag, schemas=schemas, config=config + endpoint, schemas, parameters = Endpoint.from_data( + data=operation, + path=path, + method=method, + tag=tag, + schemas=schemas, + parameters=parameters, + config=config, ) # Add `PathItem` parameters if not isinstance(endpoint, ParseError): - endpoint, schemas = Endpoint.add_parameters( - endpoint=endpoint, data=path_data, schemas=schemas, config=config + endpoint, schemas, parameters = Endpoint.add_parameters( + endpoint=endpoint, data=path_data, schemas=schemas, parameters=parameters, config=config ) if not isinstance(endpoint, ParseError): endpoint = Endpoint.sort_parameters(endpoint=endpoint) @@ -68,7 +85,7 @@ def from_data( collection.parse_errors.append(error) collection.endpoints.append(endpoint) - return endpoints_by_tag, schemas + return endpoints_by_tag, schemas, parameters def generate_operation_id(*, path: str, method: str) -> str: @@ -248,8 +265,13 @@ def _add_responses( # pylint: disable=too-many-return-statements @staticmethod def add_parameters( - *, endpoint: "Endpoint", data: Union[oai.Operation, oai.PathItem], schemas: Schemas, config: Config - ) -> Tuple[Union["Endpoint", ParseError], Schemas]: + *, + endpoint: "Endpoint", + data: Union[oai.Operation, oai.PathItem], + schemas: Schemas, + parameters: Parameters, + config: Config, + ) -> Tuple[Union["Endpoint", ParseError], Schemas, Parameters]: """Process the defined `parameters` for an Endpoint. Any existing parameters will be ignored, so earlier instances of a parameter take precedence. PathItem @@ -259,6 +281,7 @@ def add_parameters( endpoint: The endpoint to add parameters to. data: The Operation or PathItem to add parameters from. schemas: The cumulative Schemas of processing so far which should contain details for any references. + parameters: The cumulative Parameters of processing so far which should contain details for any references. config: User-provided config for overrides within parameters. Returns: @@ -271,9 +294,10 @@ def add_parameters( - https://swagger.io/docs/specification/paths-and-operations/ """ - endpoint = deepcopy(endpoint) if data.parameters is None: - return endpoint, schemas + return endpoint, schemas, parameters + + endpoint = deepcopy(endpoint) unique_parameters: Set[Tuple[str, oai.ParameterLocation]] = set() parameters_by_location = { @@ -283,9 +307,22 @@ def add_parameters( oai.ParameterLocation.COOKIE: endpoint.cookie_parameters, } - for param in data.parameters: - if isinstance(param, oai.Reference) or param.param_schema is None: - continue + for _param in data.parameters: + param: oai.Parameter + + if _param is None: + return ParseError(data=data, detail="Null parameter provided."), schemas, parameters + + if isinstance(_param, oai.Reference): + ref_path = parse_reference_path(_param.ref) + if isinstance(ref_path, ParseError): + return ref_path, schemas, parameters + _resolved_class = parameters.classes_by_reference.get(ref_path) + if _resolved_class is None: + return ParseError(data=data, detail=f"Reference `{ref_path}` not found."), schemas, parameters + param = _resolved_class + elif isinstance(_param, oai.Parameter): + param = _param unique_param = (param.name, param.param_in) if unique_param in unique_parameters: @@ -294,9 +331,12 @@ def add_parameters( "A unique parameter is defined by a combination of a name and location. " f"Duplicated parameters named `{param.name}` detected in `{param.param_in}`." ) - return ParseError(data=data, detail=duplication_detail), schemas + return ParseError(data=data, detail=duplication_detail), schemas, parameters unique_parameters.add(unique_param) + if param.param_schema is None: + continue + prop, new_schemas = property_from_data( name=param.name, required=param.required, @@ -305,13 +345,21 @@ def add_parameters( parent_name=endpoint.name, config=config, ) + if isinstance(prop, ParseError): - return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), schemas + return ( + ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), + schemas, + parameters, + ) + + schemas = new_schemas + location_error = prop.validate_location(param.param_in) if location_error is not None: location_error.data = param - return location_error, schemas - schemas = new_schemas + return location_error, schemas, parameters + if prop.name in parameters_by_location[param.param_in]: # This parameter was defined in the Operation, so ignore the PathItem definition continue @@ -331,6 +379,7 @@ def add_parameters( data=data, ), schemas, + parameters, ) endpoint.used_python_identifiers.add(existing_prop.python_name) prop.set_python_name(new_name=f"{param.name}_{param.param_in}", config=config) @@ -341,6 +390,7 @@ def add_parameters( detail=f"Parameters with same Python identifier `{prop.python_name}` detected", data=data ), schemas, + parameters, ) if param.param_in == oai.ParameterLocation.QUERY and (prop.nullable or not prop.required): # There is no NULL for query params, so nullable and not required are the same. @@ -350,7 +400,7 @@ def add_parameters( endpoint.used_python_identifiers.add(prop.python_name) parameters_by_location[param.param_in][prop.name] = prop - return endpoint, schemas + return endpoint, schemas, parameters @staticmethod def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]: @@ -382,8 +432,15 @@ def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]: @staticmethod def from_data( - *, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas, config: Config - ) -> Tuple[Union["Endpoint", ParseError], Schemas]: + *, + data: oai.Operation, + path: str, + method: str, + tag: str, + schemas: Schemas, + parameters: Parameters, + config: Config, + ) -> Tuple[Union["Endpoint", ParseError], Schemas, Parameters]: """Construct an endpoint from the OpenAPI data""" if data.operationId is None: @@ -401,13 +458,15 @@ def from_data( tag=tag, ) - result, schemas = Endpoint.add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config) + result, schemas, parameters = Endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=schemas, parameters=parameters, config=config + ) if isinstance(result, ParseError): - return result, schemas + return result, schemas, parameters result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas, config=config) result, schemas = Endpoint._add_body(endpoint=result, data=data, schemas=schemas, config=config) - return result, schemas + return result, schemas, parameters def response_type(self) -> str: """Get the Python type of any response from this endpoint""" @@ -459,10 +518,15 @@ def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", ) return GeneratorError(header="Failed to parse OpenAPI document", detail=detail) schemas = Schemas() + parameters = Parameters() if openapi.components and openapi.components.schemas: schemas = build_schemas(components=openapi.components.schemas, schemas=schemas, config=config) - endpoint_collections_by_tag, schemas = EndpointCollection.from_data( - data=openapi.paths, schemas=schemas, config=config + if openapi.components and openapi.components.parameters: + parameters = build_parameters( + components=openapi.components.parameters, schemas=schemas, parameters=parameters, config=config + ) + endpoint_collections_by_tag, schemas, parameters = EndpointCollection.from_data( + data=openapi.paths, schemas=schemas, parameters=parameters, config=config ) enums = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty)) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 524ff5ba0..7e4f89462 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -3,9 +3,11 @@ "Class", "EnumProperty", "ModelProperty", + "Parameters", "Property", "Schemas", "build_schemas", + "build_parameters", "property_from_data", ] @@ -17,12 +19,19 @@ from ... import Config from ... import schema as oai from ... import utils -from ..errors import ParseError, PropertyError, ValidationError +from ..errors import ParameterError, ParseError, PropertyError, ValidationError from .converter import convert, convert_chain from .enum_property import EnumProperty from .model_property import ModelProperty, build_model_property from .property import Property -from .schemas import Class, Schemas, parse_reference_path, update_schemas_with_data +from .schemas import ( + Class, + Parameters, + Schemas, + parse_reference_path, + update_parameters_with_data, + update_schemas_with_data, +) @attr.s(auto_attribs=True, frozen=True) @@ -728,3 +737,46 @@ def build_schemas( schemas.errors.extend(errors) return schemas + + +def build_parameters( + *, + components: Dict[str, Union[oai.Reference, oai.Parameter]], + parameters: Parameters, + schemas: Schemas, + config: Config, +) -> Parameters: + """Get a list of Schemas from an OpenAPI dict""" + to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Parameter]]] = [] + if components is not None: + to_process = components.items() + still_making_progress = True + errors: List[ParameterError] = [] + + # References could have forward References so keep going as long as we are making progress + while still_making_progress: + still_making_progress = False + errors = [] + next_round = [] + # Only accumulate errors from the last round, since we might fix some along the way + for name, data in to_process: + if isinstance(data, oai.Reference): + parameters.errors.append(PropertyError(data=data, detail="Reference schemas are not supported.")) + continue + ref_path = parse_reference_path(f"#/components/parameters/{name}") + if isinstance(ref_path, ParseError): + parameters.errors.append(PropertyError(detail=ref_path.detail, data=data)) + continue + parameters_or_err = update_parameters_with_data( + ref_path=ref_path, data=data, schemas=schemas, parameters=parameters, config=config + ) + if isinstance(parameters_or_err, ParameterError): + next_round.append((name, data)) + errors.append(parameters_or_err) + continue + parameters = parameters_or_err + still_making_progress = True + to_process = next_round + + parameters.errors.extend(errors) + return parameters diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 9951f149f..4eaa3840b 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -1,14 +1,15 @@ -__all__ = ["Class", "Schemas", "parse_reference_path", "update_schemas_with_data"] +__all__ = ["Class", "Schemas", "Parameters", "parse_reference_path", "update_schemas_with_data"] -from typing import TYPE_CHECKING, Dict, List, NewType, Union, cast +from typing import TYPE_CHECKING, Dict, List, NewType, Tuple, Union, cast from urllib.parse import urlparse import attr from ... import Config from ... import schema as oai +from ...schema.openapi_schema_pydantic import Parameter from ...utils import ClassName, PythonIdentifier -from ..errors import ParseError, PropertyError +from ..errors import ParameterError, ParseError, PropertyError if TYPE_CHECKING: # pragma: no cover from .property import Property @@ -105,3 +106,87 @@ def update_schemas_with_data( schemas = attr.evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference}) return schemas + + +@attr.s(auto_attribs=True, frozen=True) +class Parameters: + """Structure for containing all defined, shareable, and reusable parameters""" + + classes_by_reference: Dict[_ReferencePath, Parameter] = attr.ib(factory=dict) + classes_by_name: Dict[ClassName, Parameter] = attr.ib(factory=dict) + errors: List[ParseError] = attr.ib(factory=list) + + +def parameter_from_data( + *, + name: str, + required: bool, + data: Union[oai.Reference, oai.Parameter], + schemas: Schemas, + parameters: Parameters, + config: Config, +) -> Tuple[Union[Parameter, ParameterError], Schemas, Parameters]: + """Generates parameters from""" + from . import property_from_data + + if isinstance(data, oai.Reference): + return ParameterError("Unable to resolve another reference"), schemas, parameters + + if data.param_schema is None: + return ParameterError("Parameter has no schema"), schemas, parameters + + _, new_schemas = property_from_data( + name=name, + required=required, + data=data.param_schema, + schemas=schemas, + parent_name="", + config=config, + ) + + new_param = Parameter( + name=name, + required=required, + explode=data.explode, + style=data.style, + param_schema=data.param_schema, + param_in=data.param_in, + ) + parameters = attr.evolve(parameters, classes_by_name={**parameters.classes_by_name, name: new_param}) + return new_param, new_schemas, parameters + + +def update_parameters_with_data( + *, ref_path: _ReferencePath, data: oai.Parameter, parameters: Parameters, schemas: Schemas, config: Config +) -> Union[Parameters, ParameterError]: + """ + Update a `Schemas` using some new reference. + + Args: + ref_path: The output of `parse_reference_path` (validated $ref). + data: The schema of the thing to add to Schemas. + schemas: `Schemas` up until now. + config: User-provided config for overriding default behavior. + + Returns: + Either the updated `schemas` input or a `PropertyError` if something went wrong. + + See Also: + - https://swagger.io/docs/specification/using-ref/ + """ + param, schemas, parameters = parameter_from_data( + data=data, name=data.name, parameters=parameters, schemas=schemas, required=True, config=config + ) + + if isinstance(param, ParameterError): + param.detail = f"{param.header}: {param.detail}" + param.header = f"Unable to parse schema {ref_path}" + if isinstance(param.data, oai.Reference) and param.data.ref.endswith(ref_path): # pragma: nocover + param.detail += ( + "\n\nRecursive and circular references are not supported. " + "See https://github.com/openapi-generators/openapi-python-client/issues/466" + ) + return param + + parameters = attr.evolve(parameters, classes_by_reference={ref_path: param, **parameters.classes_by_reference}) + return parameters diff --git a/openapi_python_client/schema/__init__.py b/openapi_python_client/schema/__init__.py index 151fe298e..d3de0e493 100644 --- a/openapi_python_client/schema/__init__.py +++ b/openapi_python_client/schema/__init__.py @@ -6,6 +6,7 @@ "ParameterLocation", "DataType", "PathItem", + "Parameter", "Reference", "RequestBody", "Response", diff --git a/pyproject.toml b/pyproject.toml index 7dfcb3b75..e359dbe81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openapi-python-client" -version = "0.11.1" +version = "0.11.2" description = "Generate modern Python clients from OpenAPI" repository = "https://github.com/triaxtec/openapi-python-client" license = "MIT" @@ -84,6 +84,7 @@ exclude = ''' /( | \.git | \.venv + | env | \.mypy_cache | openapi_python_client/templates | tests/test_templates @@ -96,7 +97,7 @@ exclude = ''' [tool.isort] line_length = 120 profile = "black" -skip = [".venv", "tests/test_templates", "integration-tests"] +skip = [".venv", "tests/test_templates", "integration-tests", "env"] [tool.coverage.run] omit = ["openapi_python_client/templates/*"] diff --git a/tests/conftest.py b/tests/conftest.py index 2a683f102..a1391ab4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,8 @@ StringProperty, UnionProperty, ) +from openapi_python_client.schema.openapi_schema_pydantic import Parameter +from openapi_python_client.schema.parameter_location import ParameterLocation @pytest.fixture @@ -222,6 +224,25 @@ def _factory(**kwargs): return _factory +@pytest.fixture +def param_factory() -> Callable[..., Parameter]: + """ + This fixture surfaces in the test as a function which manufactures a Parameter with defaults. + + You can pass the same params into this as the Parameter constructor to override defaults. + """ + + def _factory(**kwargs): + kwargs = { + "name": "", + "in": ParameterLocation.QUERY, + **kwargs, + } + return Parameter(**kwargs) + + return _factory + + def _common_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]: kwargs = { "name": "test", diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 3b8d1c672..72e11596b 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -7,7 +7,7 @@ from openapi_python_client import Config, GeneratorError from openapi_python_client.parser.errors import ParseError from openapi_python_client.parser.openapi import Endpoint, EndpointCollection -from openapi_python_client.parser.properties import IntProperty, Schemas +from openapi_python_client.parser.properties import IntProperty, Parameters, Schemas MODULE_NAME = "openapi_python_client.parser.openapi" @@ -17,14 +17,17 @@ def test_from_dict(self, mocker, model_property_factory, enum_property_factory): from openapi_python_client.parser.properties import Schemas build_schemas = mocker.patch(f"{MODULE_NAME}.build_schemas") + build_parameters = mocker.patch(f"{MODULE_NAME}.build_parameters") EndpointCollection = mocker.patch(f"{MODULE_NAME}.EndpointCollection") schemas = mocker.MagicMock() schemas.classes_by_name = { "Model": model_property_factory(), "Enum": enum_property_factory(), } + parameters = Parameters() + endpoints_collections_by_tag = mocker.MagicMock() - EndpointCollection.from_data.return_value = (endpoints_collections_by_tag, schemas) + EndpointCollection.from_data.return_value = (endpoints_collections_by_tag, schemas, parameters) OpenAPI = mocker.patch(f"{MODULE_NAME}.oai.OpenAPI") openapi = OpenAPI.parse_obj.return_value openapi.openapi = mocker.MagicMock(major=3) @@ -37,8 +40,17 @@ def test_from_dict(self, mocker, model_property_factory, enum_property_factory): OpenAPI.parse_obj.assert_called_once_with(in_dict) build_schemas.assert_called_once_with(components=openapi.components.schemas, config=config, schemas=Schemas()) + build_parameters.assert_called_once_with( + components=openapi.components.parameters, + config=config, + schemas=build_schemas.return_value, + parameters=parameters, + ) EndpointCollection.from_data.assert_called_once_with( - data=openapi.paths, schemas=build_schemas.return_value, config=config + data=openapi.paths, + schemas=build_schemas.return_value, + parameters=build_parameters.return_value, + config=config, ) assert generator_data.title == openapi.info.title assert generator_data.description == openapi.info.description @@ -481,33 +493,37 @@ def test_add_parameters_handles_no_params(self): endpoint = self.make_endpoint() schemas = Schemas() + parameters = Parameters() config = MagicMock() # Just checking there's no exception here assert Endpoint.add_parameters( - endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas, config=config - ) == ( - endpoint, - schemas, - ) + endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas, parameters=parameters, config=config + ) == (endpoint, schemas, parameters) def test_add_parameters_parse_error(self, mocker): from openapi_python_client.parser.openapi import Endpoint endpoint = self.make_endpoint() initial_schemas = mocker.MagicMock() + initial_parameters = mocker.MagicMock() parse_error = ParseError(data=mocker.MagicMock()) property_schemas = mocker.MagicMock() mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=(parse_error, property_schemas)) param = oai.Parameter.construct(name="test", required=True, param_schema=mocker.MagicMock(), param_in="cookie") config = MagicMock() - result = Endpoint.add_parameters( - endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=initial_schemas, config=config + result, schemas, parameters = Endpoint.add_parameters( + endpoint=endpoint, + data=oai.Operation.construct(parameters=[param]), + schemas=initial_schemas, + parameters=initial_parameters, + config=config, ) - assert result == ( + assert (result, schemas, parameters) == ( ParseError(data=parse_error.data, detail=f"cannot parse parameter of endpoint {endpoint.name}"), initial_schemas, + initial_parameters, ) @pytest.mark.parametrize( @@ -526,13 +542,18 @@ def test_add_parameters_header_types(self, data_type, allowed): endpoint = self.make_endpoint() initial_schemas = Schemas() + parameters = Parameters() param = oai.Parameter.construct( name="test", required=True, param_schema=oai.Schema(type=data_type), param_in=oai.ParameterLocation.HEADER ) config = Config() result = Endpoint.add_parameters( - endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=initial_schemas, config=config + endpoint=endpoint, + data=oai.Operation.construct(parameters=[param]), + schemas=initial_schemas, + parameters=parameters, + config=config, ) if allowed: assert isinstance(result[0], Endpoint) @@ -548,11 +569,16 @@ def test__add_parameters_parse_error_on_non_required_path_param(self): param_in=oai.ParameterLocation.PATH, ) schemas = Schemas() + parameters = Parameters() result = Endpoint.add_parameters( - endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=schemas, config=Config() + endpoint=endpoint, + data=oai.Operation.construct(parameters=[param]), + parameters=parameters, + schemas=schemas, + config=Config(), ) - assert result == (ParseError(data=param, detail="Path parameter must be required"), schemas) + assert result == (ParseError(data=param, detail="Path parameter must be required"), schemas, parameters) def test_validation_error_when_location_not_supported(self, mocker): parsed_schemas = mocker.MagicMock() @@ -599,9 +625,12 @@ def test__add_parameters_with_location_postfix_conflict1(self, mocker, property_ ] ) initial_schemas = mocker.MagicMock() + parameters = mocker.MagicMock() config = MagicMock() - result = Endpoint.add_parameters(endpoint=endpoint, data=data, schemas=initial_schemas, config=config)[0] + result = Endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=initial_schemas, parameters=parameters, config=config + )[0] assert isinstance(result, ParseError) assert result.detail == "Parameters with same Python identifier `prop_name_path` detected" @@ -642,13 +671,16 @@ def test__add_parameters_with_location_postfix_conflict2(self, mocker, property_ ] ) initial_schemas = mocker.MagicMock() + parameters = mocker.MagicMock() config = MagicMock() - result = Endpoint.add_parameters(endpoint=endpoint, data=data, schemas=initial_schemas, config=config)[0] + result = Endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=initial_schemas, parameters=parameters, config=config + )[0] assert isinstance(result, ParseError) assert result.detail == "Parameters with same Python identifier `prop_name_path` detected" - def test__add_parameters_skips_references(self): + def test__add_parameters_handles_invalid_references(self): """References are not supported as direct params yet""" endpoint = self.make_endpoint() data = oai.Operation.construct( @@ -657,17 +689,37 @@ def test__add_parameters_skips_references(self): ] ) - (endpoint, _) = endpoint.add_parameters(endpoint=endpoint, data=data, schemas=Schemas(), config=Config()) + parameters = Parameters() + (error, _, return_parameters) = endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=Schemas(), parameters=parameters, config=Config() + ) - assert isinstance(endpoint, Endpoint) - assert ( - len(endpoint.path_parameters) - + len(endpoint.query_parameters) - + len(endpoint.cookie_parameters) - + len(endpoint.header_parameters) - == 0 + assert isinstance(error, ParseError) + assert parameters == return_parameters + + def test__add_parameters_resolves_references(self, mocker, param_factory): + """References are not supported as direct params yet""" + endpoint = self.make_endpoint() + data = oai.Operation.construct( + parameters=[ + oai.Reference.construct(ref="#components/parameters/blah"), + ] + ) + + parameters = mocker.MagicMock() + new_param = param_factory(name="blah") + parameters.classes_by_name = { + "blah": new_param, + } + parameters.classes_by_reference = {"components/parameters/blah": new_param} + + (endpoint, _, return_parameters) = endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=Schemas(), parameters=parameters, config=Config() ) + assert isinstance(endpoint, Endpoint) + assert parameters == return_parameters + def test__add_parameters_skips_params_without_schemas(self): """Params without schemas are allowed per spec, but the any type doesn't make sense as a parameter""" endpoint = self.make_endpoint() @@ -680,7 +732,9 @@ def test__add_parameters_skips_params_without_schemas(self): ] ) - (endpoint, _) = endpoint.add_parameters(endpoint=endpoint, data=data, schemas=Schemas(), config=Config()) + (endpoint, _, _) = endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=Schemas(), parameters=Parameters(), config=Config() + ) assert isinstance(endpoint, Endpoint) assert len(endpoint.path_parameters) == 0 @@ -709,7 +763,9 @@ def test__add_parameters_same_identifier_conflict(self): ] ) - (err, _) = endpoint.add_parameters(endpoint=endpoint, data=data, schemas=Schemas(), config=Config()) + (err, _, _) = endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=Schemas(), parameters=Parameters(), config=Config() + ) assert isinstance(err, ParseError) assert "param_path" in err.detail @@ -745,7 +801,9 @@ def test__add_parameters_query_optionality(self): ] ) - (endpoint, _) = endpoint.add_parameters(endpoint=endpoint, data=data, schemas=Schemas(), config=Config()) + (endpoint, _, _) = endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=Schemas(), parameters=Parameters(), config=Config() + ) assert len(endpoint.query_parameters) == 4, "Not all query params were added" for param in endpoint.query_parameters.values(): @@ -765,9 +823,12 @@ def test_add_parameters_duplicate_properties(self): ) data = oai.Operation.construct(parameters=[param, param]) schemas = Schemas() + parameters = Parameters() config = MagicMock() - result = Endpoint.add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config) + result = Endpoint.add_parameters( + endpoint=endpoint, data=data, schemas=schemas, parameters=parameters, config=config + ) assert result == ( ParseError( data=data, @@ -776,6 +837,7 @@ def test_add_parameters_duplicate_properties(self): "Duplicated parameters named `test` detected in `path`.", ), schemas, + parameters, ) def test_add_parameters_duplicate_properties_different_location(self): @@ -789,12 +851,14 @@ def test_add_parameters_duplicate_properties_different_location(self): name="test", required=True, param_schema=oai.Schema.construct(type="string"), param_in="query" ) schemas = Schemas() + parameters = Parameters() config = MagicMock() result = Endpoint.add_parameters( endpoint=endpoint, data=oai.Operation.construct(parameters=[path_param, query_param]), schemas=schemas, + parameters=parameters, config=config, )[0] assert isinstance(result, Endpoint) @@ -852,21 +916,31 @@ def test_from_data_bad_params(self, mocker): method = mocker.MagicMock() parse_error = ParseError(data=mocker.MagicMock()) return_schemas = mocker.MagicMock() - add_parameters = mocker.patch.object(Endpoint, "add_parameters", return_value=(parse_error, return_schemas)) + return_parameters = mocker.MagicMock() + add_parameters = mocker.patch.object( + Endpoint, "add_parameters", return_value=(parse_error, return_schemas, return_parameters) + ) data = oai.Operation.construct( description=mocker.MagicMock(), operationId=mocker.MagicMock(), security={"blah": "bloo"}, responses=mocker.MagicMock(), ) - inital_schemas = mocker.MagicMock() + initial_schemas = mocker.MagicMock() + parameters = Parameters() config = MagicMock() result = Endpoint.from_data( - data=data, path=path, method=method, tag="default", schemas=inital_schemas, config=config + data=data, + path=path, + method=method, + tag="default", + schemas=initial_schemas, + parameters=parameters, + config=config, ) - assert result == (parse_error, return_schemas) + assert result == (parse_error, return_schemas, return_parameters) def test_from_data_bad_responses(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -875,8 +949,9 @@ def test_from_data_bad_responses(self, mocker): method = mocker.MagicMock() parse_error = ParseError(data=mocker.MagicMock()) param_schemas = mocker.MagicMock() + return_parameters = mocker.MagicMock() add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), param_schemas) + Endpoint, "add_parameters", return_value=(mocker.MagicMock(), param_schemas, return_parameters) ) response_schemas = mocker.MagicMock() _add_responses = mocker.patch.object(Endpoint, "_add_responses", return_value=(parse_error, response_schemas)) @@ -887,13 +962,20 @@ def test_from_data_bad_responses(self, mocker): responses=mocker.MagicMock(), ) initial_schemas = mocker.MagicMock() + initial_parameters = mocker.MagicMock() config = MagicMock() result = Endpoint.from_data( - data=data, path=path, method=method, tag="default", schemas=initial_schemas, config=config + data=data, + path=path, + method=method, + tag="default", + schemas=initial_schemas, + parameters=initial_parameters, + config=config, ) - assert result == (parse_error, response_schemas) + assert result == (parse_error, response_schemas, return_parameters) def test_from_data_standard(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -902,7 +984,10 @@ def test_from_data_standard(self, mocker): method = mocker.MagicMock() param_schemas = mocker.MagicMock() param_endpoint = mocker.MagicMock() - add_parameters = mocker.patch.object(Endpoint, "add_parameters", return_value=(param_endpoint, param_schemas)) + return_parameters = mocker.MagicMock() + add_parameters = mocker.patch.object( + Endpoint, "add_parameters", return_value=(param_endpoint, param_schemas, return_parameters) + ) response_schemas = mocker.MagicMock() response_endpoint = mocker.MagicMock() _add_responses = mocker.patch.object( @@ -918,15 +1003,22 @@ def test_from_data_standard(self, mocker): responses=mocker.MagicMock(), ) initial_schemas = mocker.MagicMock() + initial_parameters = mocker.MagicMock() config = MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) endpoint = Endpoint.from_data( - data=data, path=path, method=method, tag="default", schemas=initial_schemas, config=config + data=data, + path=path, + method=method, + tag="default", + schemas=initial_schemas, + parameters=initial_parameters, + config=config, ) - assert endpoint == _add_body.return_value + assert (endpoint[0], endpoint[1]) == _add_body.return_value add_parameters.assert_called_once_with( endpoint=Endpoint( @@ -940,6 +1032,7 @@ def test_from_data_standard(self, mocker): ), data=data, schemas=initial_schemas, + parameters=initial_parameters, config=config, ) _add_responses.assert_called_once_with( @@ -955,7 +1048,7 @@ def test_from_data_no_operation_id(self, mocker): path = "/path/with/{param}/" method = "get" add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) + Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock()) ) _add_responses = mocker.patch.object( Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) @@ -970,10 +1063,13 @@ def test_from_data_no_operation_id(self, mocker): schemas = mocker.MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) config = MagicMock() + parameters = mocker.MagicMock() - result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=schemas, config=config) + endpoint, return_schemas, return_params = Endpoint.from_data( + data=data, path=path, method=method, tag="default", schemas=schemas, parameters=parameters, config=config + ) - assert result == _add_body.return_value + assert (endpoint, return_schemas) == _add_body.return_value add_parameters.assert_called_once_with( endpoint=Endpoint( @@ -988,6 +1084,7 @@ def test_from_data_no_operation_id(self, mocker): data=data, schemas=schemas, config=config, + parameters=parameters, ) _add_responses.assert_called_once_with( endpoint=add_parameters.return_value[0], @@ -1009,7 +1106,7 @@ def test_from_data_no_security(self, mocker): responses=mocker.MagicMock(), ) add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) + Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock()) ) _add_responses = mocker.patch.object( Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) @@ -1019,9 +1116,12 @@ def test_from_data_no_security(self, mocker): method = mocker.MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) schemas = mocker.MagicMock() + parameters = mocker.MagicMock() config = MagicMock() - Endpoint.from_data(data=data, path=path, method=method, tag="a", schemas=schemas, config=config) + Endpoint.from_data( + data=data, path=path, method=method, tag="a", schemas=schemas, parameters=parameters, config=config + ) add_parameters.assert_called_once_with( endpoint=Endpoint( @@ -1034,6 +1134,7 @@ def test_from_data_no_security(self, mocker): tag="a", ), data=data, + parameters=parameters, schemas=schemas, config=config, ) @@ -1099,26 +1200,52 @@ def test_from_data(self, mocker): schemas_1 = mocker.MagicMock() schemas_2 = mocker.MagicMock() schemas_3 = mocker.MagicMock() + parameters_1 = mocker.MagicMock() + parameters_2 = mocker.MagicMock() + parameters_3 = mocker.MagicMock() endpoint_from_data = mocker.patch.object( Endpoint, "from_data", - side_effect=[(endpoint_1, schemas_1), (endpoint_2, schemas_2), (endpoint_3, schemas_3)], + side_effect=[ + (endpoint_1, schemas_1, parameters_1), + (endpoint_2, schemas_2, parameters_2), + (endpoint_3, schemas_3, parameters_3), + ], ) schemas = mocker.MagicMock() + parameters = mocker.MagicMock() config = MagicMock() - result = EndpointCollection.from_data(data=data, schemas=schemas, config=config) + result = EndpointCollection.from_data(data=data, schemas=schemas, parameters=parameters, config=config) endpoint_from_data.assert_has_calls( [ mocker.call( - data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas, config=config + data=path_1_put, + path="path_1", + method="put", + tag="default", + schemas=schemas, + parameters=parameters, + config=config, ), mocker.call( - data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1, config=config + data=path_1_post, + path="path_1", + method="post", + tag="tag_2", + schemas=schemas_1, + parameters=parameters_1, + config=config, ), mocker.call( - data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2, config=config + data=path_2_get, + path="path_2", + method="get", + tag="default", + schemas=schemas_2, + parameters=parameters_2, + config=config, ), ], ) @@ -1128,6 +1255,7 @@ def test_from_data(self, mocker): "tag_2": EndpointCollection("tag_2", endpoints=[endpoint_2]), }, schemas_3, + parameters_3, ) def test_from_data_overrides_path_item_params_with_operation_params(self): @@ -1149,9 +1277,10 @@ def test_from_data_overrides_path_item_params_with_operation_params(self): ) } - collections, schemas = EndpointCollection.from_data( + collections, schemas, parameters = EndpointCollection.from_data( data=data, schemas=Schemas(), + parameters=Parameters(), config=Config(), ) collection: EndpointCollection = collections["default"] @@ -1170,30 +1299,54 @@ def test_from_data_errors(self, mocker): schemas_1 = mocker.MagicMock() schemas_2 = mocker.MagicMock() schemas_3 = mocker.MagicMock() + parameters_1 = mocker.MagicMock() + parameters_2 = mocker.MagicMock() + parameters_3 = mocker.MagicMock() endpoint_from_data = mocker.patch.object( Endpoint, "from_data", side_effect=[ - (ParseError(data="1"), schemas_1), - (ParseError(data="2"), schemas_2), - (mocker.MagicMock(errors=[ParseError(data="3")], path="path_2"), schemas_3), + (ParseError(data="1"), schemas_1, parameters_1), + (ParseError(data="2"), schemas_2, parameters_2), + (mocker.MagicMock(errors=[ParseError(data="3")], path="path_2"), schemas_3, parameters_3), ], ) schemas = mocker.MagicMock() + parameters = mocker.MagicMock() config = MagicMock() - result, result_schemas = EndpointCollection.from_data(data=data, schemas=schemas, config=config) + result, result_schemas, result_parameters = EndpointCollection.from_data( + data=data, schemas=schemas, config=config, parameters=parameters + ) endpoint_from_data.assert_has_calls( [ mocker.call( - data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas, config=config + data=path_1_put, + path="path_1", + method="put", + tag="default", + schemas=schemas, + parameters=parameters, + config=config, ), mocker.call( - data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1, config=config + data=path_1_post, + path="path_1", + method="post", + tag="tag_2", + schemas=schemas_1, + parameters=parameters_1, + config=config, ), mocker.call( - data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2, config=config + data=path_2_get, + path="path_2", + method="get", + tag="default", + schemas=schemas_2, + parameters=parameters_2, + config=config, ), ], ) @@ -1220,20 +1373,34 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): schemas_1 = mocker.MagicMock() schemas_2 = mocker.MagicMock() schemas_3 = mocker.MagicMock() + parameters_1 = mocker.MagicMock() + parameters_2 = mocker.MagicMock() + parameters_3 = mocker.MagicMock() endpoint_from_data = mocker.patch.object( Endpoint, "from_data", - side_effect=[(endpoint_1, schemas_1), (endpoint_2, schemas_2), (endpoint_3, schemas_3)], + side_effect=[ + (endpoint_1, schemas_1, parameters_1), + (endpoint_2, schemas_2, parameters_2), + (endpoint_3, schemas_3, parameters_3), + ], ) schemas = mocker.MagicMock() + parameters = mocker.MagicMock() config = MagicMock() - result = EndpointCollection.from_data(data=data, schemas=schemas, config=config) + result = EndpointCollection.from_data(data=data, schemas=schemas, parameters=parameters, config=config) endpoint_from_data.assert_has_calls( [ mocker.call( - data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas, config=config + data=path_1_put, + path="path_1", + method="put", + tag="default", + schemas=schemas, + parameters=parameters, + config=config, ), mocker.call( data=path_1_post, @@ -1241,10 +1408,17 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): method="post", tag="amf_subscription_info_document", schemas=schemas_1, + parameters=parameters_1, config=config, ), mocker.call( - data=path_2_get, path="path_2", method="get", tag="tag3_abc", schemas=schemas_2, config=config + data=path_2_get, + path="path_2", + method="get", + tag="tag3_abc", + schemas=schemas_2, + parameters=parameters_2, + config=config, ), ], ) @@ -1257,4 +1431,5 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): "tag3_abc": EndpointCollection("tag3_abc", endpoints=[endpoint_3]), }, schemas_3, + parameters_3, ) From 53912d7867f2c3f0bc779cde8fc9c2e1e1eb6d29 Mon Sep 17 00:00:00 2001 From: Jordi Sanchez Date: Fri, 8 Jul 2022 12:46:29 -0700 Subject: [PATCH 2/5] Fixes linter issues and a failing test with the latest changes. --- openapi_python_client/parser/openapi.py | 49 +++++++++---------- .../parser/properties/schemas.py | 41 +++++++++++++++- tests/test_parser/test_openapi.py | 2 +- 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index cf0d4c2b9..79fe6ae05 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -23,7 +23,7 @@ build_schemas, property_from_data, ) -from .properties.schemas import parse_reference_path +from .properties.schemas import parameter_from_reference from .responses import Response, response_from_data _PATH_PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)}") @@ -293,6 +293,8 @@ def add_parameters( - https://swagger.io/docs/specification/describing-parameters/ - https://swagger.io/docs/specification/paths-and-operations/ """ + # pylint: disable=too-many-branches, too-many-locals + # There isn't much value in breaking down this function further other than to satisfy the linter. if data.parameters is None: return endpoint, schemas, parameters @@ -307,35 +309,32 @@ def add_parameters( oai.ParameterLocation.COOKIE: endpoint.cookie_parameters, } - for _param in data.parameters: - param: oai.Parameter + for param in data.parameters: + # Obtain the parameter from the reference or just the parameter itself + param_or_error = parameter_from_reference(param=param, parameters=parameters) + if isinstance(param_or_error, ParseError): + return param_or_error, schemas, parameters + param = param_or_error - if _param is None: - return ParseError(data=data, detail="Null parameter provided."), schemas, parameters - - if isinstance(_param, oai.Reference): - ref_path = parse_reference_path(_param.ref) - if isinstance(ref_path, ParseError): - return ref_path, schemas, parameters - _resolved_class = parameters.classes_by_reference.get(ref_path) - if _resolved_class is None: - return ParseError(data=data, detail=f"Reference `{ref_path}` not found."), schemas, parameters - param = _resolved_class - elif isinstance(_param, oai.Parameter): - param = _param + if param.param_schema is None: + continue unique_param = (param.name, param.param_in) - if unique_param in unique_parameters: - duplication_detail = ( - "Parameters MUST NOT contain duplicates. " - "A unique parameter is defined by a combination of a name and location. " - f"Duplicated parameters named `{param.name}` detected in `{param.param_in}`." + if (param.name, param.param_in) in unique_parameters: + return ( + ParseError( + data=data, + detail=( + "Parameters MUST NOT contain duplicates. " + "A unique parameter is defined by a combination of a name and location. " + f"Duplicated parameters named `{param.name}` detected in `{param.param_in}`." + ), + ), + schemas, + parameters, ) - return ParseError(data=data, detail=duplication_detail), schemas, parameters - unique_parameters.add(unique_param) - if param.param_schema is None: - continue + unique_parameters.add(unique_param) prop, new_schemas = property_from_data( name=param.name, diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 4eaa3840b..f79fadf44 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -1,4 +1,11 @@ -__all__ = ["Class", "Schemas", "Parameters", "parse_reference_path", "update_schemas_with_data"] +__all__ = [ + "Class", + "Schemas", + "Parameters", + "parse_reference_path", + "update_schemas_with_data", + "parameter_from_reference", +] from typing import TYPE_CHECKING, Dict, List, NewType, Tuple, Union, cast from urllib.parse import urlparse @@ -190,3 +197,35 @@ def update_parameters_with_data( parameters = attr.evolve(parameters, classes_by_reference={ref_path: param, **parameters.classes_by_reference}) return parameters + + +def parameter_from_reference( + *, + param: Union[oai.Reference, Parameter], + parameters: Parameters, +) -> Union[Parameter, ParameterError]: + """ + Returns a Parameter from a Reference or the Parameter itself if one was provided. + + Args: + param: A parameter by `Reference`. + parameters: `Parameters` up until now. + + Returns: + Either the updated `schemas` input or a `PropertyError` if something went wrong. + + See Also: + - https://swagger.io/docs/specification/using-ref/ + """ + if isinstance(param, Parameter): + return param + + ref_path = parse_reference_path(param.ref) + + if isinstance(ref_path, ParseError): + return ParameterError(detail=ref_path.detail) + + _resolved_parameter_class = parameters.classes_by_reference.get(ref_path, None) + if _resolved_parameter_class is None: + return ParameterError(detail=f"Reference `{ref_path}` not found.") + return _resolved_parameter_class diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 72e11596b..69eb672a6 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -707,7 +707,7 @@ def test__add_parameters_resolves_references(self, mocker, param_factory): ) parameters = mocker.MagicMock() - new_param = param_factory(name="blah") + new_param = param_factory(name="blah", schema=oai.Schema.construct(nullable=False, type="string")) parameters.classes_by_name = { "blah": new_param, } From d7db6d0a8514d7cdaf71aa821236365e8c20c916 Mon Sep 17 00:00:00 2001 From: Jordi Sanchez Date: Fri, 15 Jul 2022 11:00:37 -0700 Subject: [PATCH 3/5] Code cleanup. Adds missing tests for a number of parameter-related methods. --- openapi_python_client/parser/openapi.py | 4 +- .../parser/properties/__init__.py | 12 +- .../parser/properties/schemas.py | 39 +++--- tests/test_parser/test_openapi.py | 4 +- .../test_parser/test_properties/test_init.py | 89 ++++++++++++-- .../test_properties/test_schemas.py | 113 ++++++++++++++++++ 6 files changed, 214 insertions(+), 47 deletions(-) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 79fe6ae05..b3ece0ded 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -521,9 +521,7 @@ def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", if openapi.components and openapi.components.schemas: schemas = build_schemas(components=openapi.components.schemas, schemas=schemas, config=config) if openapi.components and openapi.components.parameters: - parameters = build_parameters( - components=openapi.components.parameters, schemas=schemas, parameters=parameters, config=config - ) + parameters = build_parameters(components=openapi.components.parameters, parameters=parameters) endpoint_collections_by_tag, schemas, parameters = EndpointCollection.from_data( data=openapi.paths, schemas=schemas, parameters=parameters, config=config ) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 7e4f89462..3eb678c62 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -743,10 +743,8 @@ def build_parameters( *, components: Dict[str, Union[oai.Reference, oai.Parameter]], parameters: Parameters, - schemas: Schemas, - config: Config, ) -> Parameters: - """Get a list of Schemas from an OpenAPI dict""" + """Get a list of Parameters from an OpenAPI dict""" to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Parameter]]] = [] if components is not None: to_process = components.items() @@ -761,15 +759,13 @@ def build_parameters( # Only accumulate errors from the last round, since we might fix some along the way for name, data in to_process: if isinstance(data, oai.Reference): - parameters.errors.append(PropertyError(data=data, detail="Reference schemas are not supported.")) + parameters.errors.append(ParameterError(data=data, detail="Reference parameters are not supported.")) continue ref_path = parse_reference_path(f"#/components/parameters/{name}") if isinstance(ref_path, ParseError): - parameters.errors.append(PropertyError(detail=ref_path.detail, data=data)) + parameters.errors.append(ParameterError(detail=ref_path.detail, data=data)) continue - parameters_or_err = update_parameters_with_data( - ref_path=ref_path, data=data, schemas=schemas, parameters=parameters, config=config - ) + parameters_or_err = update_parameters_with_data(ref_path=ref_path, data=data, parameters=parameters) if isinstance(parameters_or_err, ParameterError): next_round.append((name, data)) errors.append(parameters_or_err) diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index f79fadf44..bf6e89a35 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -4,7 +4,9 @@ "Parameters", "parse_reference_path", "update_schemas_with_data", + "update_parameters_with_data", "parameter_from_reference", + "parameter_from_data", ] from typing import TYPE_CHECKING, Dict, List, NewType, Tuple, Union, cast @@ -129,27 +131,15 @@ def parameter_from_data( name: str, required: bool, data: Union[oai.Reference, oai.Parameter], - schemas: Schemas, parameters: Parameters, - config: Config, -) -> Tuple[Union[Parameter, ParameterError], Schemas, Parameters]: - """Generates parameters from""" - from . import property_from_data +) -> Tuple[Union[Parameter, ParameterError], Parameters]: + """Generates parameters from an OpenAPI Parameter spec.""" if isinstance(data, oai.Reference): - return ParameterError("Unable to resolve another reference"), schemas, parameters + return ParameterError("Unable to resolve another reference"), parameters if data.param_schema is None: - return ParameterError("Parameter has no schema"), schemas, parameters - - _, new_schemas = property_from_data( - name=name, - required=required, - data=data.param_schema, - schemas=schemas, - parent_name="", - config=config, - ) + return ParameterError("Parameter has no schema"), parameters new_param = Parameter( name=name, @@ -160,34 +150,32 @@ def parameter_from_data( param_in=data.param_in, ) parameters = attr.evolve(parameters, classes_by_name={**parameters.classes_by_name, name: new_param}) - return new_param, new_schemas, parameters + return new_param, parameters def update_parameters_with_data( - *, ref_path: _ReferencePath, data: oai.Parameter, parameters: Parameters, schemas: Schemas, config: Config + *, ref_path: _ReferencePath, data: oai.Parameter, parameters: Parameters ) -> Union[Parameters, ParameterError]: """ - Update a `Schemas` using some new reference. + Update a `Parameters` using some new reference. Args: ref_path: The output of `parse_reference_path` (validated $ref). data: The schema of the thing to add to Schemas. - schemas: `Schemas` up until now. + parameters: `Parameters` up until now. config: User-provided config for overriding default behavior. Returns: - Either the updated `schemas` input or a `PropertyError` if something went wrong. + Either the updated `parameters` input or a `PropertyError` if something went wrong. See Also: - https://swagger.io/docs/specification/using-ref/ """ - param, schemas, parameters = parameter_from_data( - data=data, name=data.name, parameters=parameters, schemas=schemas, required=True, config=config - ) + param, parameters = parameter_from_data(data=data, name=data.name, parameters=parameters, required=True) if isinstance(param, ParameterError): param.detail = f"{param.header}: {param.detail}" - param.header = f"Unable to parse schema {ref_path}" + param.header = f"Unable to parse parameter {ref_path}" if isinstance(param.data, oai.Reference) and param.data.ref.endswith(ref_path): # pragma: nocover param.detail += ( "\n\nRecursive and circular references are not supported. " @@ -196,6 +184,7 @@ def update_parameters_with_data( return param parameters = attr.evolve(parameters, classes_by_reference={ref_path: param, **parameters.classes_by_reference}) + print("ref_path is: %s", ref_path) return parameters diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 69eb672a6..4ff372c05 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -42,8 +42,6 @@ def test_from_dict(self, mocker, model_property_factory, enum_property_factory): build_schemas.assert_called_once_with(components=openapi.components.schemas, config=config, schemas=Schemas()) build_parameters.assert_called_once_with( components=openapi.components.parameters, - config=config, - schemas=build_schemas.return_value, parameters=parameters, ) EndpointCollection.from_data.assert_called_once_with( @@ -63,10 +61,12 @@ def test_from_dict(self, mocker, model_property_factory, enum_property_factory): # Test no components openapi.components = None build_schemas.reset_mock() + build_parameters.reset_mock() GeneratorData.from_dict(in_dict, config=config) build_schemas.assert_not_called() + build_parameters.assert_not_called() def test_from_dict_invalid_schema(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 3d2de6519..3c063365e 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -6,15 +6,8 @@ import openapi_python_client.schema as oai from openapi_python_client import Config -from openapi_python_client.parser.errors import PropertyError, ValidationError -from openapi_python_client.parser.properties import ( - BooleanProperty, - FloatProperty, - IntProperty, - NoneProperty, - Property, - Schemas, -) +from openapi_python_client.parser.errors import ParameterError, PropertyError, ValidationError +from openapi_python_client.parser.properties import BooleanProperty, FloatProperty, IntProperty, Property, Schemas MODULE_NAME = "openapi_python_client.parser.properties" @@ -995,6 +988,84 @@ def test_retries_failing_properties_while_making_progress(self, mocker): assert result.errors == [PropertyError()] +class TestBuildParameters: + def test_skips_references_and_keeps_going(self, mocker): + from openapi_python_client.parser.properties import Parameters, build_parameters + from openapi_python_client.schema import Parameter, Reference + + parameters = { + "reference": Reference(ref="#/components/parameters/another_parameter"), + "defined": Parameter( + name="page", + param_in="query", + required=False, + style="form", + explode=True, + schema=oai.Schema(type="integer", default=0), + ), + } + + update_parameters_with_data = mocker.patch(f"{MODULE_NAME}.update_parameters_with_data") + parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") + + result = build_parameters(components=parameters, parameters=Parameters()) + # Should not even try to parse a path for the Reference + parse_reference_path.assert_called_once_with("#/components/parameters/defined") + update_parameters_with_data.assert_called_once_with( + ref_path=parse_reference_path.return_value, + data=parameters["defined"], + parameters=Parameters( + errors=[ParameterError(detail="Reference parameters are not supported.", data=parameters["reference"])] + ), + ) + assert result == update_parameters_with_data.return_value + + def test_records_bad_uris_and_keeps_going(self, mocker): + from openapi_python_client.parser.properties import Parameters, build_parameters + from openapi_python_client.schema import Parameter + + parameters = {"first": Parameter.construct(), "second": Parameter.construct()} + update_parameters_with_data = mocker.patch(f"{MODULE_NAME}.update_parameters_with_data") + parse_reference_path = mocker.patch( + f"{MODULE_NAME}.parse_reference_path", side_effect=[ParameterError(detail="some details"), "a_path"] + ) + + result = build_parameters(components=parameters, parameters=Parameters()) + parse_reference_path.assert_has_calls( + [ + call("#/components/parameters/first"), + call("#/components/parameters/second"), + ] + ) + update_parameters_with_data.assert_called_once_with( + ref_path="a_path", + data=parameters["second"], + parameters=Parameters(errors=[ParameterError(detail="some details", data=parameters["first"])]), + ) + assert result == update_parameters_with_data.return_value + + def test_retries_failing_parameters_while_making_progress(self, mocker): + from openapi_python_client.parser.properties import Parameters, build_parameters + from openapi_python_client.schema import Parameter + + parameters = {"first": Parameter.construct(), "second": Parameter.construct()} + update_parameters_with_data = mocker.patch( + f"{MODULE_NAME}.update_parameters_with_data", side_effect=[ParameterError(), Parameters(), ParameterError()] + ) + + parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") + result = build_parameters(components=parameters, parameters=Parameters()) + parse_reference_path.assert_has_calls( + [ + call("#/components/parameters/first"), + call("#/components/parameters/second"), + call("#/components/parameters/first"), + ] + ) + assert update_parameters_with_data.call_count == 3 + assert result.errors == [ParameterError()] + + def test_build_enum_property_conflict(): from openapi_python_client.parser.properties import Schemas, build_enum_property diff --git a/tests/test_parser/test_properties/test_schemas.py b/tests/test_parser/test_properties/test_schemas.py index 42dd6c323..629286cae 100644 --- a/tests/test_parser/test_properties/test_schemas.py +++ b/tests/test_parser/test_properties/test_schemas.py @@ -1,5 +1,12 @@ import pytest +from openapi_python_client.parser.errors import ParameterError +from openapi_python_client.parser.properties import Class, Parameters +from openapi_python_client.parser.properties.schemas import parameter_from_reference +from openapi_python_client.schema import Parameter, Reference + +MODULE_NAME = "openapi_python_client.parser.properties.schemas" + def test_class_from_string_default_config(): from openapi_python_client import Config @@ -32,3 +39,109 @@ def test_class_from_string(class_override, module_override, expected_class, expe result = Class.from_string(string=ref, config=config) assert result.name == expected_class assert result.module_name == expected_module + + +class TestParameterFromData: + def test_cannot_parse_parameters_by_reference(self): + from openapi_python_client.parser.properties import Parameters + from openapi_python_client.parser.properties.schemas import parameter_from_data + + ref = Reference.construct(ref="#/components/parameters/a_param") + parameters = Parameters() + param_or_error, new_parameters = parameter_from_data( + name="a_param", required=True, data=ref, parameters=parameters + ) + assert param_or_error == ParameterError("Unable to resolve another reference") + assert new_parameters == parameters + + def test_parameters_without_schema_are_ignored(self): + from openapi_python_client.parser.properties import Parameters + from openapi_python_client.parser.properties.schemas import parameter_from_data + from openapi_python_client.schema import ParameterLocation, Schema + + param = Parameter(name="a_schemaless_param", param_in=ParameterLocation.QUERY) + parameters = Parameters() + param_or_error, new_parameters = parameter_from_data( + name=param.name, required=param.required, data=param, parameters=parameters + ) + assert param_or_error == ParameterError("Parameter has no schema") + assert new_parameters == parameters + + def test_registers_new_parameters(self): + from openapi_python_client.parser.properties import Parameters + from openapi_python_client.parser.properties.schemas import parameter_from_data + from openapi_python_client.schema import ParameterLocation, Schema + + param = Parameter.construct(name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.construct()) + parameters = Parameters() + param_or_error, new_parameters = parameter_from_data( + name=param.name, required=param.required, data=param, parameters=parameters + ) + assert param_or_error == param + assert new_parameters.classes_by_name[param.name] == param + + +class TestParameterFromReference: + def test_returns_parameter_if_parameter_provided(self): + param = Parameter.construct() + params = Parameters() + param_or_error = parameter_from_reference(param=param, parameters=params) + assert param_or_error == param + + def test_errors_out_if_reference_not_in_parameters(self): + ref = Reference.construct(ref="#/components/parameters/a_param") + class_info = Class(name="a_param", module_name="module_name") + existing_param = Parameter.construct(name="a_param") + param_by_ref = Reference.construct(ref="#/components/parameters/another_param") + params = Parameters( + classes_by_name={class_info.name: existing_param}, classes_by_reference={ref.ref: existing_param} + ) + param_or_error = parameter_from_reference(param=param_by_ref, parameters=params) + assert param_or_error == ParameterError( + detail="Reference `/components/parameters/another_param` not found.", + ) + + def test_returns_reference_from_registry(self): + existing_param = Parameter.construct(name="a_param") + class_info = Class(name="MyParameter", module_name="module_name") + params = Parameters( + classes_by_name={class_info.name: existing_param}, + classes_by_reference={"/components/parameters/a_param": existing_param}, + ) + + param_by_ref = Reference.construct(ref="#/components/parameters/a_param") + param_or_error = parameter_from_reference(param=param_by_ref, parameters=params) + assert param_or_error == existing_param + + +class TestUpdateParametersFromData: + def test_reports_parameters_with_errors(self, mocker): + from openapi_python_client.parser.properties.schemas import update_parameters_with_data + from openapi_python_client.schema import ParameterLocation, Schema + + parameters = Parameters() + param = Parameter.construct(name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.construct()) + parameter_from_data = mocker.patch( + f"{MODULE_NAME}.parameter_from_data", side_effect=[(ParameterError(), parameters)] + ) + ref_path = Reference.construct(ref="#/components/parameters/a_param") + new_parameters_or_error = update_parameters_with_data(ref_path=ref_path.ref, data=param, parameters=parameters) + + parameter_from_data.assert_called_once() + assert new_parameters_or_error == ParameterError( + detail="Unable to parse this part of your OpenAPI document: : None", + header="Unable to parse parameter #/components/parameters/a_param", + ) + + def test_records_references_to_parameters(self, mocker): + from openapi_python_client.parser.properties.schemas import update_parameters_with_data + from openapi_python_client.schema import ParameterLocation, Schema + + parameters = Parameters() + param = Parameter.construct(name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.construct()) + parameter_from_data = mocker.patch(f"{MODULE_NAME}.parameter_from_data", side_effect=[(param, parameters)]) + ref_path = "#/components/parameters/a_param" + new_parameters = update_parameters_with_data(ref_path=ref_path, data=param, parameters=parameters) + + parameter_from_data.assert_called_once() + assert new_parameters.classes_by_reference[ref_path] == param From 1fa552a70678108919310f4bd5544c6c6af667a0 Mon Sep 17 00:00:00 2001 From: Jordi Sanchez Date: Mon, 18 Jul 2022 15:56:11 -0700 Subject: [PATCH 4/5] Removes superfluous print statement. --- openapi_python_client/parser/properties/schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index bf6e89a35..0e97c9246 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -184,7 +184,6 @@ def update_parameters_with_data( return param parameters = attr.evolve(parameters, classes_by_reference={ref_path: param, **parameters.classes_by_reference}) - print("ref_path is: %s", ref_path) return parameters From da9af679612ebe039e3b15c611ffe90007f86a2c Mon Sep 17 00:00:00 2001 From: Jordi Sanchez Date: Wed, 10 Aug 2022 21:46:42 -0700 Subject: [PATCH 5/5] Makes parameter schema errors be reported. Removes a nonexisting parameter from update_parameters_with_data's docstring. Adds an end-to-end test for parameters passed by reference. --- .../my_test_api_client/api/__init__.py | 10 + .../api/buildings/__init__.py | 14 ++ .../api/jamf_connect/__init__.py | 15 ++ .../api/buildings/__init__.py | 0 .../api/buildings/get_v1_buildings.py | 203 ++++++++++++++++ .../api/jamf_connect/__init__.py | 0 .../get_v1_jamf_connect_history.py | 198 +++++++++++++++ .../my_test_api_client/models/__init__.py | 4 + .../my_test_api_client/models/building.py | 115 +++++++++ .../models/building_search_results.py | 77 ++++++ .../models/history_search_results.py | 77 ++++++ .../models/object_history.py | 89 +++++++ end_to_end_tests/openapi.json | 230 ++++++++++++++++++ openapi_python_client/parser/openapi.py | 4 +- .../parser/properties/schemas.py | 1 - 15 files changed, 1034 insertions(+), 3 deletions(-) create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/buildings/__init__.py create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/jamf_connect/__init__.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/buildings/__init__.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/buildings/get_v1_buildings.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/jamf_connect/__init__.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/jamf_connect/get_v1_jamf_connect_history.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/building.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/building_search_results.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/history_search_results.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/object_history.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py index cd12c2407..ca3ec6d0a 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py @@ -2,7 +2,9 @@ from typing import Type +from .buildings import BuildingsEndpoints from .default import DefaultEndpoints +from .jamf_connect import JamfConnectEndpoints from .location import LocationEndpoints from .parameters import ParametersEndpoints from .responses import ResponsesEndpoints @@ -39,3 +41,11 @@ def location(cls) -> Type[LocationEndpoints]: @classmethod def true_(cls) -> Type[True_Endpoints]: return True_Endpoints + + @classmethod + def buildings(cls) -> Type[BuildingsEndpoints]: + return BuildingsEndpoints + + @classmethod + def jamf_connect(cls) -> Type[JamfConnectEndpoints]: + return JamfConnectEndpoints diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/buildings/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/buildings/__init__.py new file mode 100644 index 000000000..083758540 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/buildings/__init__.py @@ -0,0 +1,14 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from . import get_v1_buildings + + +class BuildingsEndpoints: + @classmethod + def get_v1_buildings(cls) -> types.ModuleType: + """ + Search for sorted and paged buildings + """ + return get_v1_buildings diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/jamf_connect/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/jamf_connect/__init__.py new file mode 100644 index 000000000..4bbc62d08 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/jamf_connect/__init__.py @@ -0,0 +1,15 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from . import get_v1_jamf_connect_history + + +class JamfConnectEndpoints: + @classmethod + def get_v1_jamf_connect_history(cls) -> types.ModuleType: + """ + Get Jamf Connect history + + """ + return get_v1_jamf_connect_history diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/buildings/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/buildings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/buildings/get_v1_buildings.py b/end_to_end_tests/golden-record/my_test_api_client/api/buildings/get_v1_buildings.py new file mode 100644 index 000000000..39f5b3e2a --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/buildings/get_v1_buildings.py @@ -0,0 +1,203 @@ +from typing import Any, Dict, List, Optional, Union + +import httpx + +from ...client import Client +from ...models.building_search_results import BuildingSearchResults +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: Union[Unset, None, List[str]] = UNSET, + filter_: Union[Unset, None, str] = "", +) -> Dict[str, Any]: + url = "{}/v1/buildings".format(client.base_url) + + headers: Dict[str, str] = client.get_headers() + cookies: Dict[str, Any] = client.get_cookies() + + params: Dict[str, Any] = {} + params["page"] = page + + params["page-size"] = page_size + + json_sort: Union[Unset, None, List[str]] = UNSET + if not isinstance(sort, Unset): + if sort is None: + json_sort = None + else: + json_sort = sort + + params["sort"] = json_sort + + params["filter"] = filter_ + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + return { + "method": "get", + "url": url, + "headers": headers, + "cookies": cookies, + "timeout": client.get_timeout(), + "params": params, + } + + +def _parse_response(*, response: httpx.Response) -> Optional[BuildingSearchResults]: + if response.status_code == 200: + response_200 = BuildingSearchResults.from_dict(response.json()) + + return response_200 + return None + + +def _build_response(*, response: httpx.Response) -> Response[BuildingSearchResults]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def sync_detailed( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: Union[Unset, None, List[str]] = UNSET, + filter_: Union[Unset, None, str] = "", +) -> Response[BuildingSearchResults]: + """Search for sorted and paged Buildings + + Search for sorted and paged buildings + + Args: + page (int): + page_size (int): Default: 100. + sort (Union[Unset, None, List[str]]): + filter_ (Union[Unset, None, str]): Default: ''. + + Returns: + Response[BuildingSearchResults] + """ + + kwargs = _get_kwargs( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ) + + response = httpx.request( + verify=client.verify_ssl, + **kwargs, + ) + + return _build_response(response=response) + + +def sync( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: Union[Unset, None, List[str]] = UNSET, + filter_: Union[Unset, None, str] = "", +) -> Optional[BuildingSearchResults]: + """Search for sorted and paged Buildings + + Search for sorted and paged buildings + + Args: + page (int): + page_size (int): Default: 100. + sort (Union[Unset, None, List[str]]): + filter_ (Union[Unset, None, str]): Default: ''. + + Returns: + Response[BuildingSearchResults] + """ + + return sync_detailed( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ).parsed + + +async def asyncio_detailed( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: Union[Unset, None, List[str]] = UNSET, + filter_: Union[Unset, None, str] = "", +) -> Response[BuildingSearchResults]: + """Search for sorted and paged Buildings + + Search for sorted and paged buildings + + Args: + page (int): + page_size (int): Default: 100. + sort (Union[Unset, None, List[str]]): + filter_ (Union[Unset, None, str]): Default: ''. + + Returns: + Response[BuildingSearchResults] + """ + + kwargs = _get_kwargs( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ) + + async with httpx.AsyncClient(verify=client.verify_ssl) as _client: + response = await _client.request(**kwargs) + + return _build_response(response=response) + + +async def asyncio( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: Union[Unset, None, List[str]] = UNSET, + filter_: Union[Unset, None, str] = "", +) -> Optional[BuildingSearchResults]: + """Search for sorted and paged Buildings + + Search for sorted and paged buildings + + Args: + page (int): + page_size (int): Default: 100. + sort (Union[Unset, None, List[str]]): + filter_ (Union[Unset, None, str]): Default: ''. + + Returns: + Response[BuildingSearchResults] + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/jamf_connect/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/jamf_connect/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/jamf_connect/get_v1_jamf_connect_history.py b/end_to_end_tests/golden-record/my_test_api_client/api/jamf_connect/get_v1_jamf_connect_history.py new file mode 100644 index 000000000..b76b839d4 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/jamf_connect/get_v1_jamf_connect_history.py @@ -0,0 +1,198 @@ +from typing import Any, Dict, List, Optional + +import httpx + +from ...client import Client +from ...models.history_search_results import HistorySearchResults +from ...types import UNSET, Response + + +def _get_kwargs( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: List[str], + filter_: str = "", +) -> Dict[str, Any]: + url = "{}/v1/jamf-connect/history".format(client.base_url) + + headers: Dict[str, str] = client.get_headers() + cookies: Dict[str, Any] = client.get_cookies() + + params: Dict[str, Any] = {} + params["page"] = page + + params["page-size"] = page_size + + json_sort = sort + + params["sort"] = json_sort + + params["filter"] = filter_ + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + return { + "method": "get", + "url": url, + "headers": headers, + "cookies": cookies, + "timeout": client.get_timeout(), + "params": params, + } + + +def _parse_response(*, response: httpx.Response) -> Optional[HistorySearchResults]: + if response.status_code == 200: + response_200 = HistorySearchResults.from_dict(response.json()) + + return response_200 + return None + + +def _build_response(*, response: httpx.Response) -> Response[HistorySearchResults]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def sync_detailed( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: List[str], + filter_: str = "", +) -> Response[HistorySearchResults]: + """Get Jamf Connect history + + Get Jamf Connect history + + Args: + page (int): + page_size (int): Default: 100. + sort (List[str]): + filter_ (str): Default: ''. + + Returns: + Response[HistorySearchResults] + """ + + kwargs = _get_kwargs( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ) + + response = httpx.request( + verify=client.verify_ssl, + **kwargs, + ) + + return _build_response(response=response) + + +def sync( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: List[str], + filter_: str = "", +) -> Optional[HistorySearchResults]: + """Get Jamf Connect history + + Get Jamf Connect history + + Args: + page (int): + page_size (int): Default: 100. + sort (List[str]): + filter_ (str): Default: ''. + + Returns: + Response[HistorySearchResults] + """ + + return sync_detailed( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ).parsed + + +async def asyncio_detailed( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: List[str], + filter_: str = "", +) -> Response[HistorySearchResults]: + """Get Jamf Connect history + + Get Jamf Connect history + + Args: + page (int): + page_size (int): Default: 100. + sort (List[str]): + filter_ (str): Default: ''. + + Returns: + Response[HistorySearchResults] + """ + + kwargs = _get_kwargs( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ) + + async with httpx.AsyncClient(verify=client.verify_ssl) as _client: + response = await _client.request(**kwargs) + + return _build_response(response=response) + + +async def asyncio( + *, + client: Client, + page: int = 0, + page_size: int = 100, + sort: List[str], + filter_: str = "", +) -> Optional[HistorySearchResults]: + """Get Jamf Connect history + + Get Jamf Connect history + + Args: + page (int): + page_size (int): Default: 100. + sort (List[str]): + filter_ (str): Default: ''. + + Returns: + Response[HistorySearchResults] + """ + + return ( + await asyncio_detailed( + client=client, + page=page, + page_size=page_size, + sort=sort, + filter_=filter_, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index f34a171f6..77a16d2f2 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -17,8 +17,11 @@ from .body_upload_file_tests_upload_post_some_nullable_object import BodyUploadFileTestsUploadPostSomeNullableObject from .body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject from .body_upload_file_tests_upload_post_some_optional_object import BodyUploadFileTestsUploadPostSomeOptionalObject +from .building import Building +from .building_search_results import BuildingSearchResults from .different_enum import DifferentEnum from .free_form_model import FreeFormModel +from .history_search_results import HistorySearchResults from .http_validation_error import HTTPValidationError from .import_ import Import from .model_from_all_of import ModelFromAllOf @@ -40,6 +43,7 @@ from .model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 from .model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 from .none import None_ +from .object_history import ObjectHistory from .post_responses_unions_simple_before_complex_response_200 import PostResponsesUnionsSimpleBeforeComplexResponse200 from .post_responses_unions_simple_before_complex_response_200a_type_1 import ( PostResponsesUnionsSimpleBeforeComplexResponse200AType1, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/building.py b/end_to_end_tests/golden-record/my_test_api_client/models/building.py new file mode 100644 index 000000000..6a258552e --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/building.py @@ -0,0 +1,115 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Building") + + +@attr.s(auto_attribs=True) +class Building: + """ + Attributes: + name (str): Example: Apple Park. + id (Union[Unset, str]): Example: 1. + street_address_1 (Union[Unset, None, str]): Example: The McIntosh Tree. + street_address_2 (Union[Unset, None, str]): Example: One Apple Park Way. + city (Union[Unset, None, str]): Example: Cupertino. + state_province (Union[Unset, None, str]): Example: California. + zip_postal_code (Union[Unset, None, str]): Example: 95014. + country (Union[Unset, None, str]): Example: The United States of America. + """ + + name: str + id: Union[Unset, str] = UNSET + street_address_1: Union[Unset, None, str] = UNSET + street_address_2: Union[Unset, None, str] = UNSET + city: Union[Unset, None, str] = UNSET + state_province: Union[Unset, None, str] = UNSET + zip_postal_code: Union[Unset, None, str] = UNSET + country: Union[Unset, None, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + name = self.name + id = self.id + street_address_1 = self.street_address_1 + street_address_2 = self.street_address_2 + city = self.city + state_province = self.state_province + zip_postal_code = self.zip_postal_code + country = self.country + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + } + ) + if id is not UNSET: + field_dict["id"] = id + if street_address_1 is not UNSET: + field_dict["streetAddress1"] = street_address_1 + if street_address_2 is not UNSET: + field_dict["streetAddress2"] = street_address_2 + if city is not UNSET: + field_dict["city"] = city + if state_province is not UNSET: + field_dict["stateProvince"] = state_province + if zip_postal_code is not UNSET: + field_dict["zipPostalCode"] = zip_postal_code + if country is not UNSET: + field_dict["country"] = country + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + name = d.pop("name") + + id = d.pop("id", UNSET) + + street_address_1 = d.pop("streetAddress1", UNSET) + + street_address_2 = d.pop("streetAddress2", UNSET) + + city = d.pop("city", UNSET) + + state_province = d.pop("stateProvince", UNSET) + + zip_postal_code = d.pop("zipPostalCode", UNSET) + + country = d.pop("country", UNSET) + + building = cls( + name=name, + id=id, + street_address_1=street_address_1, + street_address_2=street_address_2, + city=city, + state_province=state_province, + zip_postal_code=zip_postal_code, + country=country, + ) + + building.additional_properties = d + return building + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/building_search_results.py b/end_to_end_tests/golden-record/my_test_api_client/models/building_search_results.py new file mode 100644 index 000000000..34b523073 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/building_search_results.py @@ -0,0 +1,77 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..models.building import Building +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BuildingSearchResults") + + +@attr.s(auto_attribs=True) +class BuildingSearchResults: + """ + Attributes: + total_count (Union[Unset, int]): Example: 3. + results (Union[Unset, List[Building]]): + """ + + total_count: Union[Unset, int] = UNSET + results: Union[Unset, List[Building]] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + total_count = self.total_count + results: Union[Unset, List[Dict[str, Any]]] = UNSET + if not isinstance(self.results, Unset): + results = [] + for results_item_data in self.results: + results_item = results_item_data.to_dict() + + results.append(results_item) + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if total_count is not UNSET: + field_dict["totalCount"] = total_count + if results is not UNSET: + field_dict["results"] = results + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + total_count = d.pop("totalCount", UNSET) + + results = [] + _results = d.pop("results", UNSET) + for results_item_data in _results or []: + results_item = Building.from_dict(results_item_data) + + results.append(results_item) + + building_search_results = cls( + total_count=total_count, + results=results, + ) + + building_search_results.additional_properties = d + return building_search_results + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/history_search_results.py b/end_to_end_tests/golden-record/my_test_api_client/models/history_search_results.py new file mode 100644 index 000000000..94b0512a5 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/history_search_results.py @@ -0,0 +1,77 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..models.object_history import ObjectHistory +from ..types import UNSET, Unset + +T = TypeVar("T", bound="HistorySearchResults") + + +@attr.s(auto_attribs=True) +class HistorySearchResults: + """ + Attributes: + total_count (Union[Unset, int]): Example: 1. + results (Union[Unset, List[ObjectHistory]]): + """ + + total_count: Union[Unset, int] = UNSET + results: Union[Unset, List[ObjectHistory]] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + total_count = self.total_count + results: Union[Unset, List[Dict[str, Any]]] = UNSET + if not isinstance(self.results, Unset): + results = [] + for results_item_data in self.results: + results_item = results_item_data.to_dict() + + results.append(results_item) + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if total_count is not UNSET: + field_dict["totalCount"] = total_count + if results is not UNSET: + field_dict["results"] = results + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + total_count = d.pop("totalCount", UNSET) + + results = [] + _results = d.pop("results", UNSET) + for results_item_data in _results or []: + results_item = ObjectHistory.from_dict(results_item_data) + + results.append(results_item) + + history_search_results = cls( + total_count=total_count, + results=results, + ) + + history_search_results.additional_properties = d + return history_search_results + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/object_history.py b/end_to_end_tests/golden-record/my_test_api_client/models/object_history.py new file mode 100644 index 000000000..b59afd463 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/object_history.py @@ -0,0 +1,89 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ObjectHistory") + + +@attr.s(auto_attribs=True) +class ObjectHistory: + """ + Attributes: + id (Union[Unset, int]): Example: 1. + username (Union[Unset, str]): Example: admin. + date (Union[Unset, str]): Example: 2019-02-04T21:09:31.661Z. + note (Union[Unset, str]): Example: Sso settings update. + details (Union[Unset, str]): Example: Is SSO Enabled false\nSelected SSO Provider. + """ + + id: Union[Unset, int] = UNSET + username: Union[Unset, str] = UNSET + date: Union[Unset, str] = UNSET + note: Union[Unset, str] = UNSET + details: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + id = self.id + username = self.username + date = self.date + note = self.note + details = self.details + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if id is not UNSET: + field_dict["id"] = id + if username is not UNSET: + field_dict["username"] = username + if date is not UNSET: + field_dict["date"] = date + if note is not UNSET: + field_dict["note"] = note + if details is not UNSET: + field_dict["details"] = details + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + id = d.pop("id", UNSET) + + username = d.pop("username", UNSET) + + date = d.pop("date", UNSET) + + note = d.pop("note", UNSET) + + details = d.pop("details", UNSET) + + object_history = cls( + id=id, + username=username, + date=date, + note=note, + details=details, + ) + + object_history.additional_properties = d + return object_history + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index a05d19d36..385dad9d6 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1082,6 +1082,85 @@ } } } + }, + "/v1/buildings" : { + "get" : { + "tags" : [ "buildings" ], + "summary" : "Search for sorted and paged Buildings\n", + "description" : "Search for sorted and paged buildings", + "parameters" : [ { + "$ref" : "#/components/parameters/page" + }, { + "$ref" : "#/components/parameters/page-size" + }, { + "name" : "sort", + "in" : "query", + "description" : "Sorting criteria in the format: property:asc/desc. Default sort is id:asc. Multiple sort criteria are supported and must be separated with a comma. Example: sort=date:desc,name:asc ", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "default" : [ "id:asc" ] + } + }, { + "name" : "filter", + "in" : "query", + "description" : "Query in the RSQL format, allowing to filter buildings collection. Default filter is empty query - returning all results for the requested page. Fields allowed in the query: name, streetAddress1, streetAddress2, city, stateProvince, zipPostalCode, country. This param can be combined with paging and sorting. Example: filter=city==\"Chicago\" and name==\"*build*\"", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string", + "default" : "" + } + } ], + "responses" : { + "200" : { + "description" : "Successful response", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BuildingSearchResults" + } + } + } + } + }, + "x-required-privileges" : [ "Read Buildings" ] + } + }, + "/v1/jamf-connect/history" : { + "get" : { + "tags" : [ "jamf-connect" ], + "summary" : "Get Jamf Connect history\n", + "description" : "Get Jamf Connect history\n", + "parameters" : [ { + "$ref" : "#/components/parameters/page" + }, { + "$ref" : "#/components/parameters/page-size" + }, { + "$ref" : "#/components/parameters/sort" + }, { + "$ref" : "#/components/parameters/filter" + } ], + "responses" : { + "200" : { + "description" : "Details of Jamf Connect history were found", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HistorySearchResults" + } + } + } + } + }, + "x-required-privileges" : [ "Read Jamf Connect Settings" ] + } } }, "components": { @@ -1955,6 +2034,157 @@ "model.reference.with.Periods": { "type": "object", "description": "A Model with periods in its reference" + }, + "Building" : { + "required" : [ "name" ], + "type" : "object", + "properties" : { + "id" : { + "type" : "string", + "readOnly" : true, + "example" : "1" + }, + "name" : { + "type" : "string", + "example" : "Apple Park" + }, + "streetAddress1" : { + "type" : "string", + "nullable" : true, + "example" : "The McIntosh Tree" + }, + "streetAddress2" : { + "type" : "string", + "nullable" : true, + "example" : "One Apple Park Way" + }, + "city" : { + "type" : "string", + "nullable" : true, + "example" : "Cupertino" + }, + "stateProvince" : { + "type" : "string", + "nullable" : true, + "example" : "California" + }, + "zipPostalCode" : { + "type" : "string", + "nullable" : true, + "example" : "95014" + }, + "country" : { + "type" : "string", + "nullable" : true, + "example" : "The United States of America" + } + } + }, + "BuildingSearchResults" : { + "type" : "object", + "properties" : { + "totalCount" : { + "type" : "integer", + "example" : 3 + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Building" + } + } + } + }, + "ObjectHistory" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "integer", + "example" : 1 + }, + "username" : { + "type" : "string", + "example" : "admin" + }, + "date" : { + "type" : "string", + "example" : "2019-02-04T21:09:31.661Z" + }, + "note" : { + "type" : "string", + "example" : "Sso settings update" + }, + "details" : { + "type" : "string", + "example" : "Is SSO Enabled false\\nSelected SSO Provider" + } + } + }, + "HistorySearchResults" : { + "type" : "object", + "properties" : { + "totalCount" : { + "type" : "integer", + "example" : 1 + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ObjectHistory" + } + } + } + } + }, + "parameters" : { + "page" : { + "name" : "page", + "in" : "query", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "integer", + "default" : 0 + } + }, + "page-size" : { + "name" : "page-size", + "in" : "query", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "integer", + "default" : 100 + } + }, + "sort" : { + "name" : "sort", + "in" : "query", + "description" : "Sorting criteria in the format: property:asc/desc. Default sort order is descending. Multiple sort criteria are supported and must be entered on separate lines in Swagger UI. In the URI the 'sort' query param is not duplicated for each sort criterion, e.g., ...&sort=name:asc,date:desc. Fields that can be sorted: status, updated", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "default" : [ ] + } + }, + "filter" : { + "name" : "filter", + "in" : "query", + "description" : "Query in the RSQL format, allowing to filter results. Default filter is empty query - returning all results for the requested page. Fields allowed in the query: status, updated, version This param can be combined with paging and sorting. Example: filter=username!=admin and details==*disabled* and date<2019-12-15", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string", + "default" : "" + } } } } diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index b3ece0ded..f945e844b 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -320,7 +320,7 @@ def add_parameters( continue unique_param = (param.name, param.param_in) - if (param.name, param.param_in) in unique_parameters: + if unique_param in unique_parameters: return ( ParseError( data=data, @@ -535,6 +535,6 @@ def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", version=openapi.info.version, endpoint_collections_by_tag=endpoint_collections_by_tag, models=models, - errors=schemas.errors, + errors=schemas.errors + parameters.errors, enums=enums, ) diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 0e97c9246..a0606b8c1 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -163,7 +163,6 @@ def update_parameters_with_data( ref_path: The output of `parse_reference_path` (validated $ref). data: The schema of the thing to add to Schemas. parameters: `Parameters` up until now. - config: User-provided config for overriding default behavior. Returns: Either the updated `parameters` input or a `PropertyError` if something went wrong.