From e6eccfdf87773a70d29653f26aace90bf92793f3 Mon Sep 17 00:00:00 2001 From: "liqiankun.1111" Date: Fri, 22 May 2026 16:04:59 +0800 Subject: [PATCH 1/2] [python][asyncio] Expose ClientSession extension points (closes #23830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the python/httpx template's `_create_pool_manager()` factory and add a parallel `_create_connector()` so subclasses can customize the `aiohttp.ClientSession` / `aiohttp.TCPConnector` without overriding `request()`. Also surface three typed fields on `Configuration` (asyncio block): - `trace_configs: list[aiohttp.TraceConfig]` — forwarded to `ClientSession` for tracing / instrumentation (OpenTelemetry, etc.). This was the original issue request. - `tcp_connector_limit_per_host: int` — per-host concurrency cap forwarded to `aiohttp.TCPConnector(limit_per_host=...)`. - `client_session_kwargs: dict` — merged into `ClientSession(**kwargs)` for less common knobs (`json_serialize=orjson.dumps`, `cookie_jar=aiohttp.DummyCookieJar()`, `raise_for_status=...`). All new Configuration fields are read via `getattr(self.configuration, ..., None)` so older Configuration objects (e.g. produced by an older generator version and passed manually) remain compatible. The httpx template is left untouched; only the asyncio side is changed in this PR. No Java code change is needed — the `{{#asyncio}}` mustache block already gates the new code path. Closes #23830 --- .../resources/python/asyncio/rest.mustache | 45 ++++++++++++++++--- .../resources/python/configuration.mustache | 22 +++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/python/asyncio/rest.mustache b/modules/openapi-generator/src/main/resources/python/asyncio/rest.mustache index 6945dfd4fc95..952d3f99bb43 100644 --- a/modules/openapi-generator/src/main/resources/python/asyncio/rest.mustache +++ b/modules/openapi-generator/src/main/resources/python/asyncio/rest.mustache @@ -7,7 +7,7 @@ import io import json import re import ssl -from typing import Optional, Union +from typing import Any, Dict, Optional, Union import aiohttp import aiohttp_retry @@ -49,6 +49,10 @@ class RESTClientObject: def __init__(self, configuration) -> None: + # Keep a reference so factory methods (_create_pool_manager / _create_connector) + # and subclasses can read extension fields like trace_configs. + self.configuration = configuration + # maxsize is number of requests to host that are allowed in parallel self.maxsize = configuration.connection_pool_maxsize @@ -92,6 +96,40 @@ class RESTClientObject: if self.retry_client is not None: await self.retry_client.close() + def _create_connector(self) -> aiohttp.TCPConnector: + """Build the TCPConnector used by the ClientSession. + + Override in a subclass to customize DNS resolver, keepalive, etc. + """ + kwargs: Dict[str, Any] = { + "limit": self.maxsize, + "ssl": self.ssl_context, + } + limit_per_host = getattr(self.configuration, "tcp_connector_limit_per_host", None) + if limit_per_host is not None: + kwargs["limit_per_host"] = limit_per_host + return aiohttp.TCPConnector(**kwargs) + + def _create_pool_manager(self) -> aiohttp.ClientSession: + """Build the aiohttp.ClientSession used as the connection pool. + + Override in a subclass to fully customize the session (e.g. attach + aiohttp.TraceConfig, swap json_serialize, etc.). Typed Configuration + fields (trace_configs / client_session_kwargs) are read via getattr + so older Configuration objects remain compatible. + """ + kwargs: Dict[str, Any] = { + "connector": self._create_connector(), + "trust_env": True, + } + trace_configs = getattr(self.configuration, "trace_configs", None) + if trace_configs is not None: + kwargs["trace_configs"] = trace_configs + extra = getattr(self.configuration, "client_session_kwargs", None) + if extra: + kwargs.update(extra) + return aiohttp.ClientSession(**kwargs) + async def request( self, method, @@ -200,10 +238,7 @@ class RESTClientObject: # https pool manager if self.pool_manager is None: - self.pool_manager = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(limit=self.maxsize, ssl=self.ssl_context), - trust_env=True, - ) + self.pool_manager = self._create_pool_manager() pool_manager = self.pool_manager if self._effective_retry_options is not None and method in ALLOW_RETRY_METHODS: diff --git a/modules/openapi-generator/src/main/resources/python/configuration.mustache b/modules/openapi-generator/src/main/resources/python/configuration.mustache index 4027fe59b15c..a2449cb106c8 100644 --- a/modules/openapi-generator/src/main/resources/python/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/python/configuration.mustache @@ -1,6 +1,7 @@ {{>partial_header}} {{#asyncio}} +import aiohttp import aiohttp_retry {{/asyncio}} {{#async}} @@ -192,6 +193,13 @@ class Configuration: in PEM format. {{#asyncio}} :param retries: int | aiohttp_retry.RetryOptionsBase - Retry configuration. + :param trace_configs: list of aiohttp.TraceConfig instances forwarded to + aiohttp.ClientSession for tracing/instrumentation (e.g. OpenTelemetry). + :param tcp_connector_limit_per_host: Per-host concurrency cap forwarded to + aiohttp.TCPConnector(limit_per_host=...). None leaves aiohttp's default (0 = unlimited). + :param client_session_kwargs: Extra keyword arguments merged into + aiohttp.ClientSession(**kwargs) (e.g. json_serialize=orjson.dumps, + cookie_jar=aiohttp.DummyCookieJar()). {{/asyncio}} {{#httpx}} :param retries: int - Retry configuration. @@ -320,6 +328,9 @@ conf = {{{packageName}}}.Configuration( ssl_ca_cert: Optional[str]=None, {{#asyncio}} retries: Optional[Union[int, aiohttp_retry.RetryOptionsBase]] = None, + trace_configs: Optional[List[aiohttp.TraceConfig]] = None, + tcp_connector_limit_per_host: Optional[int] = None, + client_session_kwargs: Optional[Dict[str, Any]] = None, {{/asyncio}} {{#httpx}} retries: Optional[int] = None, @@ -470,6 +481,17 @@ conf = {{{packageName}}}.Configuration( self.retries = retries """Retry configuration """ +{{#asyncio}} + self.trace_configs = trace_configs + """aiohttp.TraceConfig list forwarded to ClientSession for tracing. + """ + self.tcp_connector_limit_per_host = tcp_connector_limit_per_host + """Per-host concurrency cap forwarded to TCPConnector. + """ + self.client_session_kwargs = client_session_kwargs + """Extra kwargs merged into aiohttp.ClientSession(**kwargs). + """ +{{/asyncio}} # Enable client side validation self.client_side_validation = client_side_validation From 2b0dbf41fd5a5831e990268cef6728431260ebac Mon Sep 17 00:00:00 2001 From: "liqiankun.1111" Date: Fri, 22 May 2026 18:37:35 +0800 Subject: [PATCH 2/2] update python-aiohttp samples --- .../petstore_api/configuration.py | 20 +++++++++ .../python-aiohttp/petstore_api/rest.py | 45 ++++++++++++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/configuration.py b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/configuration.py index e6a976c640e9..0ac1159a14a6 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/configuration.py +++ b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/configuration.py @@ -10,6 +10,7 @@ """ # noqa: E501 +import aiohttp import aiohttp_retry import base64 import copy @@ -169,6 +170,13 @@ class Configuration: :param ssl_ca_cert: str - the path to a file of concatenated CA certificates in PEM format. :param retries: int | aiohttp_retry.RetryOptionsBase - Retry configuration. + :param trace_configs: list of aiohttp.TraceConfig instances forwarded to + aiohttp.ClientSession for tracing/instrumentation (e.g. OpenTelemetry). + :param tcp_connector_limit_per_host: Per-host concurrency cap forwarded to + aiohttp.TCPConnector(limit_per_host=...). None leaves aiohttp's default (0 = unlimited). + :param client_session_kwargs: Extra keyword arguments merged into + aiohttp.ClientSession(**kwargs) (e.g. json_serialize=orjson.dumps, + cookie_jar=aiohttp.DummyCookieJar()). :param ca_cert_data: verify the peer using concatenated CA certificate data in PEM (str) or DER (bytes) format. :param cert_file: the path to a client certificate file, for mTLS. @@ -279,6 +287,9 @@ def __init__( ignore_operation_servers: bool=False, ssl_ca_cert: Optional[str]=None, retries: Optional[Union[int, aiohttp_retry.RetryOptionsBase]] = None, + trace_configs: Optional[List[aiohttp.TraceConfig]] = None, + tcp_connector_limit_per_host: Optional[int] = None, + client_session_kwargs: Optional[Dict[str, Any]] = None, ca_cert_data: Optional[Union[str, bytes]] = None, cert_file: Optional[str]=None, key_file: Optional[str]=None, @@ -409,6 +420,15 @@ def __init__( self.retries = retries """Retry configuration """ + self.trace_configs = trace_configs + """aiohttp.TraceConfig list forwarded to ClientSession for tracing. + """ + self.tcp_connector_limit_per_host = tcp_connector_limit_per_host + """Per-host concurrency cap forwarded to TCPConnector. + """ + self.client_session_kwargs = client_session_kwargs + """Extra kwargs merged into aiohttp.ClientSession(**kwargs). + """ # Enable client side validation self.client_side_validation = client_side_validation diff --git a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/rest.py b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/rest.py index 89481af0d71d..085d16acb62f 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/rest.py +++ b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/rest.py @@ -16,7 +16,7 @@ import json import re import ssl -from typing import Optional, Union +from typing import Any, Dict, Optional, Union import aiohttp import aiohttp_retry @@ -58,6 +58,10 @@ class RESTClientObject: def __init__(self, configuration) -> None: + # Keep a reference so factory methods (_create_pool_manager / _create_connector) + # and subclasses can read extension fields like trace_configs. + self.configuration = configuration + # maxsize is number of requests to host that are allowed in parallel self.maxsize = configuration.connection_pool_maxsize @@ -101,6 +105,40 @@ async def close(self) -> None: if self.retry_client is not None: await self.retry_client.close() + def _create_connector(self) -> aiohttp.TCPConnector: + """Build the TCPConnector used by the ClientSession. + + Override in a subclass to customize DNS resolver, keepalive, etc. + """ + kwargs: Dict[str, Any] = { + "limit": self.maxsize, + "ssl": self.ssl_context, + } + limit_per_host = getattr(self.configuration, "tcp_connector_limit_per_host", None) + if limit_per_host is not None: + kwargs["limit_per_host"] = limit_per_host + return aiohttp.TCPConnector(**kwargs) + + def _create_pool_manager(self) -> aiohttp.ClientSession: + """Build the aiohttp.ClientSession used as the connection pool. + + Override in a subclass to fully customize the session (e.g. attach + aiohttp.TraceConfig, swap json_serialize, etc.). Typed Configuration + fields (trace_configs / client_session_kwargs) are read via getattr + so older Configuration objects remain compatible. + """ + kwargs: Dict[str, Any] = { + "connector": self._create_connector(), + "trust_env": True, + } + trace_configs = getattr(self.configuration, "trace_configs", None) + if trace_configs is not None: + kwargs["trace_configs"] = trace_configs + extra = getattr(self.configuration, "client_session_kwargs", None) + if extra: + kwargs.update(extra) + return aiohttp.ClientSession(**kwargs) + async def request( self, method, @@ -209,10 +247,7 @@ async def request( # https pool manager if self.pool_manager is None: - self.pool_manager = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(limit=self.maxsize, ssl=self.ssl_context), - trust_env=True, - ) + self.pool_manager = self._create_pool_manager() pool_manager = self.pool_manager if self._effective_retry_options is not None and method in ALLOW_RETRY_METHODS: