Skip to content

Commit fd5629f

Browse files
committed
Path parameters as function positional arguments
OpenAPI Specification indicates that if parameter location is `path` the `required` property is REQUIRED and its value MUST be `true` Ref: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-object With this in mind, this PR: - Tighten the parsing by throwing an error for path parameters with required=false - Update the templates to pass the path parameters to the function as positional arguments instead of kwargs to improve usability
1 parent 0f58cfd commit fd5629f

File tree

6 files changed

+133
-10
lines changed

6 files changed

+133
-10
lines changed

end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py

Lines changed: 5 additions & 5 deletions
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: Union[Unset, str] = UNSET,
1313
param_query: Union[Unset, str] = UNSET,
1414
) -> Dict[str, Any]:
1515
url = "{}/same-name-multiple-locations/{param}".format(client.base_url, param=param_path)
@@ -41,14 +41,14 @@ def _build_response(*, response: httpx.Response) -> Response[None]:
4141

4242

4343
def sync_detailed(
44+
param_path: str,
4445
*,
4546
client: Client,
46-
param_path: Union[Unset, str] = UNSET,
4747
param_query: Union[Unset, str] = UNSET,
4848
) -> Response[None]:
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: Union[Unset, str] = UNSET,
6666
param_query: Union[Unset, str] = UNSET,
6767
) -> Response[None]:
6868
kwargs = _get_kwargs(
69-
client=client,
7069
param_path=param_path,
70+
client=client,
7171
param_query=param_query,
7272
)
7373

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
param_1: str,
11+
param_2: int,
12+
*,
13+
client: Client,
14+
) -> Dict[str, Any]:
15+
url = "{}/multiple-path-parameters/{param1}/{param2}".format(client.base_url, param1=param_1, param2=param_2)
16+
17+
headers: Dict[str, Any] = client.get_headers()
18+
cookies: Dict[str, Any] = client.get_cookies()
19+
20+
return {
21+
"url": url,
22+
"headers": headers,
23+
"cookies": cookies,
24+
"timeout": client.get_timeout(),
25+
}
26+
27+
28+
def _build_response(*, response: httpx.Response) -> Response[None]:
29+
return Response(
30+
status_code=response.status_code,
31+
content=response.content,
32+
headers=response.headers,
33+
parsed=None,
34+
)
35+
36+
37+
def sync_detailed(
38+
param_1: str,
39+
param_2: int,
40+
*,
41+
client: Client,
42+
) -> Response[None]:
43+
kwargs = _get_kwargs(
44+
param_1=param_1,
45+
param_2=param_2,
46+
client=client,
47+
)
48+
49+
response = httpx.get(
50+
**kwargs,
51+
)
52+
53+
return _build_response(response=response)
54+
55+
56+
async def asyncio_detailed(
57+
param_1: str,
58+
param_2: int,
59+
*,
60+
client: Client,
61+
) -> Response[None]:
62+
kwargs = _get_kwargs(
63+
param_1=param_1,
64+
param_2=param_2,
65+
client=client,
66+
)
67+
68+
async with httpx.AsyncClient() as _client:
69+
response = await _client.get(**kwargs)
70+
71+
return _build_response(response=response)

end_to_end_tests/openapi.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@
782782
{
783783
"name": "param",
784784
"in": "path",
785+
"required": true,
785786
"schema": {
786787
"type": "string"
787788
}
@@ -793,6 +794,38 @@
793794
}
794795
}
795796
}
797+
},
798+
"/multiple-path-parameters/{param1}/{param2}": {
799+
"description": "Test with multiple path parameters",
800+
"get": {
801+
"tags": [
802+
"parameters"
803+
],
804+
"operationId": "multiple_path_parameters",
805+
"parameters": [
806+
{
807+
"name": "param1",
808+
"in": "path",
809+
"required": true,
810+
"schema": {
811+
"type": "string"
812+
}
813+
},
814+
{
815+
"name": "param2",
816+
"in": "path",
817+
"required": true,
818+
"schema": {
819+
"type": "integer"
820+
}
821+
}
822+
],
823+
"responses": {
824+
"200": {
825+
"description": "Success"
826+
}
827+
}
828+
}
796829
}
797830
},
798831
"components": {

openapi_python_client/parser/openapi.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ def _add_parameters(
245245
if param.param_in == oai.ParameterLocation.QUERY:
246246
endpoint.query_parameters.append(prop)
247247
elif param.param_in == oai.ParameterLocation.PATH:
248+
if not param.required:
249+
return ParseError(data=param, detail="Path parameter must be required"), schemas
248250
endpoint.path_parameters.append(prop)
249251
elif param.param_in == oai.ParameterLocation.HEADER:
250252
endpoint.header_parameters.append(prop)

openapi_python_client/templates/endpoint_macros.py.jinja

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,17 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
7373

7474
{# The all the kwargs passed into an endpoint (and variants thereof)) #}
7575
{% macro arguments(endpoint) %}
76+
{# path parameters #}
77+
{% for parameter in endpoint.path_parameters %}
78+
{{ parameter.to_string() }},
79+
{% endfor %}
7680
*,
7781
{# Proper client based on whether or not the endpoint requires authentication #}
7882
{% if endpoint.requires_security %}
7983
client: AuthenticatedClient,
8084
{% else %}
8185
client: Client,
8286
{% endif %}
83-
{# path parameters #}
84-
{% for parameter in endpoint.path_parameters %}
85-
{{ parameter.to_string() }},
86-
{% endfor %}
8787
{# Form data if any #}
8888
{% if endpoint.form_body_class %}
8989
form_data: {{ endpoint.form_body_class.name }},
@@ -111,10 +111,10 @@ json_body: {{ endpoint.json_body.get_type_string() }},
111111

112112
{# Just lists all kwargs to endpoints as name=name for passing to other functions #}
113113
{% macro kwargs(endpoint) %}
114-
client=client,
115114
{% for parameter in endpoint.path_parameters %}
116115
{{ parameter.python_name }}={{ parameter.python_name }},
117116
{% endfor %}
117+
client=client,
118118
{% if endpoint.form_body_class %}
119119
form_data=form_data,
120120
{% endif %}

tests/test_parser/test_openapi.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,23 @@ def test__add_parameters_parse_error(self, mocker):
447447
property_schemas,
448448
)
449449

450+
def test__add_parameters_parse_error_on_non_required_path_param(self, mocker):
451+
from openapi_python_client.parser.openapi import Endpoint, Schemas
452+
453+
endpoint = self.make_endpoint()
454+
parsed_schemas = mocker.MagicMock()
455+
mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=(mocker.MagicMock(), parsed_schemas))
456+
param = oai.Parameter.construct(
457+
name="test", required=False, param_schema=mocker.MagicMock(), param_in=oai.ParameterLocation.PATH
458+
)
459+
schemas = Schemas()
460+
config = MagicMock()
461+
462+
result = Endpoint._add_parameters(
463+
endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=schemas, config=config
464+
)
465+
assert result == (ParseError(data=param, detail="Path parameter must be required"), parsed_schemas)
466+
450467
def test__add_parameters_fail_loudly_when_location_not_supported(self, mocker):
451468
from openapi_python_client.parser.openapi import Endpoint, Schemas
452469

0 commit comments

Comments
 (0)