-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
[python] add async httpx support #22021
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| generatorName: python | ||
| outputDir: samples/openapi3/client/petstore/python-httpx | ||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
|
@@ -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"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you please explain why
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: So "asyncio" is actually not only a library option
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i agree
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")); | ||
| } | ||
|
|
||
| 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 |
|---|---|---|
| @@ -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 |
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add the new folder to the CI workflow
https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/samples-python-petstore.yaml