python3-commons 0.18.4__tar.gz → 0.18.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {python3_commons-0.18.4/src/python3_commons.egg-info → python3_commons-0.18.6}/PKG-INFO +4 -1
  2. {python3_commons-0.18.4 → python3_commons-0.18.6}/pyproject.toml +3 -0
  3. python3_commons-0.18.6/src/python3_commons/soap_client.py +250 -0
  4. {python3_commons-0.18.4 → python3_commons-0.18.6/src/python3_commons.egg-info}/PKG-INFO +4 -1
  5. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons.egg-info/requires.txt +3 -0
  6. {python3_commons-0.18.4 → python3_commons-0.18.6}/uv.lock +8 -4
  7. python3_commons-0.18.4/src/python3_commons/soap_client.py +0 -299
  8. {python3_commons-0.18.4 → python3_commons-0.18.6}/.coveragerc +0 -0
  9. {python3_commons-0.18.4 → python3_commons-0.18.6}/.devcontainer/Dockerfile +0 -0
  10. {python3_commons-0.18.4 → python3_commons-0.18.6}/.devcontainer/devcontainer.json +0 -0
  11. {python3_commons-0.18.4 → python3_commons-0.18.6}/.devcontainer/docker-compose.yml +0 -0
  12. {python3_commons-0.18.4 → python3_commons-0.18.6}/.env_template +0 -0
  13. {python3_commons-0.18.4 → python3_commons-0.18.6}/.github/workflows/checks.yml +0 -0
  14. {python3_commons-0.18.4 → python3_commons-0.18.6}/.github/workflows/python-publish.yaml +0 -0
  15. {python3_commons-0.18.4 → python3_commons-0.18.6}/.github/workflows/release-on-tag-push.yml +0 -0
  16. {python3_commons-0.18.4 → python3_commons-0.18.6}/.gitignore +0 -0
  17. {python3_commons-0.18.4 → python3_commons-0.18.6}/.pre-commit-config.yaml +0 -0
  18. {python3_commons-0.18.4 → python3_commons-0.18.6}/.python-version +0 -0
  19. {python3_commons-0.18.4 → python3_commons-0.18.6}/AUTHORS.rst +0 -0
  20. {python3_commons-0.18.4 → python3_commons-0.18.6}/CHANGELOG.rst +0 -0
  21. {python3_commons-0.18.4 → python3_commons-0.18.6}/LICENSE +0 -0
  22. {python3_commons-0.18.4 → python3_commons-0.18.6}/README.md +0 -0
  23. {python3_commons-0.18.4 → python3_commons-0.18.6}/README.rst +0 -0
  24. {python3_commons-0.18.4 → python3_commons-0.18.6}/docs/Makefile +0 -0
  25. {python3_commons-0.18.4 → python3_commons-0.18.6}/docs/_static/.gitignore +0 -0
  26. {python3_commons-0.18.4 → python3_commons-0.18.6}/docs/authors.rst +0 -0
  27. {python3_commons-0.18.4 → python3_commons-0.18.6}/docs/changelog.rst +0 -0
  28. {python3_commons-0.18.4 → python3_commons-0.18.6}/docs/conf.py +0 -0
  29. {python3_commons-0.18.4 → python3_commons-0.18.6}/docs/index.rst +0 -0
  30. {python3_commons-0.18.4 → python3_commons-0.18.6}/docs/license.rst +0 -0
  31. {python3_commons-0.18.4 → python3_commons-0.18.6}/setup.cfg +0 -0
  32. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/__init__.py +0 -0
  33. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/api_client.py +0 -0
  34. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/async_functools.py +0 -0
  35. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/audit.py +0 -0
  36. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/auth.py +0 -0
  37. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/cache.py +0 -0
  38. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/conf.py +0 -0
  39. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/db/__init__.py +0 -0
  40. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/db/helpers.py +0 -0
  41. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/db/models/__init__.py +0 -0
  42. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/db/models/auth.py +0 -0
  43. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/db/models/common.py +0 -0
  44. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/db/models/rbac.py +0 -0
  45. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/db/models/users.py +0 -0
  46. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/exceptions.py +0 -0
  47. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/fs.py +0 -0
  48. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/generators.py +0 -0
  49. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/helpers.py +0 -0
  50. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/log/__init__.py +0 -0
  51. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/log/filters.py +0 -0
  52. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/log/formatters.py +0 -0
  53. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/object_storage.py +0 -0
  54. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/permissions.py +0 -0
  55. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/serializers/__init__.py +0 -0
  56. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/serializers/common.py +0 -0
  57. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/serializers/json.py +0 -0
  58. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/serializers/msgpack.py +0 -0
  59. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons/serializers/msgspec.py +0 -0
  60. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons.egg-info/SOURCES.txt +0 -0
  61. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  62. {python3_commons-0.18.4 → python3_commons-0.18.6}/src/python3_commons.egg-info/top_level.txt +0 -0
  63. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/__init__.py +0 -0
  64. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/integration/__init__.py +0 -0
  65. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/integration/test_cache.py +0 -0
  66. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/integration/test_osc.py +0 -0
  67. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/__init__.py +0 -0
  68. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/conftest.py +0 -0
  69. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/log/__init__.py +0 -0
  70. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/log/test_formatters.py +0 -0
  71. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/test_async_functools.py +0 -0
  72. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/test_audit.py +0 -0
  73. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/test_helpers.py +0 -0
  74. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/test_msgpack.py +0 -0
  75. {python3_commons-0.18.4 → python3_commons-0.18.6}/tests/unit/test_msgspec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.18.4
3
+ Version: 0.18.6
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License-Expression: GPL-3.0
@@ -19,6 +19,7 @@ Provides-Extra: all
19
19
  Requires-Dist: python3_commons[api-client,audit,authn,authz,cache,database,object-storage,soap-client]; extra == "all"
20
20
  Provides-Extra: api-client
21
21
  Requires-Dist: aiohttp[speedups]<3.15.0,>=3.13.5; extra == "api-client"
22
+ Requires-Dist: certifi==2026.4.22; extra == "api-client"
22
23
  Requires-Dist: python3_commons[object-storage]; extra == "api-client"
23
24
  Provides-Extra: audit
24
25
  Requires-Dist: lxml~=6.1.0; extra == "audit"
@@ -26,6 +27,7 @@ Requires-Dist: zeep[async]~=4.3.2; extra == "audit"
26
27
  Requires-Dist: python3_commons[object-storage]; extra == "audit"
27
28
  Provides-Extra: authn
28
29
  Requires-Dist: aiohttp[speedups]<3.15.0,>=3.13.5; extra == "authn"
30
+ Requires-Dist: certifi==2026.4.22; extra == "authn"
29
31
  Requires-Dist: python3_commons[api-client]; extra == "authn"
30
32
  Provides-Extra: authz
31
33
  Requires-Dist: python3_commons[database]; extra == "authz"
@@ -40,6 +42,7 @@ Requires-Dist: aiobotocore~=3.5.0; extra == "object-storage"
40
42
  Requires-Dist: object-storage-client==0.0.23; extra == "object-storage"
41
43
  Provides-Extra: soap-client
42
44
  Requires-Dist: aiohttp[speedups]<3.15.0,>=3.13.5; extra == "soap-client"
45
+ Requires-Dist: certifi==2026.4.22; extra == "soap-client"
43
46
  Requires-Dist: lxml~=6.1.0; extra == "soap-client"
44
47
  Requires-Dist: requests~=2.33.1; extra == "soap-client"
45
48
  Requires-Dist: zeep[async]~=4.3.2; extra == "soap-client"
@@ -29,6 +29,7 @@ all = [
29
29
  ]
30
30
  api-client = [
31
31
  "aiohttp[speedups]>=3.13.5,<3.15.0",
32
+ "certifi==2026.4.22",
32
33
  "python3_commons[object-storage]"
33
34
  ]
34
35
  audit = [
@@ -38,6 +39,7 @@ audit = [
38
39
  ]
39
40
  authn = [
40
41
  "aiohttp[speedups]>=3.13.5,<3.15.0",
42
+ "certifi==2026.4.22",
41
43
  "python3_commons[api-client]"
42
44
  ]
43
45
  authz = [
@@ -57,6 +59,7 @@ object-storage = [
57
59
  ]
58
60
  soap-client = [
59
61
  "aiohttp[speedups]>=3.13.5,<3.15.0",
62
+ "certifi==2026.4.22",
60
63
  "lxml~=6.1.0",
61
64
  "requests~=2.33.1",
62
65
  "zeep[async]~=4.3.2",
@@ -0,0 +1,250 @@
1
+ """
2
+ Async SOAP client built on aiohttp + zeep.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import concurrent.futures
9
+ import logging
10
+ import ssl
11
+ from collections.abc import AsyncIterator, Sequence
12
+ from contextlib import asynccontextmanager
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING, Any, Self
15
+
16
+ import certifi
17
+
18
+ try:
19
+ import aiohttp
20
+ from aiohttp import ClientSession, ClientTimeout, TCPConnector
21
+ from requests import Response
22
+ from requests.cookies import RequestsCookieJar
23
+ from zeep import AsyncClient
24
+ from zeep.exceptions import TransportError
25
+ from zeep.transports import Transport
26
+ from zeep.utils import get_version
27
+ from zeep.wsdl.utils import etree_to_string
28
+ except ImportError as e:
29
+ msg = 'Install python3-commons[soap-client] to use this feature'
30
+
31
+ raise RuntimeError(msg) from e
32
+
33
+ if TYPE_CHECKING:
34
+ from zeep.plugins import Plugin
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def _make_ssl_context(*, verify: bool) -> ssl.SSLContext | bool:
40
+ if not verify:
41
+ return False # aiohttp accepts False to disable verification
42
+
43
+ return ssl.create_default_context(cafile=certifi.where())
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class TransportConfig:
48
+ """Immutable transport settings passed to AsyncTransport."""
49
+
50
+ timeout: int = 300
51
+ """Total timeout in seconds for WSDL fetches."""
52
+
53
+ operation_timeout: int = 60
54
+ """Total timeout in seconds for SOAP operation calls."""
55
+
56
+ verify_ssl: bool = True
57
+ proxy: str | None = None
58
+
59
+
60
+ class AsyncTransport(Transport):
61
+ """
62
+ Async transport for zeep using aiohttp.
63
+
64
+ Usage::
65
+
66
+ async with soap_client("https://example.com/service?wsdl") as client:
67
+ result = await client.service.SomeOperation(...)
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ session: ClientSession,
74
+ config: TransportConfig,
75
+ _owns_session: bool = False,
76
+ ) -> None:
77
+ super().__init__()
78
+ self._session = session
79
+ self._config = config
80
+ self._owns_session = _owns_session
81
+
82
+ @classmethod
83
+ def from_config(
84
+ cls,
85
+ config: TransportConfig | None = None,
86
+ *,
87
+ session: ClientSession | None = None,
88
+ ) -> Self:
89
+ config = config or TransportConfig()
90
+ owns_session = session is None
91
+
92
+ if owns_session:
93
+ session = ClientSession(
94
+ connector=TCPConnector(ssl=_make_ssl_context(verify=config.verify_ssl)),
95
+ timeout=ClientTimeout(total=config.operation_timeout),
96
+ headers={'User-Agent': f'Zeep/{get_version()} (www.python-zeep.org)'},
97
+ )
98
+
99
+ return cls(session=session, config=config, _owns_session=owns_session)
100
+
101
+ async def aclose(self) -> None:
102
+ if self._owns_session:
103
+ await self._session.close()
104
+
105
+ async def __aenter__(self) -> Self:
106
+ return self
107
+
108
+ async def __aexit__(self, *_: object) -> None:
109
+ await self.aclose()
110
+
111
+ @staticmethod
112
+ def _build_response(response: aiohttp.ClientResponse, body: bytes) -> Response:
113
+ """Convert an aiohttp response into a requests.Response for zeep."""
114
+ r = Response()
115
+ r.status_code = response.status
116
+ r._content = body # noqa: SLF001
117
+ r.headers = dict(response.headers)
118
+ r.encoding = response.charset
119
+ r.url = str(response.url)
120
+
121
+ jar = RequestsCookieJar()
122
+
123
+ for name, morsel in response.cookies.items():
124
+ jar.set(name, morsel.value)
125
+
126
+ r.cookies = jar
127
+
128
+ return r
129
+
130
+ def load(self, url: str) -> bytes:
131
+ """
132
+ Sync entry-point zeep calls during WSDL document init.
133
+
134
+ Creates a short-lived session confined to its own event loop so there
135
+ is no cross-loop session sharing with the operational session.
136
+ """
137
+ if not url:
138
+ return b''
139
+
140
+ async def _fetch() -> bytes:
141
+ async with (
142
+ ClientSession(
143
+ connector=TCPConnector(ssl=_make_ssl_context(verify=self._config.verify_ssl)),
144
+ timeout=ClientTimeout(total=self._config.timeout),
145
+ headers={'User-Agent': f'Zeep/{get_version()} (www.python-zeep.org)'},
146
+ ) as session,
147
+ session.get(url, proxy=self._config.proxy) as resp,
148
+ ):
149
+ content = await resp.read()
150
+
151
+ if resp.status >= 400:
152
+ raise TransportError(
153
+ status_code=resp.status,
154
+ message=content.decode(errors='ignore'),
155
+ )
156
+
157
+ return content
158
+
159
+ # load() is always called from within a running event loop (during
160
+ # AsyncClient.__init__ inside the soap_client context manager), so
161
+ # run_until_complete on the running loop would raise. Always delegate
162
+ # to a thread with its own fresh event loop.
163
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
164
+ return pool.submit(asyncio.run, _fetch()).result()
165
+
166
+
167
+ async def post(
168
+ self,
169
+ address: str,
170
+ message: bytes,
171
+ headers: dict[str, str],
172
+ *,
173
+ timeout: int | None = None,
174
+ ) -> Response:
175
+ logger.debug('SOAP POST → %s\n%s', address, message)
176
+
177
+ async with self._session.post(
178
+ address,
179
+ data=message,
180
+ headers=headers,
181
+ proxy=self._config.proxy,
182
+ timeout=ClientTimeout(total=timeout) if timeout is not None else None,
183
+ ) as resp:
184
+ body = await resp.read()
185
+ logger.debug('SOAP ← %s (HTTP %d)\n%s', address, resp.status, body)
186
+
187
+ return self._build_response(resp, body)
188
+
189
+
190
+ async def post_xml(
191
+ self,
192
+ address: str,
193
+ envelope: Any,
194
+ headers: dict[str, str],
195
+ ) -> Response:
196
+ return await self.post(address, etree_to_string(envelope), headers)
197
+
198
+
199
+ async def get(
200
+ self,
201
+ address: str,
202
+ params: dict[str, str],
203
+ headers: dict[str, str],
204
+ ) -> Response:
205
+ async with self._session.get(
206
+ address,
207
+ params=params,
208
+ headers=headers,
209
+ proxy=self._config.proxy,
210
+ ) as resp:
211
+ body = await resp.read()
212
+
213
+ return self._build_response(resp, body)
214
+
215
+
216
+ def build_soap_client(
217
+ wsdl_url: str,
218
+ transport: AsyncTransport,
219
+ plugins: Sequence[Plugin] | None = None,
220
+ ) -> AsyncClient:
221
+ if not wsdl_url:
222
+ msg = 'wsdl_url must be a non-empty string.'
223
+
224
+ raise ValueError(msg)
225
+
226
+ return AsyncClient(wsdl_url, transport=transport, plugins=list(plugins or []))
227
+
228
+
229
+ @asynccontextmanager
230
+ async def soap_client(
231
+ wsdl_url: str,
232
+ *,
233
+ config: TransportConfig | None = None,
234
+ session: ClientSession | None = None,
235
+ plugins: Sequence[Plugin] | None = None,
236
+ ) -> AsyncIterator[AsyncClient]:
237
+ """
238
+ Async context manager yielding a ready-to-use zeep AsyncClient.
239
+
240
+ Example::
241
+
242
+ async with soap_client("https://example.com/service?wsdl") as client:
243
+ result = await client.service.GetData(id=42)
244
+ """
245
+ transport = AsyncTransport.from_config(config, session=session)
246
+
247
+ try:
248
+ yield build_soap_client(wsdl_url, transport, plugins)
249
+ finally:
250
+ await transport.aclose()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.18.4
3
+ Version: 0.18.6
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License-Expression: GPL-3.0
@@ -19,6 +19,7 @@ Provides-Extra: all
19
19
  Requires-Dist: python3_commons[api-client,audit,authn,authz,cache,database,object-storage,soap-client]; extra == "all"
20
20
  Provides-Extra: api-client
21
21
  Requires-Dist: aiohttp[speedups]<3.15.0,>=3.13.5; extra == "api-client"
22
+ Requires-Dist: certifi==2026.4.22; extra == "api-client"
22
23
  Requires-Dist: python3_commons[object-storage]; extra == "api-client"
23
24
  Provides-Extra: audit
24
25
  Requires-Dist: lxml~=6.1.0; extra == "audit"
@@ -26,6 +27,7 @@ Requires-Dist: zeep[async]~=4.3.2; extra == "audit"
26
27
  Requires-Dist: python3_commons[object-storage]; extra == "audit"
27
28
  Provides-Extra: authn
28
29
  Requires-Dist: aiohttp[speedups]<3.15.0,>=3.13.5; extra == "authn"
30
+ Requires-Dist: certifi==2026.4.22; extra == "authn"
29
31
  Requires-Dist: python3_commons[api-client]; extra == "authn"
30
32
  Provides-Extra: authz
31
33
  Requires-Dist: python3_commons[database]; extra == "authz"
@@ -40,6 +42,7 @@ Requires-Dist: aiobotocore~=3.5.0; extra == "object-storage"
40
42
  Requires-Dist: object-storage-client==0.0.23; extra == "object-storage"
41
43
  Provides-Extra: soap-client
42
44
  Requires-Dist: aiohttp[speedups]<3.15.0,>=3.13.5; extra == "soap-client"
45
+ Requires-Dist: certifi==2026.4.22; extra == "soap-client"
43
46
  Requires-Dist: lxml~=6.1.0; extra == "soap-client"
44
47
  Requires-Dist: requests~=2.33.1; extra == "soap-client"
45
48
  Requires-Dist: zeep[async]~=4.3.2; extra == "soap-client"
@@ -7,6 +7,7 @@ python3_commons[api-client,audit,authn,authz,cache,database,object-storage,soap-
7
7
 
8
8
  [api-client]
9
9
  aiohttp[speedups]<3.15.0,>=3.13.5
10
+ certifi==2026.4.22
10
11
  python3_commons[object-storage]
11
12
 
12
13
  [audit]
@@ -16,6 +17,7 @@ python3_commons[object-storage]
16
17
 
17
18
  [authn]
18
19
  aiohttp[speedups]<3.15.0,>=3.13.5
20
+ certifi==2026.4.22
19
21
  python3_commons[api-client]
20
22
 
21
23
  [authz]
@@ -35,6 +37,7 @@ object-storage-client==0.0.23
35
37
 
36
38
  [soap-client]
37
39
  aiohttp[speedups]<3.15.0,>=3.13.5
40
+ certifi==2026.4.22
38
41
  lxml~=6.1.0
39
42
  requests~=2.33.1
40
43
  zeep[async]~=4.3.2
@@ -1051,6 +1051,7 @@ all = [
1051
1051
  { name = "aiobotocore" },
1052
1052
  { name = "aiohttp", extra = ["speedups"] },
1053
1053
  { name = "asyncpg" },
1054
+ { name = "certifi" },
1054
1055
  { name = "lxml" },
1055
1056
  { name = "object-storage-client" },
1056
1057
  { name = "requests" },
@@ -1061,7 +1062,7 @@ all = [
1061
1062
  api-client = [
1062
1063
  { name = "aiobotocore" },
1063
1064
  { name = "aiohttp", extra = ["speedups"] },
1064
- { name = "lxml" },
1065
+ { name = "certifi" },
1065
1066
  { name = "object-storage-client" },
1066
1067
  ]
1067
1068
  audit = [
@@ -1073,14 +1074,14 @@ audit = [
1073
1074
  authn = [
1074
1075
  { name = "aiobotocore" },
1075
1076
  { name = "aiohttp", extra = ["speedups"] },
1076
- { name = "lxml" },
1077
+ { name = "certifi" },
1077
1078
  { name = "object-storage-client" },
1078
1079
  ]
1079
1080
  authz = [
1080
1081
  { name = "aiobotocore" },
1081
1082
  { name = "aiohttp", extra = ["speedups"] },
1082
1083
  { name = "asyncpg" },
1083
- { name = "lxml" },
1084
+ { name = "certifi" },
1084
1085
  { name = "object-storage-client" },
1085
1086
  { name = "sqlalchemy", extra = ["asyncio"] },
1086
1087
  ]
@@ -1097,6 +1098,7 @@ object-storage = [
1097
1098
  ]
1098
1099
  soap-client = [
1099
1100
  { name = "aiohttp", extra = ["speedups"] },
1101
+ { name = "certifi" },
1100
1102
  { name = "lxml" },
1101
1103
  { name = "requests" },
1102
1104
  { name = "zeep", extra = ["async"] },
@@ -1126,7 +1128,9 @@ requires-dist = [
1126
1128
  { name = "aiohttp", extras = ["speedups"], marker = "extra == 'authn'", specifier = ">=3.13.5,<3.15.0" },
1127
1129
  { name = "aiohttp", extras = ["speedups"], marker = "extra == 'soap-client'", specifier = ">=3.13.5,<3.15.0" },
1128
1130
  { name = "asyncpg", marker = "extra == 'database'", specifier = "~=0.31.0" },
1129
- { name = "lxml", marker = "extra == 'api-client'", specifier = "~=6.1.0" },
1131
+ { name = "certifi", marker = "extra == 'api-client'", specifier = "==2026.4.22" },
1132
+ { name = "certifi", marker = "extra == 'authn'", specifier = "==2026.4.22" },
1133
+ { name = "certifi", marker = "extra == 'soap-client'", specifier = "==2026.4.22" },
1130
1134
  { name = "lxml", marker = "extra == 'audit'", specifier = "~=6.1.0" },
1131
1135
  { name = "lxml", marker = "extra == 'soap-client'", specifier = "~=6.1.0" },
1132
1136
  { name = "msgpack", specifier = "~=1.1.2" },
@@ -1,299 +0,0 @@
1
- """
2
- Async SOAP client built on aiohttp + zeep.
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- import asyncio
8
- import concurrent.futures
9
- import logging
10
- from collections.abc import AsyncIterator, Sequence
11
- from contextlib import asynccontextmanager
12
- from dataclasses import dataclass
13
- from typing import TYPE_CHECKING, Any, Self
14
-
15
- try:
16
- import aiohttp
17
- from aiohttp import ClientSession, ClientTimeout, TCPConnector
18
- from requests import Response
19
- from requests.cookies import RequestsCookieJar
20
- from zeep import AsyncClient
21
- from zeep.exceptions import TransportError
22
- from zeep.transports import Transport
23
- from zeep.utils import get_version
24
- from zeep.wsdl.utils import etree_to_string
25
- except ImportError as e:
26
- msg = 'Install python3-commons[soap-client] to use this feature'
27
-
28
- raise RuntimeError(msg) from e
29
-
30
- if TYPE_CHECKING:
31
- from zeep.plugins import Plugin
32
-
33
- logger = logging.getLogger(__name__)
34
-
35
-
36
- @dataclass(frozen=True, slots=True)
37
- class TransportConfig:
38
- """Immutable transport settings passed to AsyncTransport."""
39
-
40
- timeout: int = 300
41
- """Total timeout in seconds for WSDL fetches."""
42
-
43
- operation_timeout: int = 60
44
- """Total timeout in seconds for SOAP operation calls."""
45
-
46
- verify_ssl: bool = True
47
- proxy: str | None = None
48
-
49
-
50
- class AsyncTransport(Transport):
51
- """
52
- Async transport for zeep using aiohttp.
53
-
54
- Usage::
55
-
56
- async with AsyncTransport.from_config(config) as transport:
57
- client = AsyncClient(wsdl_url, transport=transport)
58
- result = await client.service.SomeOperation(...)
59
- """
60
-
61
- def __init__(
62
- self,
63
- *,
64
- session: ClientSession,
65
- wsdl_session: ClientSession,
66
- config: TransportConfig,
67
- _owns_session: bool = False,
68
- _owns_wsdl_session: bool = False,
69
- ) -> None:
70
- super().__init__()
71
- self._session = session
72
- self._wsdl_session = wsdl_session
73
- self._config = config
74
- self._owns_session = _owns_session
75
- self._owns_wsdl_session = _owns_wsdl_session
76
-
77
- @classmethod
78
- def from_config(
79
- cls,
80
- config: TransportConfig | None = None,
81
- *,
82
- session: ClientSession | None = None,
83
- wsdl_session: ClientSession | None = None,
84
- ) -> AsyncTransport:
85
- """
86
- Create a transport, optionally sharing an existing ClientSession.
87
-
88
- If *session* / *wsdl_session* are omitted the transport owns (and
89
- will close) the sessions it creates.
90
- """
91
- config = config or TransportConfig()
92
- connector = TCPConnector(ssl=config.verify_ssl)
93
- user_agent = f'Zeep/{get_version()} (www.python-zeep.org)'
94
-
95
- owns_session = session is None
96
- owns_wsdl_session = wsdl_session is None
97
-
98
- if owns_session:
99
- session = ClientSession(
100
- connector=connector,
101
- timeout=ClientTimeout(total=config.operation_timeout),
102
- headers={'User-Agent': user_agent},
103
- )
104
-
105
- if owns_wsdl_session:
106
- wsdl_session = ClientSession(
107
- connector=connector,
108
- timeout=ClientTimeout(total=config.timeout),
109
- headers={'User-Agent': user_agent},
110
- )
111
-
112
- return cls(
113
- session=session,
114
- wsdl_session=wsdl_session,
115
- config=config,
116
- _owns_session=owns_session,
117
- _owns_wsdl_session=owns_wsdl_session,
118
- )
119
-
120
- async def aclose(self) -> None:
121
- if self._owns_session:
122
- await self._session.close()
123
- if self._owns_wsdl_session:
124
- await self._wsdl_session.close()
125
-
126
- async def __aenter__(self) -> Self:
127
- return self
128
-
129
- async def __aexit__(self, *_: object) -> None:
130
- await self.aclose()
131
-
132
- @staticmethod
133
- def _build_response(response: aiohttp.ClientResponse, body: bytes) -> Response:
134
- """Convert an aiohttp response into a requests.Response for zeep."""
135
- r = Response()
136
- r.status_code = response.status
137
- r._content = body # noqa: SLF001 (zeep reads this attribute directly)
138
- r.headers = dict(response.headers)
139
- r.encoding = response.charset
140
- r.url = str(response.url)
141
-
142
- # Bridge aiohttp SimpleCookie → RequestsCookieJar so zeep / requests
143
- # cookie handling works correctly.
144
- jar = RequestsCookieJar()
145
-
146
- for name, morsel in response.cookies.items():
147
- jar.set(name, morsel.value)
148
-
149
- r.cookies = jar
150
-
151
- return r
152
-
153
- async def _load_remote_data(self, url: str) -> bytes:
154
- """Fetch WSDL / XSD documents (called by zeep during init)."""
155
-
156
- async def _fetch() -> bytes:
157
- async with self._wsdl_session.get(url, proxy=self._config.proxy) as resp:
158
- content = await resp.read()
159
-
160
- if resp.status >= 400:
161
- raise TransportError(
162
- status_code=resp.status,
163
- message=content.decode(errors='ignore'),
164
- )
165
-
166
- return content
167
-
168
- return await _fetch()
169
-
170
- def load(self, url: str) -> bytes:
171
- """Sync entry-point zeep calls during WSDL document init."""
172
- if not url:
173
- return b''
174
-
175
- async def _fetch() -> bytes:
176
- connector = TCPConnector(ssl=self._config.verify_ssl)
177
-
178
- async with ClientSession(
179
- connector=connector,
180
- timeout=ClientTimeout(total=self._config.timeout),
181
- headers={'User-Agent': f'Zeep/{get_version()} (www.python-zeep.org)'},
182
- ) as session, session.get(url, proxy=self._config.proxy) as resp:
183
- content = await resp.read()
184
-
185
- if resp.status >= 400:
186
- raise TransportError(
187
- status_code=resp.status,
188
- message=content.decode(errors='ignore'),
189
- )
190
-
191
- return content
192
-
193
- try:
194
- loop = asyncio.get_event_loop()
195
-
196
- return loop.run_until_complete(_fetch())
197
- except RuntimeError:
198
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
199
- future = pool.submit(asyncio.run, _fetch())
200
-
201
- return future.result()
202
-
203
- async def post(
204
- self,
205
- address: str,
206
- message: bytes,
207
- headers: dict[str, str],
208
- *,
209
- timeout: int | None = None,
210
- ) -> Response:
211
- logger.debug('SOAP POST → %s\n%s', address, message)
212
-
213
- request_timeout = ClientTimeout(total=timeout) if timeout is not None else None
214
-
215
- async def _post() -> Response:
216
- async with self._session.post(
217
- address,
218
- data=message,
219
- headers=headers,
220
- proxy=self._config.proxy,
221
- timeout=request_timeout,
222
- ) as resp:
223
- body = await resp.read()
224
- logger.debug('SOAP ← %s (HTTP %d)\n%s', address, resp.status, body)
225
-
226
- return self._build_response(resp, body)
227
-
228
- return await _post()
229
-
230
- async def post_xml(
231
- self,
232
- address: str,
233
- envelope: Any,
234
- headers: dict[str, str],
235
- ) -> Response:
236
- message = etree_to_string(envelope)
237
-
238
- return await self.post(address, message, headers)
239
-
240
- async def get(
241
- self,
242
- address: str,
243
- params: dict[str, str],
244
- headers: dict[str, str],
245
- ) -> Response:
246
- async with self._session.get(
247
- address,
248
- params=params,
249
- headers=headers,
250
- proxy=self._config.proxy,
251
- ) as resp:
252
- body = await resp.read()
253
-
254
- return self._build_response(resp, body)
255
-
256
-
257
- def build_soap_client(
258
- wsdl_url: str,
259
- transport: AsyncTransport,
260
- plugins: Sequence[Plugin] | None = None,
261
- ) -> AsyncClient:
262
- """
263
- Construct a zeep AsyncClient with the supplied transport and plugins.
264
-
265
- Raises ValueError if *wsdl_url* is empty or None.
266
- """
267
- if not wsdl_url:
268
- msg = 'wsdl_url must be a non-empty string.'
269
-
270
- raise ValueError(msg)
271
-
272
- return AsyncClient(wsdl_url, transport=transport, plugins=list(plugins or []))
273
-
274
-
275
- @asynccontextmanager
276
- async def soap_client(
277
- wsdl_url: str,
278
- *,
279
- config: TransportConfig | None = None,
280
- session: ClientSession | None = None,
281
- plugins: Sequence[Plugin] | None = None,
282
- ) -> AsyncIterator[AsyncClient]:
283
- """
284
- Async context manager that yields a ready-to-use zeep AsyncClient and
285
- cleans up the transport on exit.
286
-
287
- Example::
288
-
289
- async with soap_client('https://example.com/service?wsdl') as client:
290
- result = await client.service.GetData(id=42)
291
- """
292
- transport = AsyncTransport.from_config(config, session=session)
293
-
294
- try:
295
- client = build_soap_client(wsdl_url, transport, plugins) # WSDL loaded here (sync via load())
296
-
297
- yield client
298
- finally:
299
- await transport.aclose()