Skip to content

feat: Allow path parameters to be positional args [#429]. Thanks @tsotnikov! #464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 15, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
delete_common_parameters_overriding_param,
get_common_parameters_overriding_param,
get_same_name_multiple_locations_param,
multiple_path_parameters,
)


Expand All @@ -21,3 +22,7 @@ def delete_common_parameters_overriding_param(cls) -> types.ModuleType:
@classmethod
def get_same_name_multiple_locations_param(cls) -> types.ModuleType:
return get_same_name_multiple_locations_param

@classmethod
def multiple_path_parameters(cls) -> types.ModuleType:
return multiple_path_parameters
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@


def _get_kwargs(
param_path: str,
*,
client: Client,
param_path: str,
param_query: Union[Unset, None, str] = UNSET,
) -> Dict[str, Any]:
url = "{}/common_parameters_overriding/{param}".format(client.base_url, param=param_path)
Expand Down Expand Up @@ -41,14 +41,14 @@ def _build_response(*, response: httpx.Response) -> Response[Any]:


def sync_detailed(
param_path: str,
*,
client: Client,
param_path: str,
param_query: Union[Unset, None, str] = UNSET,
) -> Response[Any]:
kwargs = _get_kwargs(
client=client,
param_path=param_path,
client=client,
param_query=param_query,
)

Expand All @@ -60,14 +60,14 @@ def sync_detailed(


async def asyncio_detailed(
param_path: str,
*,
client: Client,
param_path: str,
param_query: Union[Unset, None, str] = UNSET,
) -> Response[Any]:
kwargs = _get_kwargs(
client=client,
param_path=param_path,
client=client,
param_query=param_query,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@


def _get_kwargs(
param_path: str,
*,
client: Client,
param_path: str,
param_query: str = "overriden_in_GET",
) -> Dict[str, Any]:
url = "{}/common_parameters_overriding/{param}".format(client.base_url, param=param_path)
Expand Down Expand Up @@ -41,14 +41,14 @@ def _build_response(*, response: httpx.Response) -> Response[Any]:


def sync_detailed(
param_path: str,
*,
client: Client,
param_path: str,
param_query: str = "overriden_in_GET",
) -> Response[Any]:
kwargs = _get_kwargs(
client=client,
param_path=param_path,
client=client,
param_query=param_query,
)

Expand All @@ -60,14 +60,14 @@ def sync_detailed(


async def asyncio_detailed(
param_path: str,
*,
client: Client,
param_path: str,
param_query: str = "overriden_in_GET",
) -> Response[Any]:
kwargs = _get_kwargs(
client=client,
param_path=param_path,
client=client,
param_query=param_query,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@


def _get_kwargs(
param_path: str,
*,
client: Client,
param_path: str,
param_query: Union[Unset, None, str] = UNSET,
param_header: Union[Unset, str] = UNSET,
param_cookie: Union[Unset, str] = UNSET,
Expand Down Expand Up @@ -49,16 +49,16 @@ def _build_response(*, response: httpx.Response) -> Response[Any]:


def sync_detailed(
param_path: str,
*,
client: Client,
param_path: str,
param_query: Union[Unset, None, str] = UNSET,
param_header: Union[Unset, str] = UNSET,
param_cookie: Union[Unset, str] = UNSET,
) -> Response[Any]:
kwargs = _get_kwargs(
client=client,
param_path=param_path,
client=client,
param_query=param_query,
param_header=param_header,
param_cookie=param_cookie,
Expand All @@ -72,16 +72,16 @@ def sync_detailed(


async def asyncio_detailed(
param_path: str,
*,
client: Client,
param_path: str,
param_query: Union[Unset, None, str] = UNSET,
param_header: Union[Unset, str] = UNSET,
param_cookie: Union[Unset, str] = UNSET,
) -> Response[Any]:
kwargs = _get_kwargs(
client=client,
param_path=param_path,
client=client,
param_query=param_query,
param_header=param_header,
param_cookie=param_cookie,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from typing import Any, Dict

import httpx

from ...client import Client
from ...types import Response


def _get_kwargs(
param4: str,
param2: int,
param1: str,
param3: int,
*,
client: Client,
) -> Dict[str, Any]:
url = "{}/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}".format(
client.base_url, param4=param4, param2=param2, param1=param1, param3=param3
)

headers: Dict[str, Any] = client.get_headers()
cookies: Dict[str, Any] = client.get_cookies()

return {
"url": url,
"headers": headers,
"cookies": cookies,
"timeout": client.get_timeout(),
}


def _build_response(*, response: httpx.Response) -> Response[Any]:
return Response(
status_code=response.status_code,
content=response.content,
headers=response.headers,
parsed=None,
)


def sync_detailed(
param4: str,
param2: int,
param1: str,
param3: int,
*,
client: Client,
) -> Response[Any]:
kwargs = _get_kwargs(
param4=param4,
param2=param2,
param1=param1,
param3=param3,
client=client,
)

response = httpx.get(
**kwargs,
)

return _build_response(response=response)


async def asyncio_detailed(
param4: str,
param2: int,
param1: str,
param3: int,
*,
client: Client,
) -> Response[Any]:
kwargs = _get_kwargs(
param4=param4,
param2=param2,
param1=param1,
param3=param3,
client=client,
)

async with httpx.AsyncClient() as _client:
response = await _client.get(**kwargs)

return _build_response(response=response)
50 changes: 50 additions & 0 deletions end_to_end_tests/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,56 @@
}
}
},
"/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}": {
"description": "Test that multiple path parameters are ordered by appearance in path",
"get": {
"tags": [
"parameters"
],
"operationId": "multiple_path_parameters",
"parameters": [
{
"name": "param1",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "param2",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
},
"parameters": [
{
"name": "param4",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "param3",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
]
},
"/location/query/optionality": {
"description": "Test what happens with various combinations of required and nullable in query parameters.",
"get": {
Expand Down
37 changes: 32 additions & 5 deletions openapi_python_client/parser/openapi.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re
from collections import OrderedDict
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
Expand All @@ -13,6 +15,8 @@
from .properties import Class, EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data
from .responses import Response, response_from_data

_PATH_PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)}")


def import_string_from_class(class_: Class, prefix: str = "") -> str:
"""Create a string which is used to import a reference"""
Expand Down Expand Up @@ -48,10 +52,11 @@ def from_data(
)
# Add `PathItem` parameters
if not isinstance(endpoint, ParseError):
endpoint, schemas = Endpoint._add_parameters(
endpoint, schemas = Endpoint.add_parameters(
endpoint=endpoint, data=path_data, schemas=schemas, config=config
)

if not isinstance(endpoint, ParseError):
endpoint = Endpoint.sort_parameters(endpoint=endpoint)
if isinstance(endpoint, ParseError):
endpoint.header = (
f"ERROR parsing {method.upper()} {path} within {tag}. Endpoint will not be generated."
Expand Down Expand Up @@ -91,7 +96,7 @@ class Endpoint:
summary: Optional[str] = ""
relative_imports: Set[str] = field(default_factory=set)
query_parameters: Dict[str, Property] = field(default_factory=dict)
path_parameters: Dict[str, Property] = field(default_factory=dict)
path_parameters: "OrderedDict[str, Property]" = field(default_factory=OrderedDict)
header_parameters: Dict[str, Property] = field(default_factory=dict)
cookie_parameters: Dict[str, Property] = field(default_factory=dict)
responses: List[Response] = field(default_factory=list)
Expand Down Expand Up @@ -240,7 +245,7 @@ def _add_responses(
return endpoint, schemas

@staticmethod
def _add_parameters(
def add_parameters(
*, endpoint: "Endpoint", data: Union[oai.Operation, oai.PathItem], schemas: Schemas, config: Config
) -> Tuple[Union["Endpoint", ParseError], Schemas]:
endpoint = deepcopy(endpoint)
Expand All @@ -259,6 +264,9 @@ def _add_parameters(
if isinstance(param, oai.Reference) or param.param_schema is None:
continue

if param.param_in == oai.ParameterLocation.PATH and not param.required:
return ParseError(data=param, detail="Path parameter must be required"), schemas

unique_param = (param.name, param.param_in)
if unique_param in unique_parameters:
duplication_detail = (
Expand All @@ -282,6 +290,7 @@ def _add_parameters(
if prop.name in parameters_by_location[param.param_in]:
# This parameter was defined in the Operation, so ignore the PathItem definition
continue

for location, parameters_dict in parameters_by_location.items():
if location == param.param_in or prop.name not in parameters_dict:
continue
Expand Down Expand Up @@ -318,6 +327,24 @@ def _add_parameters(

return endpoint, schemas

@staticmethod
def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
endpoint = deepcopy(endpoint)
parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path)
try:
sorted_params = sorted(
endpoint.path_parameters.values(), key=lambda param: parameters_from_path.index(param.name)
)
endpoint.path_parameters = OrderedDict((param.name, param) for param in sorted_params)
except ValueError:
pass # We're going to catch the difference down below
path_parameter_names = [name for name in endpoint.path_parameters]
if parameters_from_path != path_parameter_names:
return ParseError(
detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)",
)
return endpoint

@staticmethod
def from_data(
*, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas, config: Config
Expand All @@ -339,7 +366,7 @@ def from_data(
tag=tag,
)

result, schemas = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config)
result, schemas = Endpoint.add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config)
if isinstance(result, ParseError):
return result, schemas
result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas, config=config)
Expand Down
Loading