Skip to content
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .github/workflows/samples-python-petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
pull_request:
paths:
- samples/openapi3/client/petstore/python-aiohttp/**
- samples/openapi3/client/petstore/python-httpx2/**
- samples/openapi3/client/petstore/python-httpx2-sync/**
- samples/openapi3/client/petstore/python-httpx/**
- samples/openapi3/client/petstore/python-httpx-sync/**
- samples/openapi3/client/petstore/python/**
Expand Down Expand Up @@ -37,6 +39,8 @@ jobs:
- "3.14"
sample:
- samples/openapi3/client/petstore/python-aiohttp
- samples/openapi3/client/petstore/python-httpx2
- samples/openapi3/client/petstore/python-httpx2-sync
- samples/openapi3/client/petstore/python-httpx
- samples/openapi3/client/petstore/python-httpx-sync
- samples/openapi3/client/petstore/python
Expand Down
17 changes: 17 additions & 0 deletions bin/configs/python-httpx2-sync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: python
outputDir: samples/openapi3/client/petstore/python-httpx2-sync
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: httpx2
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
16 changes: 16 additions & 0 deletions bin/configs/python-httpx2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
generatorName: python
outputDir: samples/openapi3/client/petstore/python-httpx2
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: httpx2
additionalProperties:
buildSystem: hatchling
packageName: petstore_api
mapNumberTo: float
poetry1: false
nameMappings:
_type: underscore_type
type_: type_with_underscore
modelNameMappings:
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
ApiResponse: ModelApiResponse
4 changes: 2 additions & 2 deletions docs/generators/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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, urllib3, httpx| |urllib3|
|library|library template (sub-template) to use: asyncio, urllib3, httpx, httpx2| |urllib3|
|mapNumberTo|Map number to Union[StrictFloat, StrictInt], StrictFloat, float or Decimal.| |Union[StrictFloat, StrictInt]|
|packageName|python package name (convention: snake_case).| |openapi_client|
|packageUrl|python package URL.| |null|
Expand All @@ -36,7 +36,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 simply calls the corresponding async method and waits for its completion, so both synchronous and asynchronous methods are available from the same API class.| |false|
|supportHttpxSync|Generate synchronous '_sync' variants of each API method (httpx and httpx2 libraries only). Each '_sync' method simply calls the corresponding async method and waits for its completion, so both synchronous and asynchronous methods are available from the same API class.| |false|
|useIndependentImplicitClients|Give API instances without an explicit or registered default ApiClient an owned client with a copied Configuration.| |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|

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public PythonClientCodegen() {
cliOptions.add(new CliOption(POETRY1_FALLBACK, "Fallback to formatting pyproject.toml to Poetry 1.x format."));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When an OAS 3.1 license provides an SPDX identifier, this selection makes HTTPX2 emit the shared template's license = { text = ... } instead of PEP 639 license-expression metadata. Preserve HTTPX2's license-expression rendering in the shared template or keep the HTTPX2-specific template.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java, line 419:

<comment>When an OAS 3.1 license provides an SPDX identifier, this selection makes HTTPX2 emit the shared template's `license = { text = ... }` instead of PEP 639 license-expression metadata. Preserve HTTPX2's license-expression rendering in the shared template or keep the HTTPX2-specific template.</comment>

<file context>
@@ -416,7 +416,7 @@ && convertPropertyToBooleanAndWriteBack(POETRY1_FALLBACK)) {
             supportingFiles.add(new SupportingFile("gitlab-ci.mustache", "", ".gitlab-ci.yml"));
             supportingFiles.add(new SupportingFile("setup.mustache", "", "setup.py"));
-            supportingFiles.add(new SupportingFile("httpx2".equals(getLibrary()) ? "httpx2/pyproject.mustache" : "pyproject.mustache", "", "pyproject.toml"));
+            supportingFiles.add(new SupportingFile("pyproject.mustache", "", "pyproject.toml"));
             supportingFiles.add(new SupportingFile("py.typed.mustache", packagePath(), "py.typed"));
         }
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When an HTTPX2 user supplies metadata containing quotes or control characters, this line selects a template that inserts licenseInfo without TOML escaping. Preserve the HTTPX2 metadata escaping or escape these values before rendering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java, line 419:

<comment>When an HTTPX2 user supplies metadata containing quotes or control characters, this line selects a template that inserts `licenseInfo` without TOML escaping. Preserve the HTTPX2 metadata escaping or escape these values before rendering.</comment>

<file context>
@@ -416,7 +416,7 @@ && convertPropertyToBooleanAndWriteBack(POETRY1_FALLBACK)) {
             supportingFiles.add(new SupportingFile("gitlab-ci.mustache", "", ".gitlab-ci.yml"));
             supportingFiles.add(new SupportingFile("setup.mustache", "", "setup.py"));
-            supportingFiles.add(new SupportingFile("httpx2".equals(getLibrary()) ? "httpx2/pyproject.mustache" : "pyproject.mustache", "", "pyproject.toml"));
+            supportingFiles.add(new SupportingFile("pyproject.mustache", "", "pyproject.toml"));
             supportingFiles.add(new SupportingFile("py.typed.mustache", packagePath(), "py.typed"));
         }
</file context>

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). " +
cliOptions.add(CliOption.newBoolean(SUPPORT_HTTPX_SYNC, "Generate synchronous '_sync' variants of each API method (httpx and httpx2 libraries only). " +
"Each '_sync' method simply calls the corresponding async method and waits for its completion, " +
"so both synchronous and asynchronous methods are available from the same API class.").defaultValue(Boolean.FALSE.toString()));
cliOptions.add(CliOption.newBoolean(USE_INDEPENDENT_IMPLICIT_CLIENTS,
Expand All @@ -247,7 +247,8 @@ public PythonClientCodegen() {
supportedLibraries.put("urllib3", "urllib3-based client");
supportedLibraries.put("asyncio", "asyncio-based client");
supportedLibraries.put("httpx", "httpx-based client");
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use: asyncio, urllib3, httpx");
supportedLibraries.put("httpx2", "httpx2-based client");
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use: asyncio, urllib3, httpx, httpx2");
libraryOption.setDefault(DEFAULT_LIBRARY);
cliOptions.add(libraryOption);
setLibrary(DEFAULT_LIBRARY);
Expand All @@ -271,6 +272,9 @@ public void processOpts() {
this.setLegacyDiscriminatorBehavior(false);

super.processOpts();
if (additionalProperties.containsKey(POETRY1_FALLBACK)) {
convertPropertyToBooleanAndWriteBack(POETRY1_FALLBACK);
Comment thread
Bernardoow marked this conversation as resolved.
}

// map to Dot instead of Period
specialCharReplacements.put(".", "Dot");
Expand Down Expand Up @@ -450,10 +454,12 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("asyncio/rest.mustache", packagePath(), "rest.py"));
additionalProperties.put("async", "true");
additionalProperties.put("asyncio", "true");
} else if ("httpx".equals(getLibrary())) {
} else if (isHttpxLibrary()) {
supportingFiles.add(new SupportingFile("httpx/rest.mustache", packagePath(), "rest.py"));
additionalProperties.put("async", "true");
additionalProperties.put("httpx", "true");
additionalProperties.put(getLibrary(), "true");
additionalProperties.put("httpxLibrary", true);
additionalProperties.put("httpxModule", getLibrary());
if (Boolean.parseBoolean(String.valueOf(additionalProperties.get(SUPPORT_HTTPX_SYNC)))) {
// generate synchronous '_sync' method variants alongside the async ones
additionalProperties.put(SUPPORT_HTTPX_SYNC, true);
Expand All @@ -465,10 +471,10 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("rest.mustache", packagePath(), "rest.py"));
}

// 'supportHttpxSync' only makes sense for the (async) httpx library
if (!"httpx".equals(getLibrary())) {
// 'supportHttpxSync' only makes sense for the (async) httpx and httpx2 libraries
if (!isHttpxLibrary()) {
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);
LOGGER.warn("'{}' is only supported with the 'httpx' and 'httpx2' libraries and will be ignored.", SUPPORT_HTTPX_SYNC);
}
additionalProperties.remove(SUPPORT_HTTPX_SYNC);
}
Expand Down Expand Up @@ -635,7 +641,7 @@ private void renameIndependentClientOperationMembers(

private Set<String> independentClientApiMembers() {
Set<String> members = new HashSet<>(INDEPENDENT_API_MEMBER_NAMES);
boolean async = "asyncio".equals(getLibrary()) || "httpx".equals(getLibrary());
boolean async = "asyncio".equals(getLibrary()) || isHttpxLibrary();
members.addAll(async
? ASYNC_API_LIFECYCLE_METHODS
: SYNC_API_LIFECYCLE_METHODS);
Expand Down Expand Up @@ -665,8 +671,12 @@ private boolean usesLegacyApiCompatibility() {
return compatibleWithPythonLegacy && DEFAULT_LIBRARY.equals(getLibrary());
}

private boolean isHttpxLibrary() {
return "httpx".equals(getLibrary()) || "httpx2".equals(getLibrary());
}

private boolean supportsHttpxSync() {
return "httpx".equals(getLibrary())
return isHttpxLibrary()
&& Boolean.parseBoolean(String.valueOf(
additionalProperties.get(SUPPORT_HTTPX_SYNC)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}})

Python {{{generatorLanguageVersion}}}

{{#poetry1}}
Packaging retains the deprecated `[tool.poetry.dev-dependencies]` table for
compatibility with legacy Poetry 1 releases. Generate with `poetry1=false` to use
standard project metadata and PEP 735 development dependency groups.
{{/poetry1}}
{{^poetry1}}
Packaging uses standard `[project]` metadata and `[dependency-groups]` (PEP 621/735).
Install the package with `python -m pip install .`; install development dependencies
with Poetry 2.2 or newer using `poetry install`, or pip 25.1 or newer using
`python -m pip install --group dev`. The latter installs the group, not the package.
{{/poetry1}}

{{#httpx2}}
The HTTP transport is HTTPX2 (`httpx2 >= 2.13.0, < 3`). API methods are asynchronous.

{{#supportHttpxSync}}
Synchronous `_sync` variants are also available through `supportHttpxSync=true`.
{{/supportHttpxSync}}
{{/httpx2}}
## Installation & Usage
### pip install

Expand Down Expand Up @@ -58,4 +77,3 @@ Execute `pytest` to run the tests.
Please follow the [installation procedure](#installation--usage) and then run the following:

{{> common_README }}

Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,9 @@ class Configuration:
may share it.
{{/useIndependentImplicitClients}}
{{/asyncio}}
{{#httpx}}
{{#httpxLibrary}}
:param retries: int - Retry configuration.
{{/httpx}}
{{/httpxLibrary}}
{{^async}}
:param retries: int | urllib3.util.retry.Retry - Retry configuration.
{{/async}}
Expand Down Expand Up @@ -349,9 +349,9 @@ conf = {{{packageName}}}.Configuration(
tcp_connector_limit_per_host: Optional[int] = None,
client_session_kwargs: Optional[Dict[str, Any]] = None,
{{/asyncio}}
{{#httpx}}
{{#httpxLibrary}}
retries: Optional[int] = None,
{{/httpx}}
{{/httpxLibrary}}
{{^async}}
retries: Optional[Union[int, urllib3.util.retry.Retry]] = None,
{{/async}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import re
import ssl
from typing import Optional, Union

import httpx
import {{httpxModule}}

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

RESTResponseType = httpx.Response
RESTResponseType = {{httpxModule}}.Response

class RESTResponse(io.IOBase):

Expand Down Expand Up @@ -65,7 +65,7 @@ class RESTClientObject:
self.proxy = configuration.proxy
self.proxy_headers = configuration.proxy_headers

self.pool_manager: Optional[httpx.AsyncClient] = None
self.pool_manager: Optional[{{httpxModule}}.AsyncClient] = None

async def close(self):
if self.pool_manager is not None:
Expand Down Expand Up @@ -134,7 +134,7 @@ class RESTClientObject:
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
# Content-Type which generated by {{httpxModule}}
del headers['Content-Type']

files = []
Expand Down Expand Up @@ -174,17 +174,17 @@ class RESTClientObject:
r = await self.pool_manager.request(**args)
return RESTResponse(r)

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

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

return httpx.AsyncClient(
return {{httpxModule}}.AsyncClient(
limits=limits,
proxy=proxy,
verify=self.ssl_context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def _get_sync_loop() -> asyncio.AbstractEventLoop:
"""Return a dedicated event loop running in a background daemon thread.

A single, long-lived loop is reused for every synchronous call so that the
underlying ``httpx.AsyncClient`` (and its connection pool) stays bound to one
underlying ``{{httpxModule}}.AsyncClient`` (and its connection pool) stays bound to one
event loop across calls. The loop is created lazily and in a thread-safe way.
"""
global _loop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ aiohttp-retry = ">= 2.8.3"
{{#httpx}}
httpx = ">= 0.28.1"
{{/httpx}}
{{#httpx2}}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
httpx2 = ">= 2.13.0, < 3"
{{/httpx2}}
{{#hasHttpSignatureMethods}}
pem = ">= 19.3.0"
pycryptodome = ">= 3.9.0"
Expand All @@ -62,6 +65,9 @@ dependencies = [
{{#httpx}}
"httpx (>=0.28.1)",
{{/httpx}}
{{#httpx2}}
"httpx2 (>=2.13.0,<3)",
{{/httpx2}}
{{#asyncio}}
"aiohttp (>=3.14.1)",
"aiohttp-retry (>=2.8.3)",
Expand All @@ -78,23 +84,27 @@ dependencies = [
Repository = "https://{{{gitHost}}}/{{{gitUserId}}}/{{{gitRepoId}}}"

{{/poetry1}}
{{^poetry1}}
[tool.poetry]
requires-poetry = ">=2.0"
{{/poetry1}}

{{#poetry1}}
# Deprecated by Poetry; retained for compatibility with legacy Poetry 1 releases.
[tool.poetry.dev-dependencies]
{{/poetry1}}
{{^poetry1}}
[tool.poetry.group.dev.dependencies]
{{/poetry1}}
pytest = ">= 9.0.3"
pytest-cov = ">= 2.8.1"
tox = ">= 3.9.0"
flake8 = ">= 4.0.0"
types-python-dateutil = ">= 2.8.19.14"
mypy = ">= 1.5"
{{/poetry1}}
{{^poetry1}}
[dependency-groups]
dev = [
"pytest>=9.0.3",
"pytest-cov>=2.8.1",
"tox>=3.9.0",
"flake8>=4.0.0",
"types-python-dateutil>=2.8.19.14",
"mypy>=1.5",
]
{{/poetry1}}


[build-system]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ aiohttp-retry >= 2.8.3
{{#httpx}}
httpx >= 0.28.1
{{/httpx}}
{{#httpx2}}
httpx2 >= 2.13.0, < 3
{{/httpx2}}
{{#hasHttpSignatureMethods}}
pem >= 19.3.0
pycryptodome >= 3.9.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ REQUIRES = [
{{#httpx}}
"httpx >= 0.28.1",
{{/httpx}}
{{#httpx2}}
"httpx2 >= 2.13.0, < 3",
{{/httpx2}}
{{#hasHttpSignatureMethods}}
"pem >= 19.3.0",
"pycryptodome >= 3.9.0",
Expand Down
Loading
Loading