fix: correctly serialize enum path/query params · Minibrams/openapi-python@9c24209 · GitHub
Skip to content

Commit 9c24209

Browse files
author
Anders Brams
committed
fix: correctly serialize enum path/query params
1 parent 7f765af commit 9c24209

15 files changed

Lines changed: 327 additions & 30 deletions

File tree

‎openapi_python/generator/normalize.py‎

Lines changed: 54 additions & 12 deletions

‎openapi_python/generator/render.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ def _route_aliases(
471471
def _render_transport(spec: NormalizedSpec, *, protocol_only: bool) -> str:
472472
return _render_template(
473473
"transport.py.j2",
474-
typing_imports="Protocol" if protocol_only else "TYPE_CHECKING, Protocol",
474+
typing_imports=("Protocol" if protocol_only else "TYPE_CHECKING, Protocol"),
475475
include_default_transport=not protocol_only,
476476
)
477477

‎openapi_python/generator/runtime_default.py‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
from __future__ import annotations
22

33
from collections.abc import Mapping
4+
from enum import Enum
45
from typing import TYPE_CHECKING
56

67
if TYPE_CHECKING:
78
import httpx
89

910

11+
def _serialize_value(value: object) -> object:
12+
if isinstance(value, Enum):
13+
return value.value
14+
return value
15+
16+
17+
def _serialize_mapping(values: Mapping[str, object] | None) -> dict[str, object]:
18+
return {key: _serialize_value(value) for key, value in (values or {}).items()}
19+
20+
1021
class RuntimeDefaultTransport:
1122
def __init__(self, *, client: httpx.Client | None = None) -> None:
1223
if client is None:
@@ -26,11 +37,16 @@ def request(
2637
headers: Mapping[str, object] | None,
2738
body: object | None,
2839
) -> object:
29-
query_dict = {key: str(value) for key, value in (query or {}).items()}
30-
header_dict = {key: str(value) for key, value in (headers or {}).items()}
40+
path_params = _serialize_mapping(params)
41+
query_dict = {
42+
key: str(value) for key, value in _serialize_mapping(query).items()
43+
}
44+
header_dict = {
45+
key: str(value) for key, value in _serialize_mapping(headers).items()
46+
}
3147
response = self._client.request(
3248
method=method.upper(),
33-
url=f"{base_url.rstrip('/')}{route.format(**(params or {}))}",
49+
url=f"{base_url.rstrip('/')}{route.format(**path_params)}",
3450
params=query_dict or None,
3551
headers=header_dict or None,
3652
json=body,

‎openapi_python/generator/templates/default_transport.py.j2‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
def _serialize_value(value: object) -> object:
2+
if isinstance(value, Enum):
3+
return value.value
4+
return value
5+
6+
7+
def _serialize_mapping(values: Mapping[str, object] | None) -> dict[str, object]:
8+
return {key: _serialize_value(value) for key, value in (values or {}).items()}
9+
10+
111
class DefaultTransport:
212
def __init__(self, *, client: httpx.Client | None = None) -> None:
313
if client is None:
@@ -17,9 +27,10 @@ class DefaultTransport:
1727
headers: Mapping[str, object] | None,
1828
body: object | None,
1929
) -> object:
20-
formatted_route = route.format(**(params or {}))
21-
query_dict = {key: str(value) for key, value in (query or {}).items()}
22-
header_dict = {key: str(value) for key, value in (headers or {}).items()}
30+
path_params = _serialize_mapping(params)
31+
formatted_route = route.format(**path_params)
32+
query_dict = {key: str(value) for key, value in _serialize_mapping(query).items()}
33+
header_dict = {key: str(value) for key, value in _serialize_mapping(headers).items()}
2334
request_kwargs: dict[str, object] = {"json": body}
2435
if isinstance(body, Mapping) and any(
2536
isinstance(value, (bytes, bytearray)) for value in body.values()
@@ -70,9 +81,10 @@ class DefaultAsyncTransport:
7081
headers: Mapping[str, object] | None,
7182
body: object | None,
7283
) -> object:
73-
formatted_route = route.format(**(params or {}))
74-
query_dict = {key: str(value) for key, value in (query or {}).items()}
75-
header_dict = {key: str(value) for key, value in (headers or {}).items()}
84+
path_params = _serialize_mapping(params)
85+
formatted_route = route.format(**path_params)
86+
query_dict = {key: str(value) for key, value in _serialize_mapping(query).items()}
87+
header_dict = {key: str(value) for key, value in _serialize_mapping(headers).items()}
7688
request_kwargs: dict[str, object] = {"json": body}
7789
if isinstance(body, Mapping) and any(
7890
isinstance(value, (bytes, bytearray)) for value in body.values()

‎openapi_python/generator/templates/transport.py.j2‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Mapping
4+
from enum import Enum
45
from typing import {{ typing_imports }}
56

67
from .types import RouteLiteral as RouteLiteral
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum
4+
5+
from fastapi import FastAPI
6+
from pydantic import BaseModel
7+
8+
app = FastAPI()
9+
10+
11+
class PipelineName(str, Enum):
12+
create_sdwan = "create-sdwan"
13+
test_pipeline = "test-pipeline"
14+
deploy_sdwan_spoke = "deploy-sdwan-spoke"
15+
deploy_sdwan_customer_hub = "deploy-sdwan-customer-hub"
16+
deploy_cisco_access_switch = "deploy-cisco-access-switch"
17+
18+
19+
class PipelineExecution(BaseModel):
20+
name: PipelineName
21+
id: int
22+
23+
24+
@app.get("/api/v1/pipelines/{name}/executions/{id}", response_model=PipelineExecution)
25+
def get_pipeline_execution(name: PipelineName, id: int) -> PipelineExecution:
26+
return PipelineExecution(name=name, id=id)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
import json
5+
from pathlib import Path
6+
from typing import Any, cast
7+
8+
from app import app
9+
10+
from openapi_python.generator import GenerationRequest, generate_client
11+
12+
13+
class _Response:
14+
content = b"{}"
15+
16+
def raise_for_status(self) -> None:
17+
pass
18+
19+
def json(self) -> dict[str, object]:
20+
return {}
21+
22+
23+
class _HttpClient:
24+
def __init__(self) -> None:
25+
self.url = ""
26+
27+
def request(self, **kwargs: Any) -> _Response:
28+
self.url = str(kwargs["url"])
29+
return _Response()
30+
31+
32+
def main() -> None:
33+
generate_client(
34+
GenerationRequest(
35+
output_dir=Path(__file__).parent / "generated",
36+
spec_json=json.dumps(app.openapi()),
37+
overwrite=True,
38+
)
39+
)
40+
types = importlib.import_module("generated.my_client.types")
41+
transport_module = importlib.import_module("generated.my_client.transport")
42+
http_client = _HttpClient()
43+
transport = transport_module.DefaultTransport(client=cast(Any, http_client))
44+
45+
transport.request(
46+
method="get",
47+
route="/api/v1/pipelines/{name}/executions/{id}",
48+
base_url="http://testserver",
49+
params={
50+
"name": types.PipelineName.DEPLOY_CISCO_ACCESS_SWITCH,
51+
"id": 1,
52+
},
53+
query=None,
54+
headers=None,
55+
body=None,
56+
)
57+
assert (
58+
http_client.url
59+
== "http://testserver/api/v1/pipelines/deploy-cisco-access-switch/executions/1"
60+
)
61+
62+
63+
if __name__ == "__main__":
64+
main()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
from typing import assert_type
4+
5+
from generated.my_client import AsyncClient
6+
from generated.my_client.types import PipelineExecution, PipelineName
7+
8+
client = AsyncClient(base_url="http://testserver")
9+
10+
11+
async def main() -> None:
12+
result = await client.get("/api/v1/pipelines/{name}/executions/{id}")(
13+
params={
14+
"name": PipelineName.DEPLOY_CISCO_ACCESS_SWITCH.value,
15+
"id": 1,
16+
}
17+
)
18+
assert_type(result, PipelineExecution)
19+
assert_type(result["name"], PipelineName)
20+
assert_type(result["id"], int)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import annotations
2+
3+
from typing import assert_type
4+
5+
from generated.my_client import Client
6+
from generated.my_client.types import PipelineExecution, PipelineName
7+
8+
client = Client(base_url="http://testserver")
9+
10+
result = client.get("/api/v1/pipelines/{name}/executions/{id}")(
11+
params={
12+
"name": PipelineName.DEPLOY_CISCO_ACCESS_SWITCH.value,
13+
"id": 1,
14+
}
15+
)
16+
assert_type(result, PipelineExecution)
17+
assert_type(result["name"], PipelineName)
18+
assert_type(result["id"], int)
Lines changed: 23 additions & 0 deletions

0 commit comments

Comments
 (0)