Skip to content

content_type_overrides #1010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/add_config_option_to_override_content_types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
default: minor
---

# Add config option to override content types

You can now define a `content_type_overrides` field in your `config.yml`:

```yaml
content_type_overrides:
application/zip: application/octet-stream
```

This allows `openapi-python-client` to generate code for content types it doesn't recognize.

PR #1010 closes #810. Thanks @gaarutyunov!
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ If this option results in conflicts, you will need to manually override class na

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.

### content_type_overrides

Normally, `openapi-python-client` will skip any bodies or responses that it doesn't recognize the content type for.
This config tells the generator to treat a given content type like another.

```yaml
content_type_overrides:
application/zip: application/octet-stream
```

[changelog.md]: CHANGELOG.md
[poetry]: https://python-poetry.org/
[PDM]: https://pdm-project.org/latest/
Expand Down
30 changes: 30 additions & 0 deletions end_to_end_tests/baseline_openapi_3.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,36 @@
}
}
}
},
"/config/content-type-override": {
"post": {
"tags": [
"config"
],
"summary": "Content Type Override",
"operationId": "content_type_override",
"requestBody": {
"content": {
"openapi/python/client": {
"schema": {
"type": "string"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"openapi/python/client": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {
Expand Down
30 changes: 30 additions & 0 deletions end_to_end_tests/baseline_openapi_3.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,36 @@ info:
}
}
}
},
"/config/content-type-override": {
"post": {
"tags": [
"config"
],
"summary": "Content Type Override",
"operationId": "content_type_override",
"requestBody": {
"content": {
"openapi/python/client": {
"schema": {
"type": "string"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"openapi/python/client": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
"components": {
Expand Down
2 changes: 2 additions & 0 deletions end_to_end_tests/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ class_overrides:
class_name: AnEnumValue
module_name: an_enum_value
field_prefix: attr_
content_type_overrides:
openapi/python/client: application/json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Type

from .bodies import BodiesEndpoints
from .config import ConfigEndpoints
from .default import DefaultEndpoints
from .defaults import DefaultsEndpoints
from .enums import EnumsEndpoints
Expand Down Expand Up @@ -64,3 +65,7 @@ def naming(cls) -> Type[NamingEndpoints]:
@classmethod
def parameter_references(cls) -> Type[ParameterReferencesEndpoints]:
return ParameterReferencesEndpoints

@classmethod
def config(cls) -> Type[ConfigEndpoints]:
return ConfigEndpoints
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Contains methods for accessing the API Endpoints"""

import types

from . import content_type_override


class ConfigEndpoints:
@classmethod
def content_type_override(cls) -> types.ModuleType:
"""
Content Type Override
"""
return content_type_override
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from http import HTTPStatus
from typing import Any, Dict, Optional, Union, cast

import httpx

from ... import errors
from ...client import AuthenticatedClient, Client
from ...types import Response


def _get_kwargs(
*,
body: str,
) -> Dict[str, Any]:
headers: Dict[str, Any] = {}

_kwargs: Dict[str, Any] = {
"method": "post",
"url": "/config/content-type-override",
}

_body = body

_kwargs["json"] = _body
headers["Content-Type"] = "openapi/python/client"

_kwargs["headers"] = headers
return _kwargs


def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[str]:
if response.status_code == HTTPStatus.OK:
response_200 = cast(str, response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None


def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[str]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)


def sync_detailed(
*,
client: Union[AuthenticatedClient, Client],
body: str,
) -> Response[str]:
"""Content Type Override

Args:
body (str):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
Response[str]
"""

kwargs = _get_kwargs(
body=body,
)

response = client.get_httpx_client().request(
**kwargs,
)

return _build_response(client=client, response=response)


def sync(
*,
client: Union[AuthenticatedClient, Client],
body: str,
) -> Optional[str]:
"""Content Type Override

Args:
body (str):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
str
"""

return sync_detailed(
client=client,
body=body,
).parsed


async def asyncio_detailed(
*,
client: Union[AuthenticatedClient, Client],
body: str,
) -> Response[str]:
"""Content Type Override

Args:
body (str):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
Response[str]
"""

kwargs = _get_kwargs(
body=body,
)

response = await client.get_async_httpx_client().request(**kwargs)

return _build_response(client=client, response=response)


async def asyncio(
*,
client: Union[AuthenticatedClient, Client],
body: str,
) -> Optional[str]:
"""Content Type Override

Args:
body (str):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
str
"""

return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed
3 changes: 3 additions & 0 deletions openapi_python_client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ConfigFile(BaseModel):
"""

class_overrides: Optional[Dict[str, ClassOverride]] = None
content_type_overrides: Optional[Dict[str, str]] = None
project_name_override: Optional[str] = None
package_name_override: Optional[str] = None
package_version_override: Optional[str] = None
Expand Down Expand Up @@ -70,6 +71,7 @@ class Config:
http_timeout: int
document_source: Union[Path, str]
file_encoding: str
content_type_overrides: Dict[str, str]

@staticmethod
def from_sources(
Expand All @@ -91,6 +93,7 @@ def from_sources(
config = Config(
meta_type=meta_type,
class_overrides=config_file.class_overrides or {},
content_type_overrides=config_file.content_type_overrides or {},
project_name_override=config_file.project_name_override,
package_name_override=config_file.package_name_override,
package_version_override=config_file.package_version_override,
Expand Down
2 changes: 1 addition & 1 deletion openapi_python_client/parser/bodies.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def body_from_data(
prefix_type_names = len(body_content) > 1

for content_type, media_type in body_content.items():
simplified_content_type = get_content_type(content_type)
simplified_content_type = get_content_type(content_type, config)
if simplified_content_type is None:
bodies.append(
ParseError(
Expand Down
6 changes: 3 additions & 3 deletions openapi_python_client/parser/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class Response:
data: Union[oai.Response, oai.Reference] # Original data which created this response, useful for custom templates


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

Expand Down Expand Up @@ -114,7 +114,7 @@ def response_from_data(
)

for content_type, media_type in content.items():
source = _source_by_content_type(content_type)
source = _source_by_content_type(content_type, config)
if source is not None:
schema_data = media_type.media_type_schema
break
Expand Down
5 changes: 4 additions & 1 deletion openapi_python_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from keyword import iskeyword
from typing import Any

from .config import Config

DELIMITERS = r"\. _-"


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


def get_content_type(content_type: str) -> str | None:
def get_content_type(content_type: str, config: Config) -> str | None:
"""
Given a string representing a content type with optional parameters, returns the content type only
"""
content_type = config.content_type_overrides.get(content_type, content_type)
message = Message()
message.add_header("Content-Type", content_type)

Expand Down
Loading