Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 15 additions & 0 deletions bin/configs/python-httpx.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
generatorName: python
outputDir: samples/openapi3/client/petstore/python-httpx
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inputSpec: modules/openapi-generator/src/test/resources/3_0/python/petstore-with-fake-endpoints-models-for-testing.yaml
templateDir: modules/openapi-generator/src/main/resources/python
library: httpx
additionalProperties:
packageName: petstore_api
mapNumberTo: float
poetry1: true
nameMappings:
_type: underscore_type
type_: type_with_underscore
modelNameMappings:
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
ApiResponse: ModelApiResponse
2 changes: 1 addition & 1 deletion docs/generators/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|generateSourceCodeOnly|Specifies that only a library source code is to be generated.| |false|
|hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |true|
|lazyImports|Enable lazy imports.| |false|
|library|library template (sub-template) to use: asyncio, tornado (deprecated), urllib3| |urllib3|
|library|library template (sub-template) to use: asyncio, tornado (deprecated), urllib3, httpx| |urllib3|
|mapNumberTo|Map number to Union[StrictFloat, StrictInt], StrictStr or float.| |Union[StrictFloat, StrictInt]|
|packageName|python package name (convention: snake_case).| |openapi_client|
|packageUrl|python package URL.| |null|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ public PythonClientCodegen() {
supportedLibraries.put("urllib3", "urllib3-based client");
supportedLibraries.put("asyncio", "asyncio-based client");
supportedLibraries.put("tornado", "tornado-based client (deprecated)");
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use: asyncio, tornado (deprecated), urllib3");
supportedLibraries.put("httpx", "httpx-based client");
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use: asyncio, tornado (deprecated), urllib3, httpx");
libraryOption.setDefault(DEFAULT_LIBRARY);
cliOptions.add(libraryOption);
setLibrary(DEFAULT_LIBRARY);
Expand Down Expand Up @@ -334,6 +335,10 @@ public void processOpts() {
} else if ("tornado".equals(getLibrary())) {
supportingFiles.add(new SupportingFile("tornado/rest.mustache", packagePath(), "rest.py"));
additionalProperties.put("tornado", "true");
} else if ("httpx".equals(getLibrary())) {
supportingFiles.add(new SupportingFile("httpx/rest.mustache", packagePath(), "rest.py"));
additionalProperties.put("asyncio", "true");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please explain why asyncio (library option) needs to be set to true even for httpx library?

Copy link
Copy Markdown
Contributor Author

@soapun soapun Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all request methods in clients higher than RESTClientObject to be async def

like here:

api_client.mustache

api.mustache

So "asyncio" is actually not only a library option

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my take is to refactor it if both asyncio and httpx need the same piece of code

for example, add the tag {{#async}} ....{{/async}} that is enabled by both asyncio and httpx libraries

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw current library name "asyncio" is a bit misleading

asyncio - part of python stdlib (coros, eventloop and so on), not a client library
aiohttp - 3rd party rest client library

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please PM me via Slack if you want to further discussion this

additionalProperties.put("httpx", "true");
} else {
supportingFiles.add(new SupportingFile("rest.mustache", packagePath(), "rest.py"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# coding: utf-8

{{>partial_header}}


import io
import json
import re
import ssl
from typing import Optional, Union

import httpx

from {{packageName}}.exceptions import ApiException, ApiValueError

RESTResponseType = httpx.Response

class RESTResponse(io.IOBase):

def __init__(self, resp) -> None:
self.response = resp
self.status = resp.status_code
self.reason = resp.reason_phrase
self.data = None

async def read(self):
if self.data is None:
self.data = await self.response.aread()
return self.data

def getheaders(self):
"""Returns a CIMultiDictProxy of the response headers."""
return self.response.headers

def getheader(self, name, default=None):
"""Returns a given response header."""
return self.response.headers.get(name, default)


class RESTClientObject:

def __init__(self, configuration) -> None:

# maxsize is number of requests to host that are allowed in parallel
self.maxsize = configuration.connection_pool_maxsize

self.ssl_context = ssl.create_default_context(
cafile=configuration.ssl_ca_cert,
cadata=configuration.ca_cert_data,
)
if configuration.cert_file:
self.ssl_context.load_cert_chain(
configuration.cert_file, keyfile=configuration.key_file
)

if not configuration.verify_ssl:
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE

self.proxy = configuration.proxy
self.proxy_headers = configuration.proxy_headers

self.pool_manager: Optional[httpx.AsyncClient] = None

async def close(self):
await self.pool_manager.aclose()

async def request(
self,
method,
url,
headers=None,
body=None,
post_params=None,
_request_timeout=None):
"""Execute request

:param method: http request method
:param url: http request url
:param headers: http request headers
:param body: request json body, for `application/json`
:param post_params: request post parameters,
`application/x-www-form-urlencoded`
and `multipart/form-data`
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
"""
method = method.upper()
assert method in [
'GET',
'HEAD',
'DELETE',
'POST',
'PUT',
'PATCH',
'OPTIONS'
]

if post_params and body:
raise ApiValueError(
"body parameter cannot be used with post_params parameter."
)

post_params = post_params or {}
headers = headers or {}
timeout = _request_timeout or 5 * 60

if 'Content-Type' not in headers:
headers['Content-Type'] = 'application/json'

args = {
"method": method,
"url": url,
"timeout": timeout,
"headers": headers
}

# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
if re.search('json', headers['Content-Type'], re.IGNORECASE):
if body is not None:
args["json"] = body
elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
args["data"] = dict(post_params)
elif headers['Content-Type'] == 'multipart/form-data':
# must del headers['Content-Type'], or the correct
# Content-Type which generated by httpx
del headers['Content-Type']

files = []
data = {}
for param in post_params:
k, v = param
if isinstance(v, tuple) and len(v) == 3:
files.append((k, v))
else:
# Ensures that dict objects are serialized
if isinstance(v, dict):
v = json.dumps(v)
elif isinstance(v, int):
v = str(v)
data[k] = v

if files:
args["files"] = files
if data:
args["data"] = data

# Pass a `bytes` parameter directly in the body to support
# other content types than Json when `body` argument is provided
# in serialized form
elif isinstance(body, str) or isinstance(body, bytes):
args["data"] = body
else:
# Cannot generate the request from given parameters
msg = """Cannot prepare a request message for provided
arguments. Please check that your arguments match
declared content type."""
raise ApiException(status=0, reason=msg)

if self.pool_manager is None:
self.pool_manager = self._create_pool_manager()

r = await self.pool_manager.request(**args)
return RESTResponse(r)

def _create_pool_manager(self) -> httpx.AsyncClient:
limits = httpx.Limits(max_connections=self.maxsize)

proxy = None
if self.proxy:
proxy = httpx.Proxy(
url=self.proxy,
headers=self.proxy_headers
)

return httpx.AsyncClient(
limits=limits,
proxy=proxy,
verify=self.ssl_context,
trust_env=True
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ python = "^3.9"
urllib3 = ">= 2.1.0, < 3.0.0"
python-dateutil = ">= 2.8.2"
{{#asyncio}}
{{^httpx}}
aiohttp = ">= 3.8.4"
aiohttp-retry = ">= 2.8.3"
{{/httpx}}
{{#httpx}}
httpx = ">= 0.28.1"
{{/httpx}}
{{/asyncio}}
{{#tornado}}
tornado = ">=4.2, <5"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
urllib3 >= 2.1.0, < 3.0.0
python_dateutil >= 2.8.2
{{#asyncio}}
aiohttp >= 3.8.4
aiohttp-retry >= 2.8.3
{{^httpx}}
aiohttp = ">= 3.8.4"
aiohttp-retry = ">= 2.8.3"
{{/httpx}}
{{#httpx}}
httpx = ">= 0.28.1"
{{/httpx}}
{{/asyncio}}
{{#tornado}}
tornado = ">= 4.2, < 5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ REQUIRES = [
"urllib3 >= 2.1.0, < 3.0.0",
"python-dateutil >= 2.8.2",
{{#asyncio}}
{{^httpx}}
"aiohttp >= 3.8.4",
"aiohttp-retry >= 2.8.3",
{{/httpx}}
{{#httpx}}
"httpx >= 0.28.1",
{{/httpx}}
{{/asyncio}}
{{#tornado}}
"tornado>=4.2, < 5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# NOTE: This file is auto generated by OpenAPI Generator.
# URL: https://openapi-generator.tech
#
# ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: petstore_api Python package

on: [push, pull_request]

permissions:
contents: read

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r test-requirements.txt
- name: Test with pytest
run: |
pytest --cov=petstore_api
66 changes: 66 additions & 0 deletions samples/openapi3/client/petstore/python-httpx/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
venv/
.venv/
.python-version
.pytest_cache

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Ipython Notebook
.ipynb_checkpoints
Loading
Loading