Skip to content

Commit 55cb94a

Browse files
committed
fix: Multipart uploads for httpx >= 0.19.0 [openapi-generators#508, openapi-generators#548]. Thanks @skuo1-ilmn & @kairntech!
Fixes openapi-generators#508 BREAKING CHANGE: `File` uploads can now only accept binary payloads (`BinaryIO`).
1 parent c9a4d03 commit 55cb94a

36 files changed

+1367
-67
lines changed

.github/workflows/checks.yml

+63
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ jobs:
6161
- name: Run pylint
6262
run: poetry run pylint openapi_python_client
6363

64+
- name: Regenerate Golden Record
65+
run: poetry run task regen_e2e
66+
6467
- name: Run pytest
6568
run: poetry run pytest --cov=openapi_python_client --cov-report=term-missing tests end_to_end_tests/test_end_to_end.py --basetemp=tests/tmp
6669
env:
@@ -73,3 +76,63 @@ jobs:
7376
- uses: codecov/codecov-action@v2
7477
with:
7578
files: ./coverage.xml
79+
80+
- uses: stefanzweifel/git-auto-commit-action@v4
81+
if: runner.os == 'Linux'
82+
with:
83+
commit_message: "chore: Regenerate E2E Golden Record"
84+
file_pattern: end_to_end_tests/golden-record end_to_end_tests/custom-templates-golden-record
85+
86+
integration:
87+
name: Integration Tests
88+
runs-on: ubuntu-latest
89+
services:
90+
openapi-test-server:
91+
image: ghcr.io/openapi-generators/openapi-test-server:latest
92+
ports:
93+
- "3000:3000"
94+
steps:
95+
- uses: actions/checkout@v2
96+
- name: Set up Python
97+
uses: actions/setup-python@v2
98+
with:
99+
python-version: "3.10"
100+
- name: Get Python Version
101+
id: get_python_version
102+
run: echo "::set-output name=python_version::$(python --version)"
103+
- name: Cache dependencies
104+
uses: actions/cache@v2
105+
with:
106+
path: .venv
107+
key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies-${{ hashFiles('**/poetry.lock') }}
108+
restore-keys: |
109+
${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies
110+
- name: Install dependencies
111+
run: |
112+
pip install poetry
113+
python -m venv .venv
114+
poetry run python -m pip install --upgrade pip
115+
poetry install
116+
- name: Regenerate Integration Client
117+
run: |
118+
poetry run openapi-python-client update --url http://localhost:3000/openapi.json --config integration-tests-config.yaml
119+
- name: Cache Generated Client Dependencies
120+
uses: actions/cache@v2
121+
with:
122+
path: integration-tests/.venv
123+
key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies-${{ hashFiles('**/poetry.lock') }}
124+
restore-keys: |
125+
${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies
126+
- name: Install Integration Dependencies
127+
run: |
128+
cd integration-tests
129+
python -m venv .venv
130+
poetry run python -m pip install --upgrade pip
131+
poetry install
132+
- name: Run Tests
133+
run: |
134+
cd integration-tests
135+
poetry run pytest
136+
- uses: stefanzweifel/git-auto-commit-action@v4
137+
with:
138+
commit_message: "chore: Regenerate Integration Client"

CHANGELOG.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
Breaking changes to any of the following will cause the **minor** version to be incremented (as long as this project is 0.x). Only these pieces are considered part of the public API:
88

9-
1. The _behavior_ of the generated code. Specifically, the way in which generated endpoints and classes are called and the way in which those calls communicate with an OpenAPI server. Any other property of the generated code is not considered part of the versioned, public API (e.g., code formatting, comments).
10-
2. The invocation of the CLI (e.g., commands or arguments).
9+
- The _behavior_ of the generated code. Specifically, the way in which generated endpoints and classes are called and the way in which those calls communicate with an OpenAPI server. Any other property of the generated code is not considered part of the versioned, public API (e.g., code formatting, comments).
10+
- The invocation of the CLI (e.g., commands or arguments).
1111

1212
Programmatic usage of this project (e.g., importing it as a Python module) and the usage of custom templates are not considered part of the public API and therefore may change behavior at any time without notice.
1313

CONTRIBUTING.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121

2222
2. When in a Poetry shell (`poetry shell`) run `task check` in order to run most of the same checks CI runs. This will auto-reformat the code, check type annotations, run unit tests, check code coverage, and lint the code.
2323

24-
### Rework end to end tests
24+
### Rework end-to-end tests
2525

26-
3. If you're writing a new feature, try to add it to the end to end test.
26+
3. If you're writing a new feature, try to add it to the end-to-end test.
2727
1. If adding support for a new OpenAPI feature, add it somewhere in `end_to_end_tests/openapi.json`
28-
2. Regenerate the "golden records" with `task regen`. This client is generated from the OpenAPI document used for end to end testing.
28+
2. Regenerate the "golden records" with `task regen`. This client is generated from the OpenAPI document used for end-to-end testing.
2929
3. Check the changes to `end_to_end_tests/golden-record` to confirm only what you intended to change did change and that the changes look correct.
30-
4. Run the end to end tests with `task e2e`. This will generate clients against `end_to_end_tests/openapi.json` and compare them with the golden record. The tests will fail if **anything is different**. The end to end tests are not included in `task check` as they take longer to run and don't provide very useful feedback in the event of failure. If an e2e test does fail, the easiest way to check what's wrong is to run `task regen` and check the diffs. You can also use `task re` which will run `regen` and `e2e` in that order.
30+
4. **If you added a test above OR modified the templates**: Run the end-to-end tests with `task e2e`. This will generate clients against `end_to_end_tests/openapi.json` and compare them with the golden record. The tests will fail if **anything is different**. The end-to-end tests are not included in `task check` as they take longer to run and don't provide very useful feedback in the event of failure. If an e2e test does fail, the easiest way to check what's wrong is to run `task regen` and check the diffs. You can also use `task re` which will run `regen` and `e2e` in that order.
31+
3132

3233
## Creating a Pull Request
3334

end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -102,36 +102,40 @@ def to_dict(self) -> Dict[str, Any]:
102102
def to_multipart(self) -> Dict[str, Any]:
103103
some_file = self.some_file.to_tuple()
104104

105-
some_object = (None, json.dumps(self.some_object.to_dict()), "application/json")
105+
some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json")
106106

107107
some_optional_file: Union[Unset, FileJsonType] = UNSET
108108
if not isinstance(self.some_optional_file, Unset):
109109
some_optional_file = self.some_optional_file.to_tuple()
110110

111-
some_string = self.some_string if self.some_string is UNSET else (None, str(self.some_string), "text/plain")
112-
some_number = self.some_number if self.some_number is UNSET else (None, str(self.some_number), "text/plain")
113-
some_array: Union[Unset, Tuple[None, str, str]] = UNSET
111+
some_string = (
112+
self.some_string if self.some_string is UNSET else (None, str(self.some_string).encode(), "text/plain")
113+
)
114+
some_number = (
115+
self.some_number if self.some_number is UNSET else (None, str(self.some_number).encode(), "text/plain")
116+
)
117+
some_array: Union[Unset, Tuple[None, bytes, str]] = UNSET
114118
if not isinstance(self.some_array, Unset):
115119
_temp_some_array = self.some_array
116-
some_array = (None, json.dumps(_temp_some_array), "application/json")
120+
some_array = (None, json.dumps(_temp_some_array).encode(), "application/json")
117121

118-
some_optional_object: Union[Unset, Tuple[None, str, str]] = UNSET
122+
some_optional_object: Union[Unset, Tuple[None, bytes, str]] = UNSET
119123
if not isinstance(self.some_optional_object, Unset):
120-
some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()), "application/json")
124+
some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json")
121125

122126
some_nullable_object = (
123-
(None, json.dumps(self.some_nullable_object.to_dict()), "application/json")
127+
(None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json")
124128
if self.some_nullable_object
125129
else None
126130
)
127131

128-
some_enum: Union[Unset, Tuple[None, str, str]] = UNSET
132+
some_enum: Union[Unset, Tuple[None, bytes, str]] = UNSET
129133
if not isinstance(self.some_enum, Unset):
130-
some_enum = (None, str(self.some_enum.value), "text/plain")
134+
some_enum = (None, str(self.some_enum.value).encode(), "text/plain")
131135

132136
field_dict: Dict[str, Any] = {}
133137
for prop_name, prop in self.additional_properties.items():
134-
field_dict[prop_name] = (None, json.dumps(prop.to_dict()), "application/json")
138+
field_dict[prop_name] = (None, json.dumps(prop.to_dict()).encode(), "application/json")
135139

136140
field_dict.update(
137141
{

end_to_end_tests/golden-record/my_test_api_client/types.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
""" Contains some shared types for properties """
2-
from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union
2+
from typing import BinaryIO, Generic, MutableMapping, Optional, Tuple, TypeVar
33

44
import attr
55

@@ -11,14 +11,14 @@ def __bool__(self) -> bool:
1111

1212
UNSET: Unset = Unset()
1313

14-
FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]
14+
FileJsonType = Tuple[Optional[str], BinaryIO, Optional[str]]
1515

1616

1717
@attr.s(auto_attribs=True)
1818
class File:
1919
"""Contains information for file uploads"""
2020

21-
payload: Union[BinaryIO, TextIO]
21+
payload: BinaryIO
2222
file_name: Optional[str] = None
2323
mime_type: Optional[str] = None
2424

integration-tests-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
project_name_override: integration-tests

integration-tests/.gitignore

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
__pycache__/
2+
build/
3+
dist/
4+
*.egg-info/
5+
.pytest_cache/
6+
7+
# pyenv
8+
.python-version
9+
10+
# Environments
11+
.env
12+
.venv
13+
14+
# mypy
15+
.mypy_cache/
16+
.dmypy.json
17+
dmypy.json
18+
19+
# JetBrains
20+
.idea/
21+
22+
/coverage.xml
23+
/.coverage

integration-tests/README.md

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# open-api-test-server-client
2+
A client library for accessing OpenAPI Test Server
3+
4+
## Usage
5+
First, create a client:
6+
7+
```python
8+
from integration_tests import Client
9+
10+
client = Client(base_url="https://api.example.com")
11+
```
12+
13+
If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:
14+
15+
```python
16+
from integration_tests import AuthenticatedClient
17+
18+
client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
19+
```
20+
21+
Now call your endpoint and use your models:
22+
23+
```python
24+
from integration_tests.models import MyDataModel
25+
from integration_tests.api.my_tag import get_my_data_model
26+
from integration_tests.types import Response
27+
28+
my_data: MyDataModel = get_my_data_model.sync(client=client)
29+
# or if you need more info (e.g. status_code)
30+
response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client)
31+
```
32+
33+
Or do the same thing with an async version:
34+
35+
```python
36+
from integration_tests.models import MyDataModel
37+
from integration_tests.api.my_tag import get_my_data_model
38+
from integration_tests.types import Response
39+
40+
my_data: MyDataModel = await get_my_data_model.asyncio(client=client)
41+
response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client)
42+
```
43+
44+
By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle.
45+
46+
```python
47+
client = AuthenticatedClient(
48+
base_url="https://internal_api.example.com",
49+
token="SuperSecretToken",
50+
verify_ssl="/path/to/certificate_bundle.pem",
51+
)
52+
```
53+
54+
You can also disable certificate validation altogether, but beware that **this is a security risk**.
55+
56+
```python
57+
client = AuthenticatedClient(
58+
base_url="https://internal_api.example.com",
59+
token="SuperSecretToken",
60+
verify_ssl=False
61+
)
62+
```
63+
64+
Things to know:
65+
1. Every path/method combo becomes a Python module with four functions:
66+
1. `sync`: Blocking request that returns parsed data (if successful) or `None`
67+
1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful.
68+
1. `asyncio`: Like `sync` but the async instead of blocking
69+
1. `asyncio_detailed`: Like `sync_detailed` by async instead of blocking
70+
71+
1. All path/query params, and bodies become method arguments.
72+
1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above)
73+
1. Any endpoint which did not have a tag will be in `open_api_test_server_client.api.default`
74+
75+
## Building / publishing this Client
76+
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
77+
1. Update the metadata in pyproject.toml (e.g. authors, version)
78+
1. If you're using a private repository, configure it with Poetry
79+
1. `poetry config repositories.<your-repository-name> <url-to-your-repository>`
80+
1. `poetry config http-basic.<your-repository-name> <username> <password>`
81+
1. Publish the client with `poetry publish --build -r <your-repository-name>` or, if for public PyPI, just `poetry publish --build`
82+
83+
If you want to install this client into another project without publishing it (e.g. for development) then:
84+
1. If that project **is using Poetry**, you can simply do `poetry add <path-to-this-client>` from that project
85+
1. If that project is not using Poetry:
86+
1. Build a wheel with `poetry build -f wheel`
87+
1. Install that wheel from the other project `pip install <path-to-wheel>`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
""" A client library for accessing OpenAPI Test Server """
2+
from .client import AuthenticatedClient, Client
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
""" Contains methods for accessing the API """

integration-tests/integration_tests/api/body/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)