Skip to content

Commit 4cb6182

Browse files
gaarutyunovdbanty
andauthored
content_type_overrides (#1010)
Implemented content_type_overrides from #657 #810 --------- Co-authored-by: Dylan Anthony <[email protected]> Co-authored-by: Dylan Anthony <[email protected]>
1 parent a289f27 commit 4cb6182

File tree

15 files changed

+311
-9
lines changed

15 files changed

+311
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Add config option to override content types
6+
7+
You can now define a `content_type_overrides` field in your `config.yml`:
8+
9+
```yaml
10+
content_type_overrides:
11+
application/zip: application/octet-stream
12+
```
13+
14+
This allows `openapi-python-client` to generate code for content types it doesn't recognize.
15+
16+
PR #1010 closes #810. Thanks @gaarutyunov!

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ If this option results in conflicts, you will need to manually override class na
156156

157157
By default, the timeout for retrieving the schema file via HTTP is 5 seconds. In case there is an error when retrieving the schema, you might try and increase this setting to a higher value.
158158

159+
### content_type_overrides
160+
161+
Normally, `openapi-python-client` will skip any bodies or responses that it doesn't recognize the content type for.
162+
This config tells the generator to treat a given content type like another.
163+
164+
```yaml
165+
content_type_overrides:
166+
application/zip: application/octet-stream
167+
```
168+
159169
[changelog.md]: CHANGELOG.md
160170
[poetry]: https://python-poetry.org/
161171
[PDM]: https://pdm-project.org/latest/

end_to_end_tests/baseline_openapi_3.0.json

+30
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,36 @@
15781578
}
15791579
}
15801580
}
1581+
},
1582+
"/config/content-type-override": {
1583+
"post": {
1584+
"tags": [
1585+
"config"
1586+
],
1587+
"summary": "Content Type Override",
1588+
"operationId": "content_type_override",
1589+
"requestBody": {
1590+
"content": {
1591+
"openapi/python/client": {
1592+
"schema": {
1593+
"type": "string"
1594+
}
1595+
}
1596+
}
1597+
},
1598+
"responses": {
1599+
"200": {
1600+
"description": "Successful Response",
1601+
"content": {
1602+
"openapi/python/client": {
1603+
"schema": {
1604+
"type": "string"
1605+
}
1606+
}
1607+
}
1608+
}
1609+
}
1610+
}
15811611
}
15821612
},
15831613
"components": {

end_to_end_tests/baseline_openapi_3.1.yaml

+30
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,36 @@ info:
15721572
}
15731573
}
15741574
}
1575+
},
1576+
"/config/content-type-override": {
1577+
"post": {
1578+
"tags": [
1579+
"config"
1580+
],
1581+
"summary": "Content Type Override",
1582+
"operationId": "content_type_override",
1583+
"requestBody": {
1584+
"content": {
1585+
"openapi/python/client": {
1586+
"schema": {
1587+
"type": "string"
1588+
}
1589+
}
1590+
}
1591+
},
1592+
"responses": {
1593+
"200": {
1594+
"description": "Successful Response",
1595+
"content": {
1596+
"openapi/python/client": {
1597+
"schema": {
1598+
"type": "string"
1599+
}
1600+
}
1601+
}
1602+
}
1603+
}
1604+
}
15751605
}
15761606
}
15771607
"components": {

end_to_end_tests/config.yml

+2
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ class_overrides:
99
class_name: AnEnumValue
1010
module_name: an_enum_value
1111
field_prefix: attr_
12+
content_type_overrides:
13+
openapi/python/client: application/json

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Type
44

55
from .bodies import BodiesEndpoints
6+
from .config import ConfigEndpoints
67
from .default import DefaultEndpoints
78
from .defaults import DefaultsEndpoints
89
from .enums import EnumsEndpoints
@@ -64,3 +65,7 @@ def naming(cls) -> Type[NamingEndpoints]:
6465
@classmethod
6566
def parameter_references(cls) -> Type[ParameterReferencesEndpoints]:
6667
return ParameterReferencesEndpoints
68+
69+
@classmethod
70+
def config(cls) -> Type[ConfigEndpoints]:
71+
return ConfigEndpoints
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Contains methods for accessing the API Endpoints"""
2+
3+
import types
4+
5+
from . import content_type_override
6+
7+
8+
class ConfigEndpoints:
9+
@classmethod
10+
def content_type_override(cls) -> types.ModuleType:
11+
"""
12+
Content Type Override
13+
"""
14+
return content_type_override

end_to_end_tests/golden-record/my_test_api_client/api/config/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from http import HTTPStatus
2+
from typing import Any, Dict, Optional, Union, cast
3+
4+
import httpx
5+
6+
from ... import errors
7+
from ...client import AuthenticatedClient, Client
8+
from ...types import Response
9+
10+
11+
def _get_kwargs(
12+
*,
13+
body: str,
14+
) -> Dict[str, Any]:
15+
headers: Dict[str, Any] = {}
16+
17+
_kwargs: Dict[str, Any] = {
18+
"method": "post",
19+
"url": "/config/content-type-override",
20+
}
21+
22+
_body = body
23+
24+
_kwargs["json"] = _body
25+
headers["Content-Type"] = "openapi/python/client"
26+
27+
_kwargs["headers"] = headers
28+
return _kwargs
29+
30+
31+
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[str]:
32+
if response.status_code == HTTPStatus.OK:
33+
response_200 = cast(str, response.json())
34+
return response_200
35+
if client.raise_on_unexpected_status:
36+
raise errors.UnexpectedStatus(response.status_code, response.content)
37+
else:
38+
return None
39+
40+
41+
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[str]:
42+
return Response(
43+
status_code=HTTPStatus(response.status_code),
44+
content=response.content,
45+
headers=response.headers,
46+
parsed=_parse_response(client=client, response=response),
47+
)
48+
49+
50+
def sync_detailed(
51+
*,
52+
client: Union[AuthenticatedClient, Client],
53+
body: str,
54+
) -> Response[str]:
55+
"""Content Type Override
56+
57+
Args:
58+
body (str):
59+
60+
Raises:
61+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
62+
httpx.TimeoutException: If the request takes longer than Client.timeout.
63+
64+
Returns:
65+
Response[str]
66+
"""
67+
68+
kwargs = _get_kwargs(
69+
body=body,
70+
)
71+
72+
response = client.get_httpx_client().request(
73+
**kwargs,
74+
)
75+
76+
return _build_response(client=client, response=response)
77+
78+
79+
def sync(
80+
*,
81+
client: Union[AuthenticatedClient, Client],
82+
body: str,
83+
) -> Optional[str]:
84+
"""Content Type Override
85+
86+
Args:
87+
body (str):
88+
89+
Raises:
90+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
91+
httpx.TimeoutException: If the request takes longer than Client.timeout.
92+
93+
Returns:
94+
str
95+
"""
96+
97+
return sync_detailed(
98+
client=client,
99+
body=body,
100+
).parsed
101+
102+
103+
async def asyncio_detailed(
104+
*,
105+
client: Union[AuthenticatedClient, Client],
106+
body: str,
107+
) -> Response[str]:
108+
"""Content Type Override
109+
110+
Args:
111+
body (str):
112+
113+
Raises:
114+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
115+
httpx.TimeoutException: If the request takes longer than Client.timeout.
116+
117+
Returns:
118+
Response[str]
119+
"""
120+
121+
kwargs = _get_kwargs(
122+
body=body,
123+
)
124+
125+
response = await client.get_async_httpx_client().request(**kwargs)
126+
127+
return _build_response(client=client, response=response)
128+
129+
130+
async def asyncio(
131+
*,
132+
client: Union[AuthenticatedClient, Client],
133+
body: str,
134+
) -> Optional[str]:
135+
"""Content Type Override
136+
137+
Args:
138+
body (str):
139+
140+
Raises:
141+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
142+
httpx.TimeoutException: If the request takes longer than Client.timeout.
143+
144+
Returns:
145+
str
146+
"""
147+
148+
return (
149+
await asyncio_detailed(
150+
client=client,
151+
body=body,
152+
)
153+
).parsed

openapi_python_client/config.py

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class ConfigFile(BaseModel):
3535
"""
3636

3737
class_overrides: Optional[Dict[str, ClassOverride]] = None
38+
content_type_overrides: Optional[Dict[str, str]] = None
3839
project_name_override: Optional[str] = None
3940
package_name_override: Optional[str] = None
4041
package_version_override: Optional[str] = None
@@ -70,6 +71,7 @@ class Config:
7071
http_timeout: int
7172
document_source: Union[Path, str]
7273
file_encoding: str
74+
content_type_overrides: Dict[str, str]
7375

7476
@staticmethod
7577
def from_sources(
@@ -91,6 +93,7 @@ def from_sources(
9193
config = Config(
9294
meta_type=meta_type,
9395
class_overrides=config_file.class_overrides or {},
96+
content_type_overrides=config_file.content_type_overrides or {},
9497
project_name_override=config_file.project_name_override,
9598
package_name_override=config_file.package_name_override,
9699
package_version_override=config_file.package_version_override,

openapi_python_client/parser/bodies.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def body_from_data(
5656
prefix_type_names = len(body_content) > 1
5757

5858
for content_type, media_type in body_content.items():
59-
simplified_content_type = get_content_type(content_type)
59+
simplified_content_type = get_content_type(content_type, config)
6060
if simplified_content_type is None:
6161
bodies.append(
6262
ParseError(

openapi_python_client/parser/responses.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ class Response:
3737
data: Union[oai.Response, oai.Reference] # Original data which created this response, useful for custom templates
3838

3939

40-
def _source_by_content_type(content_type: str) -> Optional[_ResponseSource]:
41-
parsed_content_type = utils.get_content_type(content_type)
40+
def _source_by_content_type(content_type: str, config: Config) -> Optional[_ResponseSource]:
41+
parsed_content_type = utils.get_content_type(content_type, config)
4242
if parsed_content_type is None:
4343
return None
4444

@@ -114,7 +114,7 @@ def response_from_data(
114114
)
115115

116116
for content_type, media_type in content.items():
117-
source = _source_by_content_type(content_type)
117+
source = _source_by_content_type(content_type, config)
118118
if source is not None:
119119
schema_data = media_type.media_type_schema
120120
break

openapi_python_client/utils.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from keyword import iskeyword
77
from typing import Any
88

9+
from .config import Config
10+
911
DELIMITERS = r"\. _-"
1012

1113

@@ -105,10 +107,11 @@ def remove_string_escapes(value: str) -> str:
105107
return value.replace('"', r"\"")
106108

107109

108-
def get_content_type(content_type: str) -> str | None:
110+
def get_content_type(content_type: str, config: Config) -> str | None:
109111
"""
110112
Given a string representing a content type with optional parameters, returns the content type only
111113
"""
114+
content_type = config.content_type_overrides.get(content_type, content_type)
112115
message = Message()
113116
message.add_header("Content-Type", content_type)
114117

0 commit comments

Comments
 (0)