Skip to content

Commit 3863a0f

Browse files
author
soapun
committed
[python] fix #19255 add async httpx support
1 parent 2f69ad9 commit 3863a0f

393 files changed

Lines changed: 45729 additions & 3 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/configs/python-httpx.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
generatorName: python
2+
outputDir: samples/openapi3/client/petstore/python-httpx
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/python/petstore-with-fake-endpoints-models-for-testing.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/python
5+
library: httpx
6+
additionalProperties:
7+
packageName: petstore_api
8+
mapNumberTo: float
9+
poetry1: true
10+
nameMappings:
11+
_type: underscore_type
12+
type_: type_with_underscore
13+
modelNameMappings:
14+
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
15+
ApiResponse: ModelApiResponse

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ public PythonClientCodegen() {
157157
supportedLibraries.put("urllib3", "urllib3-based client");
158158
supportedLibraries.put("asyncio", "asyncio-based client");
159159
supportedLibraries.put("tornado", "tornado-based client (deprecated)");
160-
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use: asyncio, tornado (deprecated), urllib3");
160+
supportedLibraries.put("httpx", "httpx-based client");
161+
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use: asyncio, tornado (deprecated), urllib3, httpx");
161162
libraryOption.setDefault(DEFAULT_LIBRARY);
162163
cliOptions.add(libraryOption);
163164
setLibrary(DEFAULT_LIBRARY);
@@ -334,6 +335,10 @@ public void processOpts() {
334335
} else if ("tornado".equals(getLibrary())) {
335336
supportingFiles.add(new SupportingFile("tornado/rest.mustache", packagePath(), "rest.py"));
336337
additionalProperties.put("tornado", "true");
338+
} else if ("httpx".equals(getLibrary())) {
339+
supportingFiles.add(new SupportingFile("httpx/rest.mustache", packagePath(), "rest.py"));
340+
additionalProperties.put("asyncio", "true");
341+
additionalProperties.put("httpx", "true");
337342
} else {
338343
supportingFiles.add(new SupportingFile("rest.mustache", packagePath(), "rest.py"));
339344
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# coding: utf-8
2+
3+
{{>partial_header}}
4+
5+
6+
import io
7+
import json
8+
import re
9+
import ssl
10+
from typing import Optional, Union
11+
12+
import httpx
13+
14+
from {{packageName}}.exceptions import ApiException, ApiValueError
15+
16+
RESTResponseType = httpx.Response
17+
18+
class RESTResponse(io.IOBase):
19+
20+
def __init__(self, resp) -> None:
21+
self.response = resp
22+
self.status = resp.status_code
23+
self.reason = resp.reason_phrase
24+
self.data = None
25+
26+
async def read(self):
27+
if self.data is None:
28+
self.data = await self.response.aread()
29+
return self.data
30+
31+
def getheaders(self):
32+
"""Returns a CIMultiDictProxy of the response headers."""
33+
return self.response.headers
34+
35+
def getheader(self, name, default=None):
36+
"""Returns a given response header."""
37+
return self.response.headers.get(name, default)
38+
39+
40+
class RESTClientObject:
41+
42+
def __init__(self, configuration) -> None:
43+
44+
# maxsize is number of requests to host that are allowed in parallel
45+
self.maxsize = configuration.connection_pool_maxsize
46+
47+
self.ssl_context = ssl.create_default_context(
48+
cafile=configuration.ssl_ca_cert,
49+
cadata=configuration.ca_cert_data,
50+
)
51+
if configuration.cert_file:
52+
self.ssl_context.load_cert_chain(
53+
configuration.cert_file, keyfile=configuration.key_file
54+
)
55+
56+
if not configuration.verify_ssl:
57+
self.ssl_context.check_hostname = False
58+
self.ssl_context.verify_mode = ssl.CERT_NONE
59+
60+
self.proxy = configuration.proxy
61+
self.proxy_headers = configuration.proxy_headers
62+
63+
self.pool_manager: Optional[httpx.AsyncClient] = None
64+
65+
async def close(self):
66+
await self.pool_manager.aclose()
67+
68+
async def request(
69+
self,
70+
method,
71+
url,
72+
headers=None,
73+
body=None,
74+
post_params=None,
75+
_request_timeout=None):
76+
"""Execute request
77+
78+
:param method: http request method
79+
:param url: http request url
80+
:param headers: http request headers
81+
:param body: request json body, for `application/json`
82+
:param post_params: request post parameters,
83+
`application/x-www-form-urlencoded`
84+
and `multipart/form-data`
85+
:param _request_timeout: timeout setting for this request. If one
86+
number provided, it will be total request
87+
timeout. It can also be a pair (tuple) of
88+
(connection, read) timeouts.
89+
"""
90+
method = method.upper()
91+
assert method in [
92+
'GET',
93+
'HEAD',
94+
'DELETE',
95+
'POST',
96+
'PUT',
97+
'PATCH',
98+
'OPTIONS'
99+
]
100+
101+
if post_params and body:
102+
raise ApiValueError(
103+
"body parameter cannot be used with post_params parameter."
104+
)
105+
106+
post_params = post_params or {}
107+
headers = headers or {}
108+
timeout = _request_timeout or 5 * 60
109+
110+
if 'Content-Type' not in headers:
111+
headers['Content-Type'] = 'application/json'
112+
113+
args = {
114+
"method": method,
115+
"url": url,
116+
"timeout": timeout,
117+
"headers": headers
118+
}
119+
120+
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
121+
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
122+
if re.search('json', headers['Content-Type'], re.IGNORECASE):
123+
if body is not None:
124+
args["json"] = body
125+
elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
126+
args["data"] = dict(post_params)
127+
elif headers['Content-Type'] == 'multipart/form-data':
128+
# must del headers['Content-Type'], or the correct
129+
# Content-Type which generated by httpx
130+
del headers['Content-Type']
131+
132+
files = []
133+
data = {}
134+
for param in post_params:
135+
k, v = param
136+
if isinstance(v, tuple) and len(v) == 3:
137+
files.append((k, v))
138+
else:
139+
# Ensures that dict objects are serialized
140+
if isinstance(v, dict):
141+
v = json.dumps(v)
142+
elif isinstance(v, int):
143+
v = str(v)
144+
data[k] = v
145+
146+
if files:
147+
args["files"] = files
148+
if data:
149+
args["data"] = data
150+
151+
# Pass a `bytes` parameter directly in the body to support
152+
# other content types than Json when `body` argument is provided
153+
# in serialized form
154+
elif isinstance(body, str) or isinstance(body, bytes):
155+
args["data"] = body
156+
else:
157+
# Cannot generate the request from given parameters
158+
msg = """Cannot prepare a request message for provided
159+
arguments. Please check that your arguments match
160+
declared content type."""
161+
raise ApiException(status=0, reason=msg)
162+
163+
if self.pool_manager is None:
164+
self.pool_manager = self._create_pool_manager()
165+
166+
r = await self.pool_manager.request(**args)
167+
return RESTResponse(r)
168+
169+
def _create_pool_manager(self) -> httpx.AsyncClient:
170+
limits = httpx.Limits(max_connections=self.maxsize)
171+
172+
proxy = None
173+
if self.proxy:
174+
proxy = httpx.Proxy(
175+
url=self.proxy,
176+
headers=self.proxy_headers
177+
)
178+
179+
return httpx.AsyncClient(
180+
limits=limits,
181+
proxy=proxy,
182+
verify=self.ssl_context,
183+
trust_env=True
184+
)

modules/openapi-generator/src/main/resources/python/pyproject.mustache

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ python = "^3.9"
3636
urllib3 = ">= 2.1.0, < 3.0.0"
3737
python-dateutil = ">= 2.8.2"
3838
{{#asyncio}}
39+
{{^httpx}}
3940
aiohttp = ">= 3.8.4"
4041
aiohttp-retry = ">= 2.8.3"
42+
{{/httpx}}
43+
{{#httpx}}
44+
httpx = ">= 0.28.1"
45+
{{/httpx}}
4146
{{/asyncio}}
4247
{{#tornado}}
4348
tornado = ">=4.2, <5"

modules/openapi-generator/src/main/resources/python/requirements.mustache

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
urllib3 >= 2.1.0, < 3.0.0
22
python_dateutil >= 2.8.2
33
{{#asyncio}}
4-
aiohttp >= 3.8.4
5-
aiohttp-retry >= 2.8.3
4+
{{^httpx}}
5+
aiohttp = ">= 3.8.4"
6+
aiohttp-retry = ">= 2.8.3"
7+
{{/httpx}}
8+
{{#httpx}}
9+
httpx = ">= 0.28.1"
10+
{{/httpx}}
611
{{/asyncio}}
712
{{#tornado}}
813
tornado = ">= 4.2, < 5"

modules/openapi-generator/src/main/resources/python/setup.mustache

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ REQUIRES = [
1818
"urllib3 >= 2.1.0, < 3.0.0",
1919
"python-dateutil >= 2.8.2",
2020
{{#asyncio}}
21+
{{^httpx}}
2122
"aiohttp >= 3.8.4",
2223
"aiohttp-retry >= 2.8.3",
24+
{{/httpx}}
25+
{{#httpx}}
26+
"httpx >= 0.28.1",
27+
{{/httpx}}
2328
{{/asyncio}}
2429
{{#tornado}}
2530
"tornado>=4.2, < 5",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# NOTE: This file is auto generated by OpenAPI Generator.
2+
# URL: https://openapi-generator.tech
3+
#
4+
# ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
5+
6+
name: petstore_api Python package
7+
8+
on: [push, pull_request]
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build:
15+
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v4
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
- name: Install dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
pip install -r requirements.txt
31+
pip install -r test-requirements.txt
32+
- name: Test with pytest
33+
run: |
34+
pytest --cov=petstore_api
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
env/
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.coverage.*
42+
.cache
43+
nosetests.xml
44+
coverage.xml
45+
*,cover
46+
.hypothesis/
47+
venv/
48+
.venv/
49+
.python-version
50+
.pytest_cache
51+
52+
# Translations
53+
*.mo
54+
*.pot
55+
56+
# Django stuff:
57+
*.log
58+
59+
# Sphinx documentation
60+
docs/_build/
61+
62+
# PyBuilder
63+
target/
64+
65+
# Ipython Notebook
66+
.ipynb_checkpoints
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# NOTE: This file is auto generated by OpenAPI Generator.
2+
# URL: https://openapi-generator.tech
3+
#
4+
# ref: https://docs.gitlab.com/ee/ci/README.html
5+
# ref: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
6+
7+
stages:
8+
- test
9+
10+
.pytest:
11+
stage: test
12+
script:
13+
- pip install -r requirements.txt
14+
- pip install -r test-requirements.txt
15+
- pytest --cov=petstore_api
16+
17+
pytest-3.9:
18+
extends: .pytest
19+
image: python:3.9-alpine
20+
pytest-3.10:
21+
extends: .pytest
22+
image: python:3.10-alpine
23+
pytest-3.11:
24+
extends: .pytest
25+
image: python:3.11-alpine
26+
pytest-3.12:
27+
extends: .pytest
28+
image: python:3.12-alpine
29+
pytest-3.13:
30+
extends: .pytest
31+
image: python:3.13-alpine

0 commit comments

Comments
 (0)