python3-commons 0.18.7__tar.gz → 0.18.8__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 (74) hide show
  1. {python3_commons-0.18.7/src/python3_commons.egg-info → python3_commons-0.18.8}/PKG-INFO +1 -1
  2. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/soap_client.py +97 -110
  3. {python3_commons-0.18.7 → python3_commons-0.18.8/src/python3_commons.egg-info}/PKG-INFO +1 -1
  4. {python3_commons-0.18.7 → python3_commons-0.18.8}/.coveragerc +0 -0
  5. {python3_commons-0.18.7 → python3_commons-0.18.8}/.devcontainer/Dockerfile +0 -0
  6. {python3_commons-0.18.7 → python3_commons-0.18.8}/.devcontainer/devcontainer.json +0 -0
  7. {python3_commons-0.18.7 → python3_commons-0.18.8}/.devcontainer/docker-compose.yml +0 -0
  8. {python3_commons-0.18.7 → python3_commons-0.18.8}/.env_template +0 -0
  9. {python3_commons-0.18.7 → python3_commons-0.18.8}/.github/workflows/checks.yml +0 -0
  10. {python3_commons-0.18.7 → python3_commons-0.18.8}/.github/workflows/python-publish.yaml +0 -0
  11. {python3_commons-0.18.7 → python3_commons-0.18.8}/.github/workflows/release-on-tag-push.yml +0 -0
  12. {python3_commons-0.18.7 → python3_commons-0.18.8}/.gitignore +0 -0
  13. {python3_commons-0.18.7 → python3_commons-0.18.8}/.pre-commit-config.yaml +0 -0
  14. {python3_commons-0.18.7 → python3_commons-0.18.8}/.python-version +0 -0
  15. {python3_commons-0.18.7 → python3_commons-0.18.8}/AUTHORS.rst +0 -0
  16. {python3_commons-0.18.7 → python3_commons-0.18.8}/CHANGELOG.rst +0 -0
  17. {python3_commons-0.18.7 → python3_commons-0.18.8}/LICENSE +0 -0
  18. {python3_commons-0.18.7 → python3_commons-0.18.8}/README.md +0 -0
  19. {python3_commons-0.18.7 → python3_commons-0.18.8}/README.rst +0 -0
  20. {python3_commons-0.18.7 → python3_commons-0.18.8}/docs/Makefile +0 -0
  21. {python3_commons-0.18.7 → python3_commons-0.18.8}/docs/_static/.gitignore +0 -0
  22. {python3_commons-0.18.7 → python3_commons-0.18.8}/docs/authors.rst +0 -0
  23. {python3_commons-0.18.7 → python3_commons-0.18.8}/docs/changelog.rst +0 -0
  24. {python3_commons-0.18.7 → python3_commons-0.18.8}/docs/conf.py +0 -0
  25. {python3_commons-0.18.7 → python3_commons-0.18.8}/docs/index.rst +0 -0
  26. {python3_commons-0.18.7 → python3_commons-0.18.8}/docs/license.rst +0 -0
  27. {python3_commons-0.18.7 → python3_commons-0.18.8}/pyproject.toml +0 -0
  28. {python3_commons-0.18.7 → python3_commons-0.18.8}/setup.cfg +0 -0
  29. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/__init__.py +0 -0
  30. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/api_client.py +0 -0
  31. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/async_functools.py +0 -0
  32. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/audit.py +0 -0
  33. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/auth.py +0 -0
  34. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/cache.py +0 -0
  35. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/conf.py +0 -0
  36. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/db/__init__.py +0 -0
  37. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/db/helpers.py +0 -0
  38. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/db/models/__init__.py +0 -0
  39. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/db/models/auth.py +0 -0
  40. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/db/models/common.py +0 -0
  41. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/db/models/rbac.py +0 -0
  42. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/db/models/users.py +0 -0
  43. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/exceptions.py +0 -0
  44. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/fs.py +0 -0
  45. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/generators.py +0 -0
  46. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/helpers.py +0 -0
  47. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/log/__init__.py +0 -0
  48. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/log/filters.py +0 -0
  49. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/log/formatters.py +0 -0
  50. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/object_storage.py +0 -0
  51. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/permissions.py +0 -0
  52. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/serializers/__init__.py +0 -0
  53. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/serializers/common.py +0 -0
  54. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/serializers/json.py +0 -0
  55. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/serializers/msgpack.py +0 -0
  56. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons/serializers/msgspec.py +0 -0
  57. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons.egg-info/SOURCES.txt +0 -0
  58. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  59. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons.egg-info/requires.txt +0 -0
  60. {python3_commons-0.18.7 → python3_commons-0.18.8}/src/python3_commons.egg-info/top_level.txt +0 -0
  61. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/__init__.py +0 -0
  62. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/integration/__init__.py +0 -0
  63. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/integration/test_cache.py +0 -0
  64. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/integration/test_osc.py +0 -0
  65. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/__init__.py +0 -0
  66. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/conftest.py +0 -0
  67. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/log/__init__.py +0 -0
  68. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/log/test_formatters.py +0 -0
  69. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/test_async_functools.py +0 -0
  70. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/test_audit.py +0 -0
  71. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/test_helpers.py +0 -0
  72. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/test_msgpack.py +0 -0
  73. {python3_commons-0.18.7 → python3_commons-0.18.8}/tests/unit/test_msgspec.py +0 -0
  74. {python3_commons-0.18.7 → python3_commons-0.18.8}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.18.7
3
+ Version: 0.18.8
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,11 +1,6 @@
1
- """
2
- Async SOAP client built on aiohttp + zeep.
3
- """
4
-
5
1
  from __future__ import annotations
6
2
 
7
3
  import asyncio
8
- import concurrent.futures
9
4
  import logging
10
5
  import ssl
11
6
  from collections.abc import AsyncIterator, Sequence
@@ -13,10 +8,9 @@ from contextlib import asynccontextmanager
13
8
  from dataclasses import dataclass
14
9
  from typing import TYPE_CHECKING, Any, Self
15
10
 
16
- import certifi
17
-
18
11
  try:
19
12
  import aiohttp
13
+ import certifi
20
14
  from aiohttp import ClientSession, ClientTimeout, TCPConnector
21
15
  from requests import Response
22
16
  from requests.cookies import RequestsCookieJar
@@ -38,55 +32,48 @@ logger = logging.getLogger(__name__)
38
32
 
39
33
  def _make_ssl_context(*, verify: bool) -> ssl.SSLContext | bool:
40
34
  if not verify:
41
- return False # aiohttp accepts False to disable verification
35
+ return False
42
36
 
43
37
  return ssl.create_default_context(cafile=certifi.where())
44
38
 
45
39
 
46
40
  @dataclass(frozen=True, slots=True)
47
41
  class TransportConfig:
48
- """Immutable transport settings passed to AsyncTransport."""
49
-
50
42
  timeout: int = 300
51
- """Total timeout in seconds for WSDL fetches."""
52
-
53
43
  operation_timeout: int = 60
54
- """Total timeout in seconds for SOAP operation calls."""
55
-
56
44
  verify_ssl: bool = True
57
45
  proxy: str | None = None
58
46
 
59
47
 
60
- class AsyncTransport(Transport):
61
- """
62
- Async transport for zeep using aiohttp.
63
-
64
- Usage::
48
+ # -------------------------
49
+ # Transport
50
+ # -------------------------
65
51
 
66
- async with soap_client("https://example.com/service?wsdl") as client:
67
- result = await client.service.SomeOperation(...)
68
- """
69
52
 
53
+ class AsyncTransport(Transport):
70
54
  def __init__(
71
- self,
72
- *,
73
- session: ClientSession,
74
- config: TransportConfig,
75
- _owns_session: bool = False,
55
+ self,
56
+ *,
57
+ session: ClientSession,
58
+ config: TransportConfig,
59
+ loop: asyncio.AbstractEventLoop,
60
+ owns_session: bool = False,
76
61
  ) -> None:
77
62
  super().__init__()
78
63
  self._session = session
79
64
  self._config = config
80
- self._owns_session = _owns_session
65
+ self._loop = loop
66
+ self._owns_session = owns_session
81
67
 
82
68
  @classmethod
83
69
  def from_config(
84
- cls,
85
- config: TransportConfig | None = None,
86
- *,
87
- session: ClientSession | None = None,
88
- ) -> Self:
70
+ cls,
71
+ config: TransportConfig | None = None,
72
+ *,
73
+ session: ClientSession | None = None,
74
+ ) -> AsyncTransport:
89
75
  config = config or TransportConfig()
76
+ loop = asyncio.get_running_loop()
90
77
  owns_session = session is None
91
78
 
92
79
  if owns_session:
@@ -96,7 +83,12 @@ class AsyncTransport(Transport):
96
83
  headers={'User-Agent': f'Zeep/{get_version()} (www.python-zeep.org)'},
97
84
  )
98
85
 
99
- return cls(session=session, config=config, _owns_session=owns_session)
86
+ return cls(
87
+ session=session,
88
+ config=config,
89
+ loop=loop,
90
+ owns_session=owns_session,
91
+ )
100
92
 
101
93
  async def aclose(self) -> None:
102
94
  if self._owns_session:
@@ -110,7 +102,6 @@ class AsyncTransport(Transport):
110
102
 
111
103
  @staticmethod
112
104
  def _build_response(response: aiohttp.ClientResponse, body: bytes) -> Response:
113
- """Convert an aiohttp response into a requests.Response for zeep."""
114
105
  r = Response()
115
106
  r.status_code = response.status
116
107
  r._content = body # noqa: SLF001
@@ -127,116 +118,112 @@ class AsyncTransport(Transport):
127
118
 
128
119
  return r
129
120
 
121
+ async def _fetch(self, url: str) -> bytes:
122
+ async with self._session.get(
123
+ url,
124
+ proxy=self._config.proxy,
125
+ timeout=ClientTimeout(total=self._config.timeout),
126
+ ) as resp:
127
+ body = await resp.read()
128
+
129
+ if resp.status >= 400:
130
+ raise TransportError(
131
+ status_code=resp.status,
132
+ message=body.decode(errors='ignore'),
133
+ )
134
+
135
+ return body
136
+
130
137
  def load(self, url: str) -> bytes:
131
138
  """
132
- Sync entry-point zeep calls during WSDL document init.
139
+ Called synchronously by zeep during WSDL parsing.
133
140
 
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.
141
+ We safely hop into the main loop.
136
142
  """
137
143
  if not url:
138
144
  return b''
139
145
 
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
- async def post(
167
- self,
168
- address: str,
169
- message: bytes,
170
- headers: dict[str, str],
171
- *,
172
- timeout: int | None = None,
173
- ) -> Response:
174
- logger.debug('SOAP POST → %s\n%s', address, message)
146
+ future = asyncio.run_coroutine_threadsafe(
147
+ self._fetch(url),
148
+ self._loop,
149
+ )
175
150
 
176
- async with self._session.post(
177
- address,
178
- data=message,
179
- headers=headers,
180
- proxy=self._config.proxy,
181
- timeout=ClientTimeout(total=timeout) if timeout is not None else None,
182
- ) as resp:
183
- body = await resp.read()
184
- logger.debug('SOAP ← %s (HTTP %d)\n%s', address, resp.status, body)
185
- return self._build_response(resp, body)
151
+ return future.result()
186
152
 
187
153
  def post_xml(
188
- self,
189
- address: str,
190
- envelope: Any,
191
- headers: dict[str, str],
154
+ self,
155
+ address: str,
156
+ envelope: Any,
157
+ headers: dict[str, str],
192
158
  ) -> Response:
193
- """
194
- Sync entry-point zeep calls from a thread executor during send_async.
195
- Bridges back into the running event loop via run_coroutine_threadsafe.
196
- """
197
- loop = asyncio.get_event_loop()
198
159
  future = asyncio.run_coroutine_threadsafe(
199
160
  self.post(address, etree_to_string(envelope), headers),
200
- loop,
161
+ self._loop,
201
162
  )
163
+
202
164
  return future.result()
203
165
 
166
+ async def post(
167
+ self,
168
+ address: str,
169
+ message: bytes,
170
+ headers: dict[str, str],
171
+ *,
172
+ timeout: int | None = None,
173
+ ) -> Response:
174
+ logger.debug('SOAP POST → %s', address)
175
+
176
+ async with self._session.post(
177
+ address,
178
+ data=message,
179
+ headers=headers,
180
+ proxy=self._config.proxy,
181
+ timeout=ClientTimeout(total=timeout) if timeout is not None else None,
182
+ ) as resp:
183
+ body = await resp.read()
184
+ logger.debug('SOAP ← %s (%d)', address, resp.status)
185
+ return self._build_response(resp, body)
186
+
204
187
  async def get(
205
- self,
206
- address: str,
207
- params: dict[str, str],
208
- headers: dict[str, str],
188
+ self,
189
+ address: str,
190
+ params: dict[str, str],
191
+ headers: dict[str, str],
209
192
  ) -> Response:
210
193
  async with self._session.get(
211
- address,
212
- params=params,
213
- headers=headers,
214
- proxy=self._config.proxy,
194
+ address,
195
+ params=params,
196
+ headers=headers,
197
+ proxy=self._config.proxy,
215
198
  ) as resp:
216
199
  body = await resp.read()
217
200
  return self._build_response(resp, body)
218
201
 
219
202
 
220
203
  def build_soap_client(
221
- wsdl_url: str,
222
- transport: AsyncTransport,
223
- plugins: Sequence[Plugin] | None = None,
204
+ wsdl_url: str,
205
+ transport: AsyncTransport,
206
+ plugins: Sequence[Plugin] | None = None,
224
207
  ) -> AsyncClient:
225
208
  if not wsdl_url:
226
209
  msg = 'wsdl_url must be a non-empty string.'
227
210
 
228
211
  raise ValueError(msg)
229
212
 
230
- return AsyncClient(wsdl_url, transport=transport, plugins=list(plugins or []))
213
+ return AsyncClient(
214
+ wsdl_url,
215
+ transport=transport,
216
+ plugins=list(plugins or []),
217
+ )
231
218
 
232
219
 
233
220
  @asynccontextmanager
234
221
  async def soap_client(
235
- wsdl_url: str,
236
- *,
237
- config: TransportConfig | None = None,
238
- session: ClientSession | None = None,
239
- plugins: Sequence[Plugin] | None = None,
222
+ wsdl_url: str,
223
+ *,
224
+ config: TransportConfig | None = None,
225
+ session: ClientSession | None = None,
226
+ plugins: Sequence[Plugin] | None = None,
240
227
  ) -> AsyncIterator[AsyncClient]:
241
228
  """
242
229
  Async context manager yielding a ready-to-use zeep AsyncClient.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.18.7
3
+ Version: 0.18.8
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