Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .github/workflows/samples-python-petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
paths:
- samples/openapi3/client/petstore/python-aiohttp/**
- samples/openapi3/client/petstore/python-httpx/**
- samples/openapi3/client/petstore/python-httpx-sync/**
- samples/openapi3/client/petstore/python/**
- .github/workflows/samples-python-petstore.yaml

Expand Down Expand Up @@ -36,6 +37,7 @@ jobs:
sample:
- samples/openapi3/client/petstore/python-aiohttp
- samples/openapi3/client/petstore/python-httpx
- samples/openapi3/client/petstore/python-httpx-sync
- samples/openapi3/client/petstore/python
- samples/openapi3/client/petstore/python-lazyImports
services:
Expand Down
17 changes: 17 additions & 0 deletions bin/configs/python-httpx-sync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: python
outputDir: samples/openapi3/client/petstore/python-httpx-sync

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.

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 took into account your remarks and the one of @cubic 👍

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:
buildSystem: hatchling
packageName: petstore_api
mapNumberTo: float
poetry1: false
supportHttpxSync: true
nameMappings:
_type: underscore_type
type_: type_with_underscore
modelNameMappings:
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
ApiResponse: ModelApiResponse
1 change: 1 addition & 0 deletions docs/generators/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|projectName|python project name in setup.py (e.g. petstore-api).| |null|
|recursionLimit|Set the recursion limit. If not set, use the system default value.| |null|
|setEnsureAsciiToFalse|When set to true, add `ensure_ascii=False` in json.dumps when creating the HTTP request body.| |false|
|supportHttpxSync|Generate synchronous '_sync' variants of each API method (httpx library only). Each '_sync' method shares the same code as its async counterpart but performs the HTTP request synchronously (without coroutines), so both synchronous and asynchronous methods are available from the same API class.| |false|
|useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped.| |false|

## IMPORT MAPPING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class PythonClientCodegen extends AbstractPythonCodegen implements Codege
public static final String POETRY1_FALLBACK = "poetry1";
public static final String LAZY_IMPORTS = "lazyImports";
public static final String BUILD_SYSTEM = "buildSystem";
public static final String SUPPORT_HTTPX_SYNC = "supportHttpxSync";

@Setter protected String packageUrl;
protected String apiDocPath = "docs/";
Expand Down Expand Up @@ -158,6 +159,9 @@ public PythonClientCodegen() {
cliOptions.add(new CliOption(POETRY1_FALLBACK, "Fallback to formatting pyproject.toml to Poetry 1.x format."));
cliOptions.add(new CliOption(LAZY_IMPORTS, "Enable lazy imports.").defaultValue(Boolean.FALSE.toString()));
cliOptions.add(new CliOption(BUILD_SYSTEM, "Build system to use in pyproject.toml (setuptools, hatchling).").defaultValue("setuptools"));
cliOptions.add(CliOption.newBoolean(SUPPORT_HTTPX_SYNC, "Generate synchronous '_sync' variants of each API method (httpx library only). " +
"Each '_sync' method shares the same code as its async counterpart but performs the HTTP request synchronously (without coroutines), " +
"so both synchronous and asynchronous methods are available from the same API class.").defaultValue(Boolean.FALSE.toString()));

supportedLibraries.put("urllib3", "urllib3-based client");
supportedLibraries.put("asyncio", "asyncio-based client");
Expand Down Expand Up @@ -352,10 +356,24 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("httpx/rest.mustache", packagePath(), "rest.py"));
additionalProperties.put("async", "true");
additionalProperties.put("httpx", "true");
if (Boolean.parseBoolean(String.valueOf(additionalProperties.get(SUPPORT_HTTPX_SYNC)))) {
// also generate synchronous '_sync' method variants alongside the async ones
additionalProperties.put(SUPPORT_HTTPX_SYNC, true);
} else {
additionalProperties.remove(SUPPORT_HTTPX_SYNC);
}
} else {
supportingFiles.add(new SupportingFile("rest.mustache", packagePath(), "rest.py"));
}

// 'supportHttpxSync' only makes sense for the (async) httpx library
if (!"httpx".equals(getLibrary())) {
if (Boolean.parseBoolean(String.valueOf(additionalProperties.get(SUPPORT_HTTPX_SYNC)))) {
LOGGER.warn("'{}' is only supported with the 'httpx' library and will be ignored.", SUPPORT_HTTPX_SYNC);
}
additionalProperties.remove(SUPPORT_HTTPX_SYNC);
}

modelPackage = this.packageName + "." + modelPackage;
apiPackage = this.packageName + "." + apiPackage;
}
Expand Down
43 changes: 43 additions & 0 deletions modules/openapi-generator/src/main/resources/python/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,49 @@ class {{classname}}:
return response_data.response


{{#supportHttpxSync}}
@validate_call
def {{operationId}}_sync{{>partial_api_args}} -> {{{returnType}}}{{^returnType}}None{{/returnType}}:
{{>partial_api}}

response_data = self.api_client.call_api_sync(
*_param,
_request_timeout=_request_timeout
)
response_data.read_sync()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data


@validate_call
def {{operationId}}_sync_with_http_info{{>partial_api_args}} -> ApiResponse[{{{returnType}}}{{^returnType}}None{{/returnType}}]:
{{>partial_api}}

response_data = self.api_client.call_api_sync(
*_param,
_request_timeout=_request_timeout
)
response_data.read_sync()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)


@validate_call
def {{operationId}}_sync_without_preload_content{{>partial_api_args}} -> RESTResponseType:
{{>partial_api}}

response_data = self.api_client.call_api_sync(
*_param,
_request_timeout=_request_timeout
)
return response_data.response


{{/supportHttpxSync}}
def _{{operationId}}_serialize(
self,
{{#allParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ class ApiClient:
def __exit__(self, exc_type, exc_value, traceback):
pass
{{/async}}
{{#supportHttpxSync}}
def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
self.close_sync()

def close_sync(self):
self.rest_client.close_sync()
{{/supportHttpxSync}}

@property
def user_agent(self):
Expand Down Expand Up @@ -291,6 +301,36 @@ class ApiClient:

return response_data

{{#supportHttpxSync}}
def call_api_sync(
self,
method,
url,
header_params=None,
body=None,
post_params=None,
_request_timeout=None
) -> rest.RESTResponse:
"""Makes the synchronous HTTP request.

Synchronous counterpart of :meth:`call_api`.
"""

try:
# perform request and return response
response_data = self.rest_client.request_sync(
method, url,
headers=header_params,
body=body, post_params=post_params,
_request_timeout=_request_timeout
)

except ApiException as e:
raise e

return response_data

{{/supportHttpxSync}}
def response_deserialize(
self,
response_data: rest.RESTResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Method | HTTP request | Description
{{#operation}}
# **{{{operationId}}}**
> {{#returnType}}{{{.}}} {{/returnType}}{{{operationId}}}({{#allParams}}{{#required}}{{{paramName}}}{{/required}}{{^required}}{{{paramName}}}={{{paramName}}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}})
{{#supportHttpxSync}}

**Synchronous variant:** `{{{operationId}}}_sync(...)` — same parameters and return type, but blocks until completion instead of requiring `await`.
{{/supportHttpxSync}}

{{#summary}}
{{{summary}}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ class {{#operations}}Test{{classname}}(unittest.{{#async}}IsolatedAsyncio{{/asyn
"""
pass

{{#supportHttpxSync}}
def test_{{operationId}}_sync(self) -> None:
"""Test case for {{{operationId}}} (synchronous)

{{#summary}}
{{{.}}}
{{/summary}}
"""
pass

{{/supportHttpxSync}}
{{/operation}}
{{/operations}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ class RESTResponse(io.IOBase):
self.data = await self.response.aread()
return self.data

{{#supportHttpxSync}}
def read_sync(self):
if self.data is None:
self.data = self.response.read()
return self.data

{{/supportHttpxSync}}
@property
def headers(self):
"""Returns a CIMultiDictProxy of response headers."""
Expand Down Expand Up @@ -66,11 +73,20 @@ class RESTClientObject:
self.proxy_headers = configuration.proxy_headers

self.pool_manager: Optional[httpx.AsyncClient] = None
{{#supportHttpxSync}}
self.sync_pool_manager: Optional[httpx.Client] = None
{{/supportHttpxSync}}

async def close(self):
if self.pool_manager is not None:
await self.pool_manager.aclose()

{{#supportHttpxSync}}
def close_sync(self):
if self.sync_pool_manager is not None:
self.sync_pool_manager.close()

{{/supportHttpxSync}}
async def request(
self,
method,
Expand All @@ -93,6 +109,55 @@ class RESTClientObject:
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
"""
args = self._prepare_request_args(
method, url, headers, body, post_params, _request_timeout
)

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:
return self._build_pool_manager(httpx.AsyncClient)

{{#supportHttpxSync}}
def request_sync(
self,
method,
url,
headers=None,
body=None,
post_params=None,
_request_timeout=None):
"""Execute a synchronous request.

Synchronous counterpart of :meth:`request`; see it for the parameters.
"""
args = self._prepare_request_args(
method, url, headers, body, post_params, _request_timeout
)

if self.sync_pool_manager is None:
self.sync_pool_manager = self._create_sync_pool_manager()

r = self.sync_pool_manager.request(**args)
return RESTResponse(r)

def _create_sync_pool_manager(self) -> httpx.Client:
return self._build_pool_manager(httpx.Client)

{{/supportHttpxSync}}
def _prepare_request_args(
self,
method,
url,
headers=None,
body=None,
post_params=None,
_request_timeout=None):
"""Build the keyword arguments passed to the underlying httpx client."""
method = method.upper()
assert method in [
'GET',
Expand Down Expand Up @@ -168,13 +233,9 @@ class RESTClientObject:
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)
return args

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

proxy = None
Expand All @@ -184,7 +245,7 @@ class RESTClientObject:
headers=self.proxy_headers
)

return httpx.AsyncClient(
return client_cls(
limits=limits,
proxy=proxy,
verify=self.ssl_context,
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.10", "3.11", "3.12", "3.13", "3.14"]

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
Loading
Loading