Skip to content

Commit

Permalink
fix(python): file properties now respect custom content type (#4653)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsinghvi authored Sep 16, 2024
1 parent 95c82ca commit 2afa278
Show file tree
Hide file tree
Showing 477 changed files with 4,350 additions and 3,257 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/seed.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: seed

on:
push:
branches:
- main
pull_request:
branches:
- main
Expand Down
41 changes: 30 additions & 11 deletions generators/python/core_utilities/shared/file.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import typing
from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast

# File typing inspired by the flexibility of types within the httpx library
# https://github.com/encode/httpx/blob/master/httpx/_types.py
FileContent = typing.Union[typing.IO[bytes], bytes, str]
File = typing.Union[
FileContent = Union[IO[bytes], bytes, str]
File = Union[
# file (or bytes)
FileContent,
# (filename, file (or bytes))
typing.Tuple[typing.Optional[str], FileContent],
Tuple[Optional[str], FileContent],
# (filename, file (or bytes), content_type)
typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str]],
Tuple[Optional[str], FileContent, Optional[str]],
# (filename, file (or bytes), content_type, headers)
typing.Tuple[
typing.Optional[str],
Tuple[
Optional[str],
FileContent,
typing.Optional[str],
typing.Mapping[str, str],
Optional[str],
Mapping[str, str],
],
]


def convert_file_dict_to_httpx_tuples(
d: typing.Dict[str, typing.Union[File, typing.List[File]]],
) -> typing.List[typing.Tuple[str, File]]:
d: Dict[str, Union[File, List[File]]],
) -> List[Tuple[str, File]]:
"""
The format we use is a list of tuples, where the first element is the
name of the file and the second is the file object. Typically HTTPX wants
Expand All @@ -39,3 +39,22 @@ def convert_file_dict_to_httpx_tuples(
else:
httpx_tuples.append((key, file_like))
return httpx_tuples


def with_content_type(*, file: File, content_type: str) -> File:
""" """
if isinstance(file, tuple):
if len(file) == 2:
filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore
return (filename, content, content_type)
elif len(file) == 3:
filename, content, _ = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore
return (filename, content, content_type)
elif len(file) == 4:
filename, content, _, headers = cast( # type: ignore
Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file
)
return (filename, content, content_type, headers)
else:
raise ValueError(f"Unexpected tuple length: {len(file)}")
return (None, file, content_type)
13 changes: 13 additions & 0 deletions generators/python/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# For unreleased changes, use unreleased.yml
- version: 4.2.3
irVersion: 53
changelogEntry:
- type: fix
summary: |
The content type of file properties is now respected for multipart
requests. For example, if you have a file property called `image` that has the
content type `image/jpeg`, then it will be sent as:
```python
"image": core.with_content_type(file=image, content_type="image/jpeg"),
````
- version: 4.2.2
irVersion: 53
changelogEntry:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,21 @@ def write(writer: AST.NodeWriter) -> None:
)
writer.write_line(f', "{property_as_union.content_type}"),')
elif property_as_union.type == "file":
writer.write_line(
f'"{property_as_union.value.get_as_union().key.wire_value}": {self._get_file_property_name(property_as_union.value)},'
)
file_property_as_union = property_as_union.value.get_as_union()
if file_property_as_union.content_type is not None:
writer.write(f'"{file_property_as_union.key.wire_value}": ')
writer.write_node(
self._context.core_utilities.with_content_type(
AST.Expression(
f'file={file_property_as_union.key.wire_value}, content_type="{file_property_as_union.content_type}"'
)
)
)
writer.write_line(",")
else:
writer.write_line(
f'"{file_property_as_union.key.wire_value}": {self._get_file_property_name(property_as_union.value)},'
)
writer.write_line("}")

return AST.Expression(AST.CodeWriter(write))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def copy_to_project(self, *, project: Project) -> None:
directories=self.filepath,
file=Filepath.FilepathPart(module_name="file"),
),
exports={"File", "convert_file_dict_to_httpx_tuples"},
exports={"File", "convert_file_dict_to_httpx_tuples", "with_content_type"},
)
self._copy_file_to_project(
project=project,
Expand Down Expand Up @@ -355,6 +355,19 @@ def httpx_tuple_converter(self, obj: AST.Expression) -> AST.Expression:
)
)

def with_content_type(self, obj: AST.Expression) -> AST.Expression:
return AST.Expression(
AST.FunctionInvocation(
function_definition=AST.Reference(
qualified_name_excluding_import=("with_content_type",),
import_=AST.ReferenceImport(
module=AST.Module.local(*self._module_path_unnamed), named_import="core"
),
),
args=[obj],
)
)

def http_client(
self,
base_client: AST.Expression,
Expand Down
45 changes: 45 additions & 0 deletions generators/python/tests/utils/test_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Optional, Tuple, cast
import pytest
from io import BytesIO

from core_utilities.shared.file import FileContent, with_content_type


def test_file_content_bytes() -> None:
result = with_content_type(file=b"file content", content_type="text/plain")
assert result == (None, b"file content", "text/plain")

def test_file_content_str() -> None:
result = with_content_type(file="file content", content_type="text/plain")
assert result == (None, "file content", "text/plain")

def test_file_content_io() -> None:
file_like = BytesIO(b"file content")
result = with_content_type(file=file_like, content_type="text/plain")
filename, content, contentType = cast(Tuple[Optional[str], FileContent, Optional[str]], result)
assert filename is None
assert content == file_like
assert contentType == "text/plain"

def test_tuple_2() -> None:
result = with_content_type(file=("example.txt", b"file content"), content_type="text/plain")
assert result == ("example.txt", b"file content", "text/plain")

def test_tuple_3() -> None:
result = with_content_type(file=("example.txt", b"file content", "application/octet-stream"), content_type="text/plain")
assert result == ("example.txt", b"file content", "text/plain")

def test_tuple_4() -> None:
result = with_content_type(file=("example.txt", b"file content", "application/octet-stream", {"X-Custom": "value"}), content_type="text/plain")
assert result == ("example.txt", b"file content", "text/plain", {"X-Custom": "value"})

def test_none_filename() -> None:
result = with_content_type(file=(None, b"file content"), content_type="text/plain")
assert result == (None, b"file content", "text/plain")

def test_invalid_tuple_length() -> None:
with pytest.raises(ValueError):
with_content_type(
file=("example.txt", b"file content", "text/plain", {}, "extra"), # type: ignore
content_type="application/json"
)
Original file line number Diff line number Diff line change
Expand Up @@ -2182,7 +2182,7 @@
"wireValue": "file"
},
"isOptional": false,
"contentType": null
"contentType": "application/octet-stream"
}
},
{
Expand Down
4 changes: 3 additions & 1 deletion seed/csharp-model/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/csharp-sdk/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion seed/fastapi/examples/resources/types/types/moment.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/go-fiber/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/go-model/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/go-sdk/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/java-model/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/java-sdk/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/openapi/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/php-model/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/php-sdk/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/postman/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion seed/pydantic/file-upload/.mock/definition/service.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion seed/python-sdk/alias-extends/src/seed/core/__init__.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2afa278

Please sign in to comment.