Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion end_to_end_tests/__snapshots__/test_end_to_end.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

Path parameter must be required

Parameter(name='optional', param_in=<ParameterLocation.PATH: 'path'>, description=None, required=False, deprecated=False, allowEmptyValue=False, style=None, explode=False, allowReserved=False, param_schema=Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=<DataType.STRING: 'string'>, allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, prefixItems=[], properties=None, additionalProperties=None, description=None, schema_format=None, default=None, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None), example=None, examples=None, content=None)
Parameter(name='optional', param_in=<ParameterLocation.PATH: 'path'>, description=None, required=False, deprecated=False, allowEmptyValue=False, style=<Style.SIMPLE: 'simple'>, explode=False, allowReserved=False, param_schema=Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=<DataType.STRING: 'string'>, allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, prefixItems=[], properties=None, additionalProperties=None, description=None, schema_format=None, default=None, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None), example=None, examples=None, content=None)

If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose

Expand Down
26 changes: 26 additions & 0 deletions end_to_end_tests/baseline_openapi_3.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,32 @@
"name": "an_enum_value_with_only_null",
"in": "query"
},
{
"required": true,
"schema": {
"title": "Non exploded array",
"type": "array",
"items": {
"type": "string"
}
},
"name": "non_exploded_array",
"in": "query",
"explode": false
},
{
"required": false,
"schema": {
"title": "Optional non exploded array",
"type": "array",
"items": {
"type": "string"
}
},
"name": "optional_non_exploded_array",
"in": "query",
"explode": false
},
{
"required": true,
"schema": {
Expand Down
27 changes: 27 additions & 0 deletions end_to_end_tests/baseline_openapi_3.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,33 @@ info:
"name": "an_enum_value_with_only_null",
"in": "query"
},
{
"required": true,
"schema": {
"title": "Non exploded array",
"type": "array",
"items": {
"type": "string"
}
},
"name": "non_exploded_array",
"in": "query",
"explode": false
},

{
"required": false,
"schema": {
"title": "Optional non exploded array",
"type": "array",
"items": {
"type": "string"
}
},
"name": "optional_non_exploded_array",
"in": "query",
"explode": false
},
{
"required": true,
"schema": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ def test_model_properties(self, MyModel):
schema:
type: boolean
description: Do you want fries with that?
- name: array
in: query
required: false
schema:
type: array
items:
type: string
responses:
"200":
description: Success!
Expand Down Expand Up @@ -160,4 +167,5 @@ def test_params(self, get_attribute_by_index_sync):
"id (str): Which one.",
"index (int):",
"fries (Union[Unset, bool]): Do you want fries with that?",
"array (Union[Unset, list[str]]):",
]
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
from ...models.an_enum import AnEnum
from ...models.an_enum_with_null import AnEnumWithNull
from ...models.http_validation_error import HTTPValidationError
from ...types import UNSET, Response
from ...types import UNSET, Response, Unset


def _get_kwargs(
*,
an_enum_value: list[AnEnum],
an_enum_value_with_null: list[Union[AnEnumWithNull, None]],
an_enum_value_with_only_null: list[None],
non_exploded_array: list[str],
optional_non_exploded_array: Union[Unset, list[str]] = UNSET,
some_date: Union[datetime.date, datetime.datetime],
) -> dict[str, Any]:
params: dict[str, Any] = {}
Expand All @@ -44,6 +46,17 @@ def _get_kwargs(

params["an_enum_value_with_only_null"] = json_an_enum_value_with_only_null

json_non_exploded_array = non_exploded_array

params["non_exploded_array"] = ",".join(str(item) for item in json_non_exploded_array)

json_optional_non_exploded_array: Union[Unset, list[str]] = UNSET
if not isinstance(optional_non_exploded_array, Unset):
json_optional_non_exploded_array = optional_non_exploded_array

if not isinstance(json_optional_non_exploded_array, Unset):
params["optional_non_exploded_array"] = ",".join(str(item) for item in json_optional_non_exploded_array)

json_some_date: str
if isinstance(some_date, datetime.date):
json_some_date = some_date.isoformat()
Expand Down Expand Up @@ -106,6 +119,8 @@ def sync_detailed(
an_enum_value: list[AnEnum],
an_enum_value_with_null: list[Union[AnEnumWithNull, None]],
an_enum_value_with_only_null: list[None],
non_exploded_array: list[str],
optional_non_exploded_array: Union[Unset, list[str]] = UNSET,
some_date: Union[datetime.date, datetime.datetime],
) -> Response[Union[HTTPValidationError, list["AModel"]]]:
"""Get List
Expand All @@ -116,6 +131,8 @@ def sync_detailed(
an_enum_value (list[AnEnum]):
an_enum_value_with_null (list[Union[AnEnumWithNull, None]]):
an_enum_value_with_only_null (list[None]):
non_exploded_array (list[str]):
optional_non_exploded_array (Union[Unset, list[str]]):
some_date (Union[datetime.date, datetime.datetime]):

Raises:
Expand All @@ -130,6 +147,8 @@ def sync_detailed(
an_enum_value=an_enum_value,
an_enum_value_with_null=an_enum_value_with_null,
an_enum_value_with_only_null=an_enum_value_with_only_null,
non_exploded_array=non_exploded_array,
optional_non_exploded_array=optional_non_exploded_array,
some_date=some_date,
)

Expand All @@ -146,6 +165,8 @@ def sync(
an_enum_value: list[AnEnum],
an_enum_value_with_null: list[Union[AnEnumWithNull, None]],
an_enum_value_with_only_null: list[None],
non_exploded_array: list[str],
optional_non_exploded_array: Union[Unset, list[str]] = UNSET,
some_date: Union[datetime.date, datetime.datetime],
) -> Optional[Union[HTTPValidationError, list["AModel"]]]:
"""Get List
Expand All @@ -156,6 +177,8 @@ def sync(
an_enum_value (list[AnEnum]):
an_enum_value_with_null (list[Union[AnEnumWithNull, None]]):
an_enum_value_with_only_null (list[None]):
non_exploded_array (list[str]):
optional_non_exploded_array (Union[Unset, list[str]]):
some_date (Union[datetime.date, datetime.datetime]):

Raises:
Expand All @@ -171,6 +194,8 @@ def sync(
an_enum_value=an_enum_value,
an_enum_value_with_null=an_enum_value_with_null,
an_enum_value_with_only_null=an_enum_value_with_only_null,
non_exploded_array=non_exploded_array,
optional_non_exploded_array=optional_non_exploded_array,
some_date=some_date,
).parsed

Expand All @@ -181,6 +206,8 @@ async def asyncio_detailed(
an_enum_value: list[AnEnum],
an_enum_value_with_null: list[Union[AnEnumWithNull, None]],
an_enum_value_with_only_null: list[None],
non_exploded_array: list[str],
optional_non_exploded_array: Union[Unset, list[str]] = UNSET,
some_date: Union[datetime.date, datetime.datetime],
) -> Response[Union[HTTPValidationError, list["AModel"]]]:
"""Get List
Expand All @@ -191,6 +218,8 @@ async def asyncio_detailed(
an_enum_value (list[AnEnum]):
an_enum_value_with_null (list[Union[AnEnumWithNull, None]]):
an_enum_value_with_only_null (list[None]):
non_exploded_array (list[str]):
optional_non_exploded_array (Union[Unset, list[str]]):
some_date (Union[datetime.date, datetime.datetime]):

Raises:
Expand All @@ -205,6 +234,8 @@ async def asyncio_detailed(
an_enum_value=an_enum_value,
an_enum_value_with_null=an_enum_value_with_null,
an_enum_value_with_only_null=an_enum_value_with_only_null,
non_exploded_array=non_exploded_array,
optional_non_exploded_array=optional_non_exploded_array,
some_date=some_date,
)

Expand All @@ -219,6 +250,8 @@ async def asyncio(
an_enum_value: list[AnEnum],
an_enum_value_with_null: list[Union[AnEnumWithNull, None]],
an_enum_value_with_only_null: list[None],
non_exploded_array: list[str],
optional_non_exploded_array: Union[Unset, list[str]] = UNSET,
some_date: Union[datetime.date, datetime.datetime],
) -> Optional[Union[HTTPValidationError, list["AModel"]]]:
"""Get List
Expand All @@ -229,6 +262,8 @@ async def asyncio(
an_enum_value (list[AnEnum]):
an_enum_value_with_null (list[Union[AnEnumWithNull, None]]):
an_enum_value_with_only_null (list[None]):
non_exploded_array (list[str]):
optional_non_exploded_array (Union[Unset, list[str]]):
some_date (Union[datetime.date, datetime.datetime]):

Raises:
Expand All @@ -245,6 +280,8 @@ async def asyncio(
an_enum_value=an_enum_value,
an_enum_value_with_null=an_enum_value_with_null,
an_enum_value_with_only_null=an_enum_value_with_only_null,
non_exploded_array=non_exploded_array,
optional_non_exploded_array=optional_non_exploded_array,
some_date=some_date,
)
).parsed
1 change: 1 addition & 0 deletions openapi_python_client/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ def add_parameters(
schemas=schemas,
parent_name=endpoint.name,
config=config,
explode=param.explode,
)

if isinstance(prop, ParseError):
Expand Down
2 changes: 2 additions & 0 deletions openapi_python_client/parser/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def property_from_data( # noqa: PLR0911, PLR0912
config: Config,
process_properties: bool = True,
roots: set[ReferencePath | utils.ClassName] | None = None,
explode: bool | None = None,
) -> tuple[Property | PropertyError, Schemas]:
"""Generate a Property from the OpenAPI dictionary representation of it"""
roots = roots or set()
Expand Down Expand Up @@ -285,6 +286,7 @@ def property_from_data( # noqa: PLR0911, PLR0912
config=config,
process_properties=process_properties,
roots=roots,
explode=explode,
)
if data.type == oai.DataType.OBJECT or data.allOf or (data.type is None and data.properties):
return ModelProperty.build(
Expand Down
4 changes: 4 additions & 0 deletions openapi_python_client/parser/properties/list_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class ListProperty(PropertyProtocol):
description: str | None
example: str | None
inner_property: PropertyProtocol
explode: bool | None = None
template: ClassVar[str] = "list_property.py.jinja"

@classmethod
Expand All @@ -36,6 +37,7 @@ def build(
config: Config,
process_properties: bool,
roots: set[ReferencePath | utils.ClassName],
explode: bool | None = None,
) -> tuple[ListProperty | PropertyError, Schemas]:
"""
Build a ListProperty the right way, use this instead of the normal constructor.
Expand All @@ -51,6 +53,7 @@ def build(
property data
roots: The set of `ReferencePath`s and `ClassName`s to remove from the schemas if a child reference becomes
invalid
explode: Whether to use `explode` for array properties.

Returns:
`(result, schemas)` where `schemas` is an updated version of the input named the same including any inner
Expand Down Expand Up @@ -98,6 +101,7 @@ def build(
python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
description=data.description,
example=data.example,
explode=explode,
),
schemas,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pydantic import ConfigDict, Field

from ..parameter_location import ParameterLocation
from ..style import Style
from .parameter import Parameter


Expand All @@ -20,6 +21,7 @@ class Header(Parameter):

name: str = Field(default="")
param_in: ParameterLocation = Field(default=ParameterLocation.HEADER, alias="in")
style: Style = Field(default=Style.SIMPLE)
model_config = ConfigDict(
# `Parameter` is not build yet, will rebuild in `__init__.py`:
defer_build=True,
Expand Down
43 changes: 40 additions & 3 deletions openapi_python_client/schema/openapi_schema_pydantic/parameter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Any, Optional

from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator

from ..parameter_location import ParameterLocation
from ..style import Style
from .example import Example
from .media_type import MediaType
from .reference import ReferenceOr
Expand All @@ -27,13 +28,49 @@ class Parameter(BaseModel):
required: bool = False
deprecated: bool = False
allowEmptyValue: bool = False
style: Optional[str] = None
explode: bool = False
style: Optional[Style] = None
explode: Optional[bool] = None
allowReserved: bool = False
param_schema: Optional[ReferenceOr[Schema]] = Field(default=None, alias="schema")
example: Optional[Any] = None
examples: Optional[dict[str, ReferenceOr[Example]]] = None
content: Optional[dict[str, MediaType]] = None

@model_validator(mode="after")
@classmethod
def validate_dependencies(cls, model: "Parameter") -> "Parameter":
param_in = model.param_in
explode = model.explode

if model.style is None:
if param_in in [ParameterLocation.PATH, ParameterLocation.HEADER]:
model.style = Style.SIMPLE
elif param_in in [ParameterLocation.QUERY, ParameterLocation.COOKIE]:
model.style = Style.FORM

# Validate style based on parameter location, not all combinations are valid.
# https://swagger.io/docs/specification/v3_0/serialization/
if param_in == ParameterLocation.PATH:
if model.style not in (Style.SIMPLE, Style.LABEL, Style.MATRIX):
raise ValueError(f"Invalid style '{model.style}' for path parameter")
elif param_in == ParameterLocation.QUERY:
if model.style not in (Style.FORM, Style.SPACE_DELIMITED, Style.PIPE_DELIMITED, Style.DEEP_OBJECT):
raise ValueError(f"Invalid style '{model.style}' for query parameter")
elif param_in == ParameterLocation.HEADER:
if model.style != Style.SIMPLE:
raise ValueError(f"Invalid style '{model.style}' for header parameter")
elif param_in == ParameterLocation.COOKIE:
if model.style != Style.FORM:
raise ValueError(f"Invalid style '{model.style}' for cookie parameter")

if explode is None:
if model.style == Style.FORM:
model.explode = True
else:
model.explode = False

return model

model_config = ConfigDict(
# `MediaType` is not build yet, will rebuild in `__init__.py`:
defer_build=True,
Expand Down
18 changes: 18 additions & 0 deletions openapi_python_client/schema/style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from enum import Enum


class Style(str, Enum):
"""The style of a schema is defined by the style keyword

References:
- https://swagger.io/docs/specification/v3_0/serialization/
- https://spec.openapis.org/oas/latest.html#fixed-fields-for-use-with-schema
"""

SIMPLE = "simple"
LABEL = "label"
MATRIX = "matrix"
FORM = "form"
SPACE_DELIMITED = "spaceDelimited"
PIPE_DELIMITED = "pipeDelimited"
DEEP_OBJECT = "deepObject"
Loading