Skip to content

Commit 7d061a4

Browse files
committed
Add support for no response or unsupported content types (closes #141)
1 parent 17b64e5 commit 7d061a4

15 files changed

+289
-67
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- It's now possible to include custom headers and cookies in requests, as well as set a custom timeout. This can be done
3131
either by directly setting those parameters on a `Client` (e.g. `my_client.headers = {"Header": "Value"}`) or using
3232
a fluid api (e.g. `my_endpoint.sync(my_client.with_cookies({"MyCookie": "cookie"}).with_timeout(10.0))`).
33+
- Unsupported content types or no responses at all will no longer result in an endpoint being completely skipped. Instead,
34+
only the `detailed` versions of the endpoint will be generated, where the resulting `Response.parsed` is always `None`.
35+
(#141)
36+
37+
### Changes
38+
39+
- The format of any errors/warnings has been spaced out a bit.
3340

3441
## 0.5.4 - 2020-08-29
3542

end_to_end_tests/fastapi_app/__init__.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ def json_body(body: AModel):
9898
return
9999

100100

101-
@test_router.post("/test_defaults")
102-
def test_defaults(
101+
@test_router.post("/defaults")
102+
def defaults(
103103
string_prop: str = Query(default="the default string"),
104104
datetime_prop: datetime = Query(default=datetime(1010, 10, 10)),
105105
date_prop: date = Query(default=date(1010, 10, 10)),
@@ -115,14 +115,27 @@ def test_defaults(
115115

116116

117117
@test_router.get(
118-
"/test_octet_stream",
118+
"/octet_stream",
119119
response_class=FileResponse,
120120
responses={200: {"content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}}},
121121
)
122-
def test_octet_stream():
122+
def octet_stream():
123123
return
124124

125125

126+
@test_router.get("/no_response")
127+
def no_response():
128+
pass
129+
130+
131+
@test_router.get(
132+
"/unsupported_content",
133+
responses={200: {"content": {"not_real/content-type": {"schema": {"type": "string", "format": "binary"}}}}},
134+
)
135+
def unsupported_content():
136+
pass
137+
138+
126139
app.include_router(test_router, prefix="/tests", tags=["tests"])
127140

128141

end_to_end_tests/fastapi_app/openapi.json

+50-6
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,13 @@
289289
}
290290
}
291291
},
292-
"/tests/test_defaults": {
292+
"/tests/defaults": {
293293
"post": {
294294
"tags": [
295295
"tests"
296296
],
297-
"summary": "Test Defaults",
298-
"operationId": "test_defaults_tests_test_defaults_post",
297+
"summary": "Defaults",
298+
"operationId": "defaults_tests_defaults_post",
299299
"parameters": [
300300
{
301301
"required": false,
@@ -439,13 +439,13 @@
439439
}
440440
}
441441
},
442-
"/tests/test_octet_stream": {
442+
"/tests/octet_stream": {
443443
"get": {
444444
"tags": [
445445
"tests"
446446
],
447-
"summary": "Test Octet Stream",
448-
"operationId": "test_octet_stream_tests_test_octet_stream_get",
447+
"summary": "Octet Stream",
448+
"operationId": "octet_stream_tests_octet_stream_get",
449449
"responses": {
450450
"200": {
451451
"description": "Successful Response",
@@ -460,6 +460,50 @@
460460
}
461461
}
462462
}
463+
},
464+
"/tests/no_response": {
465+
"get": {
466+
"tags": [
467+
"tests"
468+
],
469+
"summary": "No Response",
470+
"operationId": "no_response_tests_no_response_get",
471+
"responses": {
472+
"200": {
473+
"description": "Successful Response",
474+
"content": {
475+
"application/json": {
476+
"schema": {}
477+
}
478+
}
479+
}
480+
}
481+
}
482+
},
483+
"/tests/unsupported_content": {
484+
"get": {
485+
"tags": [
486+
"tests"
487+
],
488+
"summary": "Unsupported Content",
489+
"operationId": "unsupported_content_tests_unsupported_content_get",
490+
"responses": {
491+
"200": {
492+
"description": "Successful Response",
493+
"content": {
494+
"application/json": {
495+
"schema": {}
496+
},
497+
"not_real/content-type": {
498+
"schema": {
499+
"type": "string",
500+
"format": "binary"
501+
}
502+
}
503+
}
504+
}
505+
}
506+
}
463507
}
464508
},
465509
"components": {
+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def _get_kwargs(
2424
union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float",
2525
enum_prop: Optional[AnEnum] = None,
2626
) -> Dict[str, Any]:
27-
url = "{}/tests/test_defaults".format(client.base_url)
27+
url = "{}/tests/defaults".format(client.base_url)
2828

2929
headers: Dict[str, Any] = client.get_headers()
3030

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from dataclasses import asdict
2+
from typing import Any, Dict, List, Optional, Union, cast
3+
4+
import httpx
5+
6+
from ...client import AuthenticatedClient, Client
7+
from ...types import Response
8+
9+
10+
def _get_kwargs(
11+
*,
12+
client: Client,
13+
) -> Dict[str, Any]:
14+
url = "{}/tests/no_response".format(client.base_url)
15+
16+
headers: Dict[str, Any] = client.get_headers()
17+
18+
return {
19+
"url": url,
20+
"headers": headers,
21+
"cookies": client.get_cookies(),
22+
"timeout": client.get_timeout(),
23+
}
24+
25+
26+
def _build_response(*, response: httpx.Response) -> Response[None]:
27+
return Response(
28+
status_code=response.status_code,
29+
content=response.content,
30+
headers=response.headers,
31+
parsed=None,
32+
)
33+
34+
35+
def sync_detailed(
36+
*,
37+
client: Client,
38+
) -> Response[None]:
39+
kwargs = _get_kwargs(
40+
client=client,
41+
)
42+
43+
response = httpx.get(
44+
**kwargs,
45+
)
46+
47+
return _build_response(response=response)
48+
49+
50+
async def asyncio_detailed(
51+
*,
52+
client: Client,
53+
) -> Response[None]:
54+
kwargs = _get_kwargs(
55+
client=client,
56+
)
57+
58+
async with httpx.AsyncClient() as _client:
59+
response = await _client.get(**kwargs)
60+
61+
return _build_response(response=response)
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def _get_kwargs(
1111
*,
1212
client: Client,
1313
) -> Dict[str, Any]:
14-
url = "{}/tests/test_octet_stream".format(client.base_url)
14+
url = "{}/tests/octet_stream".format(client.base_url)
1515

1616
headers: Dict[str, Any] = client.get_headers()
1717

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from dataclasses import asdict
2+
from typing import Any, Dict, List, Optional, Union, cast
3+
4+
import httpx
5+
6+
from ...client import AuthenticatedClient, Client
7+
from ...types import Response
8+
9+
10+
def _get_kwargs(
11+
*,
12+
client: Client,
13+
) -> Dict[str, Any]:
14+
url = "{}/tests/unsupported_content".format(client.base_url)
15+
16+
headers: Dict[str, Any] = client.get_headers()
17+
18+
return {
19+
"url": url,
20+
"headers": headers,
21+
"cookies": client.get_cookies(),
22+
"timeout": client.get_timeout(),
23+
}
24+
25+
26+
def _build_response(*, response: httpx.Response) -> Response[None]:
27+
return Response(
28+
status_code=response.status_code,
29+
content=response.content,
30+
headers=response.headers,
31+
parsed=None,
32+
)
33+
34+
35+
def sync_detailed(
36+
*,
37+
client: Client,
38+
) -> Response[None]:
39+
kwargs = _get_kwargs(
40+
client=client,
41+
)
42+
43+
response = httpx.get(
44+
**kwargs,
45+
)
46+
47+
return _build_response(response=response)
48+
49+
50+
async def asyncio_detailed(
51+
*,
52+
client: Client,
53+
) -> Response[None]:
54+
kwargs = _get_kwargs(
55+
client=client,
56+
)
57+
58+
async with httpx.AsyncClient() as _client:
59+
response = await _client.get(**kwargs)
60+
61+
return _build_response(response=response)

openapi_python_client/cli.py

+20-17
Original file line numberDiff line numberDiff line change
@@ -43,40 +43,41 @@ def cli(
4343

4444
def _print_parser_error(e: GeneratorError, color: str) -> None:
4545
typer.secho(e.header, bold=True, fg=color, err=True)
46+
typer.echo()
4647
if e.detail:
4748
typer.secho(e.detail, fg=color, err=True)
49+
typer.echo()
4850

4951
if isinstance(e, ParseError) and e.data is not None:
5052
formatted_data = pformat(e.data)
5153
typer.secho(formatted_data, fg=color, err=True)
5254

53-
typer.secho()
55+
typer.echo()
5456

5557

5658
def handle_errors(errors: Sequence[GeneratorError]) -> None:
5759
""" Turn custom errors into formatted error messages """
5860
if len(errors) == 0:
5961
return
62+
error_level = ErrorLevel.WARNING
63+
message = "Warning(s) encountered while generating. Client was generated, but some pieces may be missing"
64+
header_color = typer.colors.BRIGHT_YELLOW
6065
color = typer.colors.YELLOW
6166
for error in errors:
6267
if error.level == ErrorLevel.ERROR:
63-
typer.secho(
64-
"Error(s) encountered while generating, client was not created",
65-
underline=True,
66-
bold=True,
67-
fg=typer.colors.BRIGHT_RED,
68-
err=True,
69-
)
68+
error_level = ErrorLevel.ERROR
69+
message = "Error(s) encountered while generating, client was not created"
7070
color = typer.colors.RED
71+
header_color = typer.colors.BRIGHT_RED
7172
break
72-
else:
73-
typer.secho(
74-
"Warning(s) encountered while generating. Client was generated, but some pieces may be missing",
75-
underline=True,
76-
bold=True,
77-
fg=typer.colors.BRIGHT_YELLOW,
78-
err=True,
79-
)
73+
typer.secho(
74+
message,
75+
underline=True,
76+
bold=True,
77+
fg=header_color,
78+
err=True,
79+
)
80+
typer.echo()
8081

8182
for err in errors:
8283
_print_parser_error(err, color)
@@ -90,7 +91,9 @@ def handle_errors(errors: Sequence[GeneratorError]) -> None:
9091
fg=typer.colors.BLUE,
9192
err=True,
9293
)
93-
raise typer.Exit(code=1)
94+
95+
if error_level == ErrorLevel.ERROR:
96+
raise typer.Exit(code=1)
9497

9598

9699
@app.command()

0 commit comments

Comments
 (0)