Skip to content

fix: Multipart uploads for httpx >= 0.19.0 #548

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 15 commits into from
Jan 17, 2022
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
63 changes: 63 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ jobs:
- name: Run pylint
run: poetry run pylint openapi_python_client

- name: Regenerate Golden Record
run: poetry run task regen_e2e

- name: Run pytest
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
env:
Expand All @@ -73,3 +76,63 @@ jobs:
- uses: codecov/codecov-action@v2
with:
files: ./coverage.xml

- uses: stefanzweifel/git-auto-commit-action@v4
if: runner.os == 'Linux'
with:
commit_message: "chore: Regenerate E2E Golden Record"
file_pattern: end_to_end_tests/golden-record end_to_end_tests/custom-templates-golden-record

integration:
name: Integration Tests
runs-on: ubuntu-latest
services:
openapi-test-server:
image: ghcr.io/openapi-generators/openapi-test-server:latest
ports:
- "3000:3000"
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Get Python Version
id: get_python_version
run: echo "::set-output name=python_version::$(python --version)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: .venv
key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies
- name: Install dependencies
run: |
pip install poetry
python -m venv .venv
poetry run python -m pip install --upgrade pip
poetry install
- name: Regenerate Integration Client
run: |
poetry run openapi-python-client update --url http://localhost:3000/openapi.json --config integration-tests-config.yaml
- name: Cache Generated Client Dependencies
uses: actions/cache@v2
with:
path: integration-tests/.venv
key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies
- name: Install Integration Dependencies
run: |
cd integration-tests
python -m venv .venv
poetry run python -m pip install --upgrade pip
poetry install
- name: Run Tests
run: |
cd integration-tests
poetry run pytest
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "chore: Regenerate Integration Client"
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

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:

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).
2. The invocation of the CLI (e.g., commands or arguments).
- 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).
- The invocation of the CLI (e.g., commands or arguments).

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.

Expand Down
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@

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.

### Rework end to end tests
### Rework end-to-end tests

3. If you're writing a new feature, try to add it to the end to end test.
3. If you're writing a new feature, try to add it to the end-to-end test.
1. If adding support for a new OpenAPI feature, add it somewhere in `end_to_end_tests/openapi.json`
2. Regenerate the "golden records" with `task regen`. This client is generated from the OpenAPI document used for end to end testing.
2. Regenerate the "golden records" with `task regen`. This client is generated from the OpenAPI document used for end-to-end testing.
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.
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.
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.


## Creating a Pull Request

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,36 +102,40 @@ def to_dict(self) -> Dict[str, Any]:
def to_multipart(self) -> Dict[str, Any]:
some_file = self.some_file.to_tuple()

some_object = (None, json.dumps(self.some_object.to_dict()), "application/json")
some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json")

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

some_string = self.some_string if self.some_string is UNSET else (None, str(self.some_string), "text/plain")
some_number = self.some_number if self.some_number is UNSET else (None, str(self.some_number), "text/plain")
some_array: Union[Unset, Tuple[None, str, str]] = UNSET
some_string = (
self.some_string if self.some_string is UNSET else (None, str(self.some_string).encode(), "text/plain")
)
some_number = (
self.some_number if self.some_number is UNSET else (None, str(self.some_number).encode(), "text/plain")
)
some_array: Union[Unset, Tuple[None, bytes, str]] = UNSET
if not isinstance(self.some_array, Unset):
_temp_some_array = self.some_array
some_array = (None, json.dumps(_temp_some_array), "application/json")
some_array = (None, json.dumps(_temp_some_array).encode(), "application/json")

some_optional_object: Union[Unset, Tuple[None, str, str]] = UNSET
some_optional_object: Union[Unset, Tuple[None, bytes, str]] = UNSET
if not isinstance(self.some_optional_object, Unset):
some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()), "application/json")
some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json")

some_nullable_object = (
(None, json.dumps(self.some_nullable_object.to_dict()), "application/json")
(None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json")
if self.some_nullable_object
else None
)

some_enum: Union[Unset, Tuple[None, str, str]] = UNSET
some_enum: Union[Unset, Tuple[None, bytes, str]] = UNSET
if not isinstance(self.some_enum, Unset):
some_enum = (None, str(self.some_enum.value), "text/plain")
some_enum = (None, str(self.some_enum.value).encode(), "text/plain")

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

field_dict.update(
{
Expand Down
6 changes: 3 additions & 3 deletions end_to_end_tests/golden-record/my_test_api_client/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
""" Contains some shared types for properties """
from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union
from typing import BinaryIO, Generic, MutableMapping, Optional, Tuple, TypeVar

import attr

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

UNSET: Unset = Unset()

FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]
FileJsonType = Tuple[Optional[str], BinaryIO, Optional[str]]


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

payload: Union[BinaryIO, TextIO]
payload: BinaryIO
file_name: Optional[str] = None
mime_type: Optional[str] = None

Expand Down
1 change: 1 addition & 0 deletions integration-tests-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
project_name_override: integration-tests
23 changes: 23 additions & 0 deletions integration-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
__pycache__/
build/
dist/
*.egg-info/
.pytest_cache/

# pyenv
.python-version

# Environments
.env
.venv

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# JetBrains
.idea/

/coverage.xml
/.coverage
87 changes: 87 additions & 0 deletions integration-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# open-api-test-server-client
A client library for accessing OpenAPI Test Server

## Usage
First, create a client:

```python
from integration_tests import Client

client = Client(base_url="https://api.example.com")
```

If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:

```python
from integration_tests import AuthenticatedClient

client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
```

Now call your endpoint and use your models:

```python
from integration_tests.models import MyDataModel
from integration_tests.api.my_tag import get_my_data_model
from integration_tests.types import Response

my_data: MyDataModel = get_my_data_model.sync(client=client)
# or if you need more info (e.g. status_code)
response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client)
```

Or do the same thing with an async version:

```python
from integration_tests.models import MyDataModel
from integration_tests.api.my_tag import get_my_data_model
from integration_tests.types import Response

my_data: MyDataModel = await get_my_data_model.asyncio(client=client)
response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client)
```

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.

```python
client = AuthenticatedClient(
base_url="https://internal_api.example.com",
token="SuperSecretToken",
verify_ssl="/path/to/certificate_bundle.pem",
)
```

You can also disable certificate validation altogether, but beware that **this is a security risk**.

```python
client = AuthenticatedClient(
base_url="https://internal_api.example.com",
token="SuperSecretToken",
verify_ssl=False
)
```

Things to know:
1. Every path/method combo becomes a Python module with four functions:
1. `sync`: Blocking request that returns parsed data (if successful) or `None`
1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful.
1. `asyncio`: Like `sync` but the async instead of blocking
1. `asyncio_detailed`: Like `sync_detailed` by async instead of blocking

1. All path/query params, and bodies become method arguments.
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)
1. Any endpoint which did not have a tag will be in `open_api_test_server_client.api.default`

## Building / publishing this Client
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
1. Update the metadata in pyproject.toml (e.g. authors, version)
1. If you're using a private repository, configure it with Poetry
1. `poetry config repositories.<your-repository-name> <url-to-your-repository>`
1. `poetry config http-basic.<your-repository-name> <username> <password>`
1. Publish the client with `poetry publish --build -r <your-repository-name>` or, if for public PyPI, just `poetry publish --build`

If you want to install this client into another project without publishing it (e.g. for development) then:
1. If that project **is using Poetry**, you can simply do `poetry add <path-to-this-client>` from that project
1. If that project is not using Poetry:
1. Build a wheel with `poetry build -f wheel`
1. Install that wheel from the other project `pip install <path-to-wheel>`
2 changes: 2 additions & 0 deletions integration-tests/integration_tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
""" A client library for accessing OpenAPI Test Server """
from .client import AuthenticatedClient, Client
1 change: 1 addition & 0 deletions integration-tests/integration_tests/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
""" Contains methods for accessing the API """
Empty file.
Loading