Skip to content

Commit 9d991ec

Browse files
dbantytsotnikov
andauthored
feat: Allow path parameters to be positional args [#429 & #464]. Thanks @tsotnikov!
Co-authored-by: Taras Sotnikov <[email protected]>
1 parent c5293bf commit 9d991ec

File tree

9 files changed

+289
-61
lines changed

9 files changed

+289
-61
lines changed

Diff for: end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
delete_common_parameters_overriding_param,
77
get_common_parameters_overriding_param,
88
get_same_name_multiple_locations_param,
9+
multiple_path_parameters,
910
)
1011

1112

@@ -21,3 +22,7 @@ def delete_common_parameters_overriding_param(cls) -> types.ModuleType:
2122
@classmethod
2223
def get_same_name_multiple_locations_param(cls) -> types.ModuleType:
2324
return get_same_name_multiple_locations_param
25+
26+
@classmethod
27+
def multiple_path_parameters(cls) -> types.ModuleType:
28+
return multiple_path_parameters

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88

99
def _get_kwargs(
10+
param_path: str,
1011
*,
1112
client: Client,
12-
param_path: str,
1313
param_query: Union[Unset, None, str] = UNSET,
1414
) -> Dict[str, Any]:
1515
url = "{}/common_parameters_overriding/{param}".format(client.base_url, param=param_path)
@@ -41,14 +41,14 @@ def _build_response(*, response: httpx.Response) -> Response[Any]:
4141

4242

4343
def sync_detailed(
44+
param_path: str,
4445
*,
4546
client: Client,
46-
param_path: str,
4747
param_query: Union[Unset, None, str] = UNSET,
4848
) -> Response[Any]:
4949
kwargs = _get_kwargs(
50-
client=client,
5150
param_path=param_path,
51+
client=client,
5252
param_query=param_query,
5353
)
5454

@@ -60,14 +60,14 @@ def sync_detailed(
6060

6161

6262
async def asyncio_detailed(
63+
param_path: str,
6364
*,
6465
client: Client,
65-
param_path: str,
6666
param_query: Union[Unset, None, str] = UNSET,
6767
) -> Response[Any]:
6868
kwargs = _get_kwargs(
69-
client=client,
7069
param_path=param_path,
70+
client=client,
7171
param_query=param_query,
7272
)
7373

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88

99
def _get_kwargs(
10+
param_path: str,
1011
*,
1112
client: Client,
12-
param_path: str,
1313
param_query: str = "overriden_in_GET",
1414
) -> Dict[str, Any]:
1515
url = "{}/common_parameters_overriding/{param}".format(client.base_url, param=param_path)
@@ -41,14 +41,14 @@ def _build_response(*, response: httpx.Response) -> Response[Any]:
4141

4242

4343
def sync_detailed(
44+
param_path: str,
4445
*,
4546
client: Client,
46-
param_path: str,
4747
param_query: str = "overriden_in_GET",
4848
) -> Response[Any]:
4949
kwargs = _get_kwargs(
50-
client=client,
5150
param_path=param_path,
51+
client=client,
5252
param_query=param_query,
5353
)
5454

@@ -60,14 +60,14 @@ def sync_detailed(
6060

6161

6262
async def asyncio_detailed(
63+
param_path: str,
6364
*,
6465
client: Client,
65-
param_path: str,
6666
param_query: str = "overriden_in_GET",
6767
) -> Response[Any]:
6868
kwargs = _get_kwargs(
69-
client=client,
7069
param_path=param_path,
70+
client=client,
7171
param_query=param_query,
7272
)
7373

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88

99
def _get_kwargs(
10+
param_path: str,
1011
*,
1112
client: Client,
12-
param_path: str,
1313
param_query: Union[Unset, None, str] = UNSET,
1414
param_header: Union[Unset, str] = UNSET,
1515
param_cookie: Union[Unset, str] = UNSET,
@@ -49,16 +49,16 @@ def _build_response(*, response: httpx.Response) -> Response[Any]:
4949

5050

5151
def sync_detailed(
52+
param_path: str,
5253
*,
5354
client: Client,
54-
param_path: str,
5555
param_query: Union[Unset, None, str] = UNSET,
5656
param_header: Union[Unset, str] = UNSET,
5757
param_cookie: Union[Unset, str] = UNSET,
5858
) -> Response[Any]:
5959
kwargs = _get_kwargs(
60-
client=client,
6160
param_path=param_path,
61+
client=client,
6262
param_query=param_query,
6363
param_header=param_header,
6464
param_cookie=param_cookie,
@@ -72,16 +72,16 @@ def sync_detailed(
7272

7373

7474
async def asyncio_detailed(
75+
param_path: str,
7576
*,
7677
client: Client,
77-
param_path: str,
7878
param_query: Union[Unset, None, str] = UNSET,
7979
param_header: Union[Unset, str] = UNSET,
8080
param_cookie: Union[Unset, str] = UNSET,
8181
) -> Response[Any]:
8282
kwargs = _get_kwargs(
83-
client=client,
8483
param_path=param_path,
84+
client=client,
8585
param_query=param_query,
8686
param_header=param_header,
8787
param_cookie=param_cookie,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from typing import Any, Dict
2+
3+
import httpx
4+
5+
from ...client import Client
6+
from ...types import Response
7+
8+
9+
def _get_kwargs(
10+
param4: str,
11+
param2: int,
12+
param1: str,
13+
param3: int,
14+
*,
15+
client: Client,
16+
) -> Dict[str, Any]:
17+
url = "{}/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}".format(
18+
client.base_url, param4=param4, param2=param2, param1=param1, param3=param3
19+
)
20+
21+
headers: Dict[str, Any] = client.get_headers()
22+
cookies: Dict[str, Any] = client.get_cookies()
23+
24+
return {
25+
"url": url,
26+
"headers": headers,
27+
"cookies": cookies,
28+
"timeout": client.get_timeout(),
29+
}
30+
31+
32+
def _build_response(*, response: httpx.Response) -> Response[Any]:
33+
return Response(
34+
status_code=response.status_code,
35+
content=response.content,
36+
headers=response.headers,
37+
parsed=None,
38+
)
39+
40+
41+
def sync_detailed(
42+
param4: str,
43+
param2: int,
44+
param1: str,
45+
param3: int,
46+
*,
47+
client: Client,
48+
) -> Response[Any]:
49+
kwargs = _get_kwargs(
50+
param4=param4,
51+
param2=param2,
52+
param1=param1,
53+
param3=param3,
54+
client=client,
55+
)
56+
57+
response = httpx.get(
58+
**kwargs,
59+
)
60+
61+
return _build_response(response=response)
62+
63+
64+
async def asyncio_detailed(
65+
param4: str,
66+
param2: int,
67+
param1: str,
68+
param3: int,
69+
*,
70+
client: Client,
71+
) -> Response[Any]:
72+
kwargs = _get_kwargs(
73+
param4=param4,
74+
param2=param2,
75+
param1=param1,
76+
param3=param3,
77+
client=client,
78+
)
79+
80+
async with httpx.AsyncClient() as _client:
81+
response = await _client.get(**kwargs)
82+
83+
return _build_response(response=response)

Diff for: end_to_end_tests/openapi.json

+50
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,56 @@
833833
}
834834
}
835835
},
836+
"/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}": {
837+
"description": "Test that multiple path parameters are ordered by appearance in path",
838+
"get": {
839+
"tags": [
840+
"parameters"
841+
],
842+
"operationId": "multiple_path_parameters",
843+
"parameters": [
844+
{
845+
"name": "param1",
846+
"in": "path",
847+
"required": true,
848+
"schema": {
849+
"type": "string"
850+
}
851+
},
852+
{
853+
"name": "param2",
854+
"in": "path",
855+
"required": true,
856+
"schema": {
857+
"type": "integer"
858+
}
859+
}
860+
],
861+
"responses": {
862+
"200": {
863+
"description": "Success"
864+
}
865+
}
866+
},
867+
"parameters": [
868+
{
869+
"name": "param4",
870+
"in": "path",
871+
"required": true,
872+
"schema": {
873+
"type": "string"
874+
}
875+
},
876+
{
877+
"name": "param3",
878+
"in": "path",
879+
"required": true,
880+
"schema": {
881+
"type": "integer"
882+
}
883+
}
884+
]
885+
},
836886
"/location/query/optionality": {
837887
"description": "Test what happens with various combinations of required and nullable in query parameters.",
838888
"get": {

Diff for: openapi_python_client/parser/openapi.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
from collections import OrderedDict
13
from copy import deepcopy
24
from dataclasses import dataclass, field
35
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
@@ -13,6 +15,8 @@
1315
from .properties import Class, EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data
1416
from .responses import Response, response_from_data
1517

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

1721
def import_string_from_class(class_: Class, prefix: str = "") -> str:
1822
"""Create a string which is used to import a reference"""
@@ -48,10 +52,11 @@ def from_data(
4852
)
4953
# Add `PathItem` parameters
5054
if not isinstance(endpoint, ParseError):
51-
endpoint, schemas = Endpoint._add_parameters(
55+
endpoint, schemas = Endpoint.add_parameters(
5256
endpoint=endpoint, data=path_data, schemas=schemas, config=config
5357
)
54-
58+
if not isinstance(endpoint, ParseError):
59+
endpoint = Endpoint.sort_parameters(endpoint=endpoint)
5560
if isinstance(endpoint, ParseError):
5661
endpoint.header = (
5762
f"ERROR parsing {method.upper()} {path} within {tag}. Endpoint will not be generated."
@@ -91,7 +96,7 @@ class Endpoint:
9196
summary: Optional[str] = ""
9297
relative_imports: Set[str] = field(default_factory=set)
9398
query_parameters: Dict[str, Property] = field(default_factory=dict)
94-
path_parameters: Dict[str, Property] = field(default_factory=dict)
99+
path_parameters: "OrderedDict[str, Property]" = field(default_factory=OrderedDict)
95100
header_parameters: Dict[str, Property] = field(default_factory=dict)
96101
cookie_parameters: Dict[str, Property] = field(default_factory=dict)
97102
responses: List[Response] = field(default_factory=list)
@@ -240,7 +245,7 @@ def _add_responses(
240245
return endpoint, schemas
241246

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

267+
if param.param_in == oai.ParameterLocation.PATH and not param.required:
268+
return ParseError(data=param, detail="Path parameter must be required"), schemas
269+
262270
unique_param = (param.name, param.param_in)
263271
if unique_param in unique_parameters:
264272
duplication_detail = (
@@ -282,6 +290,7 @@ def _add_parameters(
282290
if prop.name in parameters_by_location[param.param_in]:
283291
# This parameter was defined in the Operation, so ignore the PathItem definition
284292
continue
293+
285294
for location, parameters_dict in parameters_by_location.items():
286295
if location == param.param_in or prop.name not in parameters_dict:
287296
continue
@@ -318,6 +327,24 @@ def _add_parameters(
318327

319328
return endpoint, schemas
320329

330+
@staticmethod
331+
def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
332+
endpoint = deepcopy(endpoint)
333+
parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path)
334+
try:
335+
sorted_params = sorted(
336+
endpoint.path_parameters.values(), key=lambda param: parameters_from_path.index(param.name)
337+
)
338+
endpoint.path_parameters = OrderedDict((param.name, param) for param in sorted_params)
339+
except ValueError:
340+
pass # We're going to catch the difference down below
341+
path_parameter_names = [name for name in endpoint.path_parameters]
342+
if parameters_from_path != path_parameter_names:
343+
return ParseError(
344+
detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)",
345+
)
346+
return endpoint
347+
321348
@staticmethod
322349
def from_data(
323350
*, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas, config: Config
@@ -339,7 +366,7 @@ def from_data(
339366
tag=tag,
340367
)
341368

342-
result, schemas = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config)
369+
result, schemas = Endpoint.add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config)
343370
if isinstance(result, ParseError):
344371
return result, schemas
345372
result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas, config=config)

0 commit comments

Comments
 (0)