python3-commons 0.18.4__tar.gz → 0.18.5__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.5}/PKG-INFO +1 -1
  2. python3_commons-0.18.5/src/python3_commons/soap_client.py +243 -0
  3. {python3_commons-0.18.4 → python3_commons-0.18.5/src/python3_commons.egg-info}/PKG-INFO +1 -1
  4. python3_commons-0.18.4/src/python3_commons/soap_client.py +0 -299
  5. {python3_commons-0.18.4 → python3_commons-0.18.5}/.coveragerc +0 -0
  6. {python3_commons-0.18.4 → python3_commons-0.18.5}/.devcontainer/Dockerfile +0 -0
  7. {python3_commons-0.18.4 → python3_commons-0.18.5}/.devcontainer/devcontainer.json +0 -0
  8. {python3_commons-0.18.4 → python3_commons-0.18.5}/.devcontainer/docker-compose.yml +0 -0
  9. {python3_commons-0.18.4 → python3_commons-0.18.5}/.env_template +0 -0
  10. {python3_commons-0.18.4 → python3_commons-0.18.5}/.github/workflows/checks.yml +0 -0
  11. {python3_commons-0.18.4 → python3_commons-0.18.5}/.github/workflows/python-publish.yaml +0 -0
  12. {python3_commons-0.18.4 → python3_commons-0.18.5}/.github/workflows/release-on-tag-push.yml +0 -0
  13. {python3_commons-0.18.4 → python3_commons-0.18.5}/.gitignore +0 -0
  14. {python3_commons-0.18.4 → python3_commons-0.18.5}/.pre-commit-config.yaml +0 -0
  15. {python3_commons-0.18.4 → python3_commons-0.18.5}/.python-version +0 -0
  16. {python3_commons-0.18.4 → python3_commons-0.18.5}/AUTHORS.rst +0 -0
  17. {python3_commons-0.18.4 → python3_commons-0.18.5}/CHANGELOG.rst +0 -0
  18. {python3_commons-0.18.4 → python3_commons-0.18.5}/LICENSE +0 -0
  19. {python3_commons-0.18.4 → python3_commons-0.18.5}/README.md +0 -0
  20. {python3_commons-0.18.4 → python3_commons-0.18.5}/README.rst +0 -0
  21. {python3_commons-0.18.4 → python3_commons-0.18.5}/docs/Makefile +0 -0
  22. {python3_commons-0.18.4 → python3_commons-0.18.5}/docs/_static/.gitignore +0 -0
  23. {python3_commons-0.18.4 → python3_commons-0.18.5}/docs/authors.rst +0 -0
  24. {python3_commons-0.18.4 → python3_commons-0.18.5}/docs/changelog.rst +0 -0
  25. {python3_commons-0.18.4 → python3_commons-0.18.5}/docs/conf.py +0 -0
  26. {python3_commons-0.18.4 → python3_commons-0.18.5}/docs/index.rst +0 -0
  27. {python3_commons-0.18.4 → python3_commons-0.18.5}/docs/license.rst +0 -0
  28. {python3_commons-0.18.4 → python3_commons-0.18.5}/pyproject.toml +0 -0
  29. {python3_commons-0.18.4 → python3_commons-0.18.5}/setup.cfg +0 -0
  30. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/__init__.py +0 -0
  31. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/api_client.py +0 -0
  32. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/async_functools.py +0 -0
  33. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/audit.py +0 -0
  34. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/auth.py +0 -0
  35. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/cache.py +0 -0
  36. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/conf.py +0 -0
  37. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/db/__init__.py +0 -0
  38. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/db/helpers.py +0 -0
  39. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/db/models/__init__.py +0 -0
  40. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/db/models/auth.py +0 -0
  41. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/db/models/common.py +0 -0
  42. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/db/models/rbac.py +0 -0
  43. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/db/models/users.py +0 -0
  44. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/exceptions.py +0 -0
  45. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/fs.py +0 -0
  46. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/generators.py +0 -0
  47. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/helpers.py +0 -0
  48. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/log/__init__.py +0 -0
  49. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/log/filters.py +0 -0
  50. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/log/formatters.py +0 -0
  51. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/object_storage.py +0 -0
  52. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/permissions.py +0 -0
  53. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/serializers/__init__.py +0 -0
  54. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/serializers/common.py +0 -0
  55. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/serializers/json.py +0 -0
  56. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/serializers/msgpack.py +0 -0
  57. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons/serializers/msgspec.py +0 -0
  58. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons.egg-info/SOURCES.txt +0 -0
  59. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  60. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons.egg-info/requires.txt +0 -0
  61. {python3_commons-0.18.4 → python3_commons-0.18.5}/src/python3_commons.egg-info/top_level.txt +0 -0
  62. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/__init__.py +0 -0
  63. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/integration/__init__.py +0 -0
  64. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/integration/test_cache.py +0 -0
  65. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/integration/test_osc.py +0 -0
  66. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/__init__.py +0 -0
  67. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/conftest.py +0 -0
  68. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/log/__init__.py +0 -0
  69. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/log/test_formatters.py +0 -0
  70. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/test_async_functools.py +0 -0
  71. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/test_audit.py +0 -0
  72. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/test_helpers.py +0 -0
  73. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/test_msgpack.py +0 -0
  74. {python3_commons-0.18.4 → python3_commons-0.18.5}/tests/unit/test_msgspec.py +0 -0
  75. {python3_commons-0.18.4 → python3_commons-0.18.5}/uv.lock +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.5
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
@@ -0,0 +1,243 @@
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 soap_client("https://example.com/service?wsdl") as client:
57
+ result = await client.service.SomeOperation(...)
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ *,
63
+ session: ClientSession,
64
+ config: TransportConfig,
65
+ _owns_session: bool = False,
66
+ ) -> None:
67
+ super().__init__()
68
+ self._session = session
69
+ self._config = config
70
+ self._owns_session = _owns_session
71
+
72
+ @classmethod
73
+ def from_config(
74
+ cls,
75
+ config: TransportConfig | None = None,
76
+ *,
77
+ session: ClientSession | None = None,
78
+ ) -> Self:
79
+ """
80
+ Create a transport, optionally sharing an existing ClientSession.
81
+
82
+ If *session* is omitted the transport owns (and will close) the
83
+ session it creates.
84
+ """
85
+ config = config or TransportConfig()
86
+ owns_session = session is None
87
+
88
+ if owns_session:
89
+ session = ClientSession(
90
+ connector=TCPConnector(ssl=config.verify_ssl),
91
+ timeout=ClientTimeout(total=config.operation_timeout),
92
+ headers={'User-Agent': f'Zeep/{get_version()} (www.python-zeep.org)'},
93
+ )
94
+
95
+ return cls(session=session, config=config, _owns_session=owns_session)
96
+
97
+ async def aclose(self) -> None:
98
+ if self._owns_session:
99
+ await self._session.close()
100
+
101
+ async def __aenter__(self) -> Self:
102
+ return self
103
+
104
+ async def __aexit__(self, *_: object) -> None:
105
+ await self.aclose()
106
+
107
+ @staticmethod
108
+ def _build_response(response: aiohttp.ClientResponse, body: bytes) -> Response:
109
+ """Convert an aiohttp response into a requests.Response for zeep."""
110
+ r = Response()
111
+ r.status_code = response.status
112
+ r._content = body # noqa: SLF001
113
+ r.headers = dict(response.headers)
114
+ r.encoding = response.charset
115
+ r.url = str(response.url)
116
+
117
+ jar = RequestsCookieJar()
118
+
119
+ for name, morsel in response.cookies.items():
120
+ jar.set(name, morsel.value)
121
+
122
+ r.cookies = jar
123
+
124
+ return r
125
+
126
+ def load(self, url: str) -> bytes:
127
+ """
128
+ Sync entry-point zeep calls during WSDL document init.
129
+
130
+ Creates a short-lived session confined to its own event loop so there
131
+ is no cross-loop session sharing with the operational session.
132
+ """
133
+ if not url:
134
+ return b''
135
+
136
+ async def _fetch() -> bytes:
137
+ async with (
138
+ ClientSession(
139
+ connector=TCPConnector(ssl=self._config.verify_ssl),
140
+ timeout=ClientTimeout(total=self._config.timeout),
141
+ headers={'User-Agent': f'Zeep/{get_version()} (www.python-zeep.org)'},
142
+ ) as session,
143
+ session.get(url, proxy=self._config.proxy) as resp,
144
+ ):
145
+ content = await resp.read()
146
+
147
+ if resp.status >= 400:
148
+ raise TransportError(
149
+ status_code=resp.status,
150
+ message=content.decode(errors='ignore'),
151
+ )
152
+
153
+ return content
154
+
155
+ # load() is always called from within a running event loop (during
156
+ # AsyncClient.__init__ inside the soap_client context manager), so
157
+ # run_until_complete on the running loop would raise. Always delegate
158
+ # to a thread with its own fresh event loop.
159
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
160
+ return pool.submit(asyncio.run, _fetch()).result()
161
+
162
+ async def post(
163
+ self,
164
+ address: str,
165
+ message: bytes,
166
+ headers: dict[str, str],
167
+ *,
168
+ timeout: int | None = None,
169
+ ) -> Response:
170
+ logger.debug('SOAP POST → %s\n%s', address, message)
171
+
172
+ async with self._session.post(
173
+ address,
174
+ data=message,
175
+ headers=headers,
176
+ proxy=self._config.proxy,
177
+ timeout=ClientTimeout(total=timeout) if timeout is not None else None,
178
+ ) as resp:
179
+ body = await resp.read()
180
+ logger.debug('SOAP ← %s (HTTP %d)\n%s', address, resp.status, body)
181
+
182
+ return self._build_response(resp, body)
183
+
184
+ async def post_xml(
185
+ self,
186
+ address: str,
187
+ envelope: Any,
188
+ headers: dict[str, str],
189
+ ) -> Response:
190
+ return await self.post(address, etree_to_string(envelope), headers)
191
+
192
+ async def get(
193
+ self,
194
+ address: str,
195
+ params: dict[str, str],
196
+ headers: dict[str, str],
197
+ ) -> Response:
198
+ async with self._session.get(
199
+ address,
200
+ params=params,
201
+ headers=headers,
202
+ proxy=self._config.proxy,
203
+ ) as resp:
204
+ body = await resp.read()
205
+
206
+ return self._build_response(resp, body)
207
+
208
+
209
+ def build_soap_client(
210
+ wsdl_url: str,
211
+ transport: AsyncTransport,
212
+ plugins: Sequence[Plugin] | None = None,
213
+ ) -> AsyncClient:
214
+ if not wsdl_url:
215
+ msg = 'wsdl_url must be a non-empty string.'
216
+
217
+ raise ValueError(msg)
218
+
219
+ return AsyncClient(wsdl_url, transport=transport, plugins=list(plugins or []))
220
+
221
+
222
+ @asynccontextmanager
223
+ async def soap_client(
224
+ wsdl_url: str,
225
+ *,
226
+ config: TransportConfig | None = None,
227
+ session: ClientSession | None = None,
228
+ plugins: Sequence[Plugin] | None = None,
229
+ ) -> AsyncIterator[AsyncClient]:
230
+ """
231
+ Async context manager yielding a ready-to-use zeep AsyncClient.
232
+
233
+ Example::
234
+
235
+ async with soap_client("https://example.com/service?wsdl") as client:
236
+ result = await client.service.GetData(id=42)
237
+ """
238
+ transport = AsyncTransport.from_config(config, session=session)
239
+
240
+ try:
241
+ yield build_soap_client(wsdl_url, transport, plugins)
242
+ finally:
243
+ 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.5
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
@@ -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()