soapix 0.1.1__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 (45) hide show
  1. soapix-0.1.1/.github/workflows/publish.yml +19 -0
  2. soapix-0.1.1/.github/workflows/test.yml +28 -0
  3. soapix-0.1.1/.gitignore +40 -0
  4. soapix-0.1.1/LICENSE +21 -0
  5. soapix-0.1.1/Makefile +93 -0
  6. soapix-0.1.1/PKG-INFO +417 -0
  7. soapix-0.1.1/README.md +385 -0
  8. soapix-0.1.1/pyproject.toml +61 -0
  9. soapix-0.1.1/soapix/__init__.py +14 -0
  10. soapix-0.1.1/soapix/__init__.pyi +6 -0
  11. soapix-0.1.1/soapix/cache.py +115 -0
  12. soapix-0.1.1/soapix/client.py +243 -0
  13. soapix-0.1.1/soapix/client.pyi +68 -0
  14. soapix-0.1.1/soapix/docs/__init__.py +0 -0
  15. soapix-0.1.1/soapix/docs/examples.py +91 -0
  16. soapix-0.1.1/soapix/docs/exporters.py +261 -0
  17. soapix-0.1.1/soapix/docs/generator.py +69 -0
  18. soapix-0.1.1/soapix/docs/resolver.py +60 -0
  19. soapix-0.1.1/soapix/docs/terminal.py +109 -0
  20. soapix-0.1.1/soapix/exceptions.py +113 -0
  21. soapix-0.1.1/soapix/py.typed +0 -0
  22. soapix-0.1.1/soapix/transport.py +183 -0
  23. soapix-0.1.1/soapix/wsdl/__init__.py +0 -0
  24. soapix-0.1.1/soapix/wsdl/namespace.py +59 -0
  25. soapix-0.1.1/soapix/wsdl/parser.py +553 -0
  26. soapix-0.1.1/soapix/wsdl/resolver.py +148 -0
  27. soapix-0.1.1/soapix/wsdl/types.py +118 -0
  28. soapix-0.1.1/soapix/xml/__init__.py +0 -0
  29. soapix-0.1.1/soapix/xml/builder.py +225 -0
  30. soapix-0.1.1/soapix/xml/parser.py +213 -0
  31. soapix-0.1.1/tests/__init__.py +0 -0
  32. soapix-0.1.1/tests/fixtures/any_type.wsdl +66 -0
  33. soapix-0.1.1/tests/fixtures/deep_import.wsdl +66 -0
  34. soapix-0.1.1/tests/fixtures/namespace_quirks.wsdl +64 -0
  35. soapix-0.1.1/tests/fixtures/simple.wsdl +107 -0
  36. soapix-0.1.1/tests/fixtures/soap12.wsdl +64 -0
  37. soapix-0.1.1/tests/fixtures/wrapped.wsdl +76 -0
  38. soapix-0.1.1/tests/test_cache.py +209 -0
  39. soapix-0.1.1/tests/test_docs.py +235 -0
  40. soapix-0.1.1/tests/test_exceptions.py +97 -0
  41. soapix-0.1.1/tests/test_namespace.py +73 -0
  42. soapix-0.1.1/tests/test_tolerant_engine.py +227 -0
  43. soapix-0.1.1/tests/test_wsdl_parser.py +112 -0
  44. soapix-0.1.1/tests/test_xml_builder.py +84 -0
  45. soapix-0.1.1/tests/test_xml_parser.py +101 -0
@@ -0,0 +1,19 @@
1
+ steps:
2
+ - uses: actions/checkout@v4
3
+
4
+ - name: Set up Python
5
+ uses: actions/setup-python@v5
6
+ with:
7
+ python-version: "3.12"
8
+
9
+ - name: Install dependencies
10
+ run: pip install -e ".[dev]"
11
+
12
+ - name: Run tests
13
+ run: pytest
14
+
15
+ - name: Build package
16
+ run: pip install build && python -m build
17
+
18
+ - name: Publish to PyPI
19
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Run tests
28
+ run: pytest tests/
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+
8
+ # Distribution / packaging
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ *.egg
13
+ MANIFEST
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+ env/
19
+ .env
20
+
21
+ # Testing & coverage
22
+ .coverage
23
+ .coverage.*
24
+ htmlcov/
25
+ .pytest_cache/
26
+ .tox/
27
+
28
+ # Type checking
29
+ .mypy_cache/
30
+
31
+ # Editor / IDE
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+ .DS_Store
37
+
38
+ # Generated docs output
39
+ api_docs.md
40
+ api_docs.html
soapix-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 soapix contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
soapix-0.1.1/Makefile ADDED
@@ -0,0 +1,93 @@
1
+ # =====================================
2
+ # ⚙️ OPM - Odoo Plugin Manager (CLI)
3
+ # =====================================
4
+
5
+ # ----------------------
6
+ # 🔧 Configurable values
7
+ # ----------------------
8
+
9
+ ifneq (,$(wildcard .env))
10
+ include .env
11
+ export $(shell sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' .env)
12
+ endif
13
+
14
+ PYTHON := python3
15
+ PACKAGE := soapix
16
+ DIST_DIR := dist
17
+
18
+ # PyPI settings
19
+ PYPI_REPO := https://upload.pypi.org/legacy/
20
+ TEST_REPO := https://test.pypi.org/legacy/
21
+
22
+ # Version extraction from pyproject.toml
23
+ VERSION := $(shell sed -n 's/^version = "\([^"]*\)".*/\1/p' pyproject.toml | head -n1)
24
+
25
+ # ----------------------
26
+ # 🎯 Default target
27
+ # ----------------------
28
+ help:
29
+ @echo ""
30
+ @echo "🧩 OPM Build Commands:"
31
+ @echo " make build → Build wheel and sdist"
32
+ @echo " make clean → Remove temporary build files"
33
+ @echo " make version → Show current version"
34
+ @echo " make publish → Upload to PyPI"
35
+ @echo " make testpublish → Upload to TestPyPI"
36
+ @echo " make bump → Bump patch version (auto-increment)"
37
+ @echo " make release → Bump version, build, and publish to PyPI"
38
+ @echo ""
39
+
40
+ # ----------------------
41
+ # 🧹 Clean (remove build artifacts)
42
+ # ----------------------
43
+ clean:
44
+ @echo "🧹 Cleaning build artifacts..."
45
+ rm -rf $(DIST_DIR) build *.egg-info
46
+ @echo "✅ Clean complete."
47
+
48
+ # ----------------------
49
+ # 🔢 Bump version (auto-increment patch)
50
+ # ----------------------
51
+ bump:
52
+ @echo "🔢 Bumping patch version..."
53
+ @old=$$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' pyproject.toml | head -n1); \
54
+ if [ -z "$$old" ]; then echo "❌ Version not found in pyproject.toml"; exit 1; fi; \
55
+ new=$$(python3 -c "parts='$$old'.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))"); \
56
+ if [ "$$(uname)" = "Darwin" ]; then \
57
+ sed -i '' -E "s/^([[:space:]]*version[[:space:]]*=[[:space:]]*)\"$$old\"/\1\"$$new\"/" pyproject.toml; \
58
+ else \
59
+ sed -i -E "s/^([[:space:]]*version[[:space:]]*=[[:space:]]*)\"$$old\"/\1\"$$new\"/" pyproject.toml; \
60
+ fi; \
61
+ echo "✅ New version: $$new"
62
+
63
+ # ----------------------
64
+ # 🏗️ Build (create wheel and sdist)
65
+ # ----------------------
66
+ build: clean
67
+ @echo "🏗️ Building $(PACKAGE) (version: $(VERSION))..."
68
+ PIP_CONFIG_FILE=/dev/null $(PYTHON) -m build
69
+ @echo "✅ Build complete. Artifacts in $(DIST_DIR)/"
70
+
71
+ # ----------------------
72
+ # 🚀 Publish (PyPI)
73
+ # ----------------------
74
+ publish: build
75
+ @echo "🚀 Publishing $(PACKAGE) $(VERSION) to PyPI..."
76
+ $(PYTHON) -m twine upload --repository-url $(PYPI_REPO) $(DIST_DIR)/*
77
+ @echo "✅ Published: https://pypi.org/project/$(PACKAGE)/$(VERSION)/"
78
+
79
+ # ----------------------
80
+ # 🧪 Test Publish (TestPyPI)
81
+ # ----------------------
82
+ testpublish:
83
+ @echo "🧪 Publishing $(PACKAGE) $(VERSION) to TestPyPI..."
84
+ $(PYTHON) -m twine upload --repository-url $(TEST_REPO) $(DIST_DIR)/*
85
+ @echo "✅ TestPyPI upload complete: https://test.pypi.org/project/$(PACKAGE)/$(VERSION)/"
86
+
87
+ # ----------------------
88
+ # 🔢 Version
89
+ # ----------------------
90
+ version:
91
+ @echo "📦 Current version: $(VERSION)"
92
+
93
+ release: bump publish
soapix-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,417 @@
1
+ Metadata-Version: 2.4
2
+ Name: soapix
3
+ Version: 0.1.1
4
+ Summary: A tolerant, self-documenting Python SOAP client library
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: client,soap,webservice,wsdl,xml
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Internet :: WWW/HTTP
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: lxml>=5.0.0
20
+ Requires-Dist: pydantic>=2.0.0
21
+ Requires-Dist: rich>=13.0.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.0.0; extra == 'dev'
24
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
26
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
27
+ Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
30
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # soapix
34
+
35
+ A tolerant, self-documenting Python SOAP client library.
36
+
37
+ soapix is designed to work with real-world SOAP services that don't perfectly follow the spec — handling namespace quirks, loose validation, and unclear error messages that break other libraries.
38
+
39
+ ## Features
40
+
41
+ - **Tolerant validation** — optional fields can be omitted; required `None` fields send `xsi:nil` instead of crashing
42
+ - **Namespace tolerance** — trailing slashes, case differences, and URI fragments are normalized automatically
43
+ - **Auto-documentation** — generates terminal, Markdown, and HTML docs directly from the WSDL
44
+ - **Meaningful errors** — exceptions include service name, method, endpoint, sent payload, and a human-readable hint
45
+ - **Async support** — native `AsyncSoapClient` with `async/await`
46
+ - **WSDL caching** — in-memory and file-based caching with TTL
47
+ - **Retry & timeout** — configurable per-client
48
+ - **Type stubs** — full `.pyi` stubs for IDE autocomplete
49
+
50
+ ## Requirements
51
+
52
+ - Python 3.10+
53
+ - Dependencies: `httpx`, `lxml`, `rich`, `pydantic`
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install soapix
59
+ ```
60
+
61
+ ## Quick Start
62
+
63
+ ```python
64
+ from soapix import SoapClient
65
+
66
+ client = SoapClient("http://service.example.com/?wsdl")
67
+ result = client.service.GetUser(userId=123)
68
+ print(result["name"])
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Configuration
74
+
75
+ All options are keyword-only and passed to the constructor.
76
+
77
+ ```python
78
+ client = SoapClient(
79
+ "http://service.example.com/?wsdl",
80
+ timeout=60.0, # HTTP timeout in seconds (default: 30.0)
81
+ retries=3, # Retry count on transient failures (default: 0)
82
+ strict=False, # Strict WSDL validation (default: False)
83
+ debug=True, # Print request/response XML to terminal (default: False)
84
+ cache=None, # Cache instance, or None to disable (default: MemoryCache)
85
+ )
86
+ ```
87
+
88
+ | Option | Type | Default | Description |
89
+ |--------|------|---------|-------------|
90
+ | `timeout` | `float` | `30.0` | HTTP request timeout in seconds |
91
+ | `retries` | `int` | `0` | Number of retries on connection/timeout errors |
92
+ | `strict` | `bool` | `False` | If `True`, raises on missing required fields instead of sending `xsi:nil` |
93
+ | `debug` | `bool` | `False` | Prints colourised request and response XML to the terminal |
94
+ | `cache` | `Cache \| None` | `MemoryCache` | WSDL parse cache; `None` disables caching |
95
+ | `verify` | `bool \| str` | `True` | SSL verification: `True` (system certs), `False` (skip), or path to a CA bundle file |
96
+ | `auth` | `tuple \| None` | `None` | HTTP Basic Auth credentials as `(username, password)` |
97
+
98
+ ---
99
+
100
+ ## Calling Operations
101
+
102
+ Operations are accessed through `client.service.<OperationName>(...)` using keyword arguments that match the WSDL parameter names.
103
+
104
+ ```python
105
+ # Simple call
106
+ result = client.service.GetUser(userId=42)
107
+
108
+ # Multiple parameters
109
+ result = client.service.CreateUser(name="Alice", email="alice@example.com")
110
+
111
+ # Optional parameters can be omitted in tolerant mode (default)
112
+ result = client.service.GetUser(userId=42) # locale is optional — omitted
113
+ result = client.service.GetUser(userId=42, locale="en-US") # or explicitly passed
114
+
115
+ # Required field sent as None → xsi:nil in tolerant mode
116
+ result = client.service.GetUser(userId=None)
117
+ ```
118
+
119
+ The return value is a plain Python `dict` (or scalar for leaf values). Nested elements become nested dicts; repeated elements become lists.
120
+
121
+ ```python
122
+ result = client.service.GetUser(userId=1)
123
+ # {"userId": 1, "name": "Alice", "email": "alice@example.com", "active": True}
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Async Client
129
+
130
+ ```python
131
+ import asyncio
132
+ from soapix import AsyncSoapClient
133
+
134
+ async def main():
135
+ async with AsyncSoapClient("http://service.example.com/?wsdl") as client:
136
+ result = await client.service.GetUser(userId=123)
137
+ print(result["name"])
138
+
139
+ asyncio.run(main())
140
+ ```
141
+
142
+ `AsyncSoapClient` accepts the same options as `SoapClient`. Use it as an async context manager to ensure the underlying HTTP connection is properly closed.
143
+
144
+ ---
145
+
146
+ ## Strict Mode
147
+
148
+ By default, soapix operates in **tolerant mode**: missing required fields send `xsi:nil`, and unknown namespaces are silently normalised. Enable **strict mode** to raise exceptions instead:
149
+
150
+ ```python
151
+ client = SoapClient("http://service.example.com/?wsdl", strict=True)
152
+
153
+ # Raises SerializationError if a required field is missing
154
+ client.service.GetUser() # userId is required → raises
155
+ client.service.GetUser(userId=None) # None on required field → raises
156
+ ```
157
+
158
+ ---
159
+
160
+ ## SSL & Authentication
161
+
162
+ ### SSL verification
163
+
164
+ ```python
165
+ # Default — uses system certificate store
166
+ client = SoapClient("https://service.example.com/?wsdl")
167
+
168
+ # Custom CA bundle (corporate / self-signed certificates)
169
+ client = SoapClient("https://service.example.com/?wsdl", verify="/path/to/ca-bundle.pem")
170
+
171
+ # Disable SSL verification — development only, not recommended for production
172
+ client = SoapClient("https://service.example.com/?wsdl", verify=False)
173
+ ```
174
+
175
+ To obtain the server's CA certificate:
176
+
177
+ ```bash
178
+ openssl s_client -connect service.example.com:443 -showcerts 2>/dev/null \
179
+ | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > ca.pem
180
+ ```
181
+
182
+ ### HTTP Basic Auth
183
+
184
+ Some services require credentials to access the WSDL itself and/or to call operations. Pass `auth` as a `(username, password)` tuple — it applies to both WSDL fetching and all SOAP calls:
185
+
186
+ ```python
187
+ client = SoapClient(
188
+ "https://service.example.com/?wsdl",
189
+ auth=("username", "password"),
190
+ )
191
+ ```
192
+
193
+ ### Combined
194
+
195
+ ```python
196
+ client = SoapClient(
197
+ "https://service.example.com/?wsdl",
198
+ verify="/path/to/ca-bundle.pem",
199
+ auth=("username", "password"),
200
+ )
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Debug Mode
206
+
207
+ Enable `debug=True` to print the full SOAP envelope sent and the raw XML response to the terminal, with syntax highlighting via `rich`.
208
+
209
+ ```python
210
+ client = SoapClient("http://service.example.com/?wsdl", debug=True)
211
+ client.service.GetUser(userId=1)
212
+ # ── REQUEST ──────────────────────────────
213
+ # POST http://service.example.com/
214
+ # SOAPAction: "..."
215
+ #
216
+ # <?xml version="1.0" ...>
217
+ # <soap:Envelope ...>
218
+ # ...
219
+ # </soap:Envelope>
220
+ #
221
+ # ── RESPONSE (200 OK, 42ms) ──────────────
222
+ # <?xml version="1.0" ...>
223
+ # ...
224
+ ```
225
+
226
+ ---
227
+
228
+ ## WSDL Caching
229
+
230
+ soapix caches parsed WSDL documents to avoid re-fetching and re-parsing on every instantiation.
231
+
232
+ ### MemoryCache (default)
233
+
234
+ ```python
235
+ from soapix.cache import MemoryCache
236
+
237
+ cache = MemoryCache(
238
+ ttl=300, # Seconds until entries expire (default: 300, None = no expiry)
239
+ maxsize=64, # Max entries before oldest is evicted (default: 64)
240
+ )
241
+
242
+ client = SoapClient("http://service.example.com/?wsdl", cache=cache)
243
+ ```
244
+
245
+ A module-level default cache is shared across all `SoapClient` instances that don't specify one. Use `get_default_cache()` to access it:
246
+
247
+ ```python
248
+ from soapix.cache import get_default_cache
249
+ get_default_cache().clear() # flush all cached WSDLs
250
+ ```
251
+
252
+ ### FileCache
253
+
254
+ `FileCache` persists parsed WSDL documents to disk using `pickle`. Useful across process restarts.
255
+
256
+ ```python
257
+ from soapix.cache import FileCache
258
+
259
+ cache = FileCache(
260
+ cache_dir=".soapix_cache", # Directory to store cache files (created if absent)
261
+ ttl=3600, # Seconds until entries expire (default: 3600)
262
+ )
263
+
264
+ client = SoapClient("http://service.example.com/?wsdl", cache=cache)
265
+ ```
266
+
267
+ ### Disable caching
268
+
269
+ ```python
270
+ client = SoapClient("http://service.example.com/?wsdl", cache=None)
271
+ # WSDL is fetched and parsed on every instantiation
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Auto-Documentation
277
+
278
+ soapix can generate human-readable documentation from the WSDL — terminal output, Markdown, or HTML.
279
+
280
+ ### Terminal
281
+
282
+ ```python
283
+ client.docs()
284
+ # Prints a formatted table with all operations, parameters, types, and example calls.
285
+ ```
286
+
287
+ ### Markdown
288
+
289
+ ```python
290
+ # Returns a string
291
+ md = client.docs(output="markdown")
292
+
293
+ # Writes to a file
294
+ client.docs(output="markdown", path="api_docs.md")
295
+ ```
296
+
297
+ ### HTML
298
+
299
+ ```python
300
+ # Returns a string
301
+ html = client.docs(output="html")
302
+
303
+ # Writes to a file (includes a search box)
304
+ client.docs(output="html", path="api_docs.html")
305
+ ```
306
+
307
+ You can also use `DocsGenerator` directly if you have a parsed WSDL document:
308
+
309
+ ```python
310
+ from soapix.docs.generator import DocsGenerator
311
+ from soapix.wsdl.parser import WsdlParser
312
+
313
+ doc = WsdlParser().load("service.wsdl")
314
+ gen = DocsGenerator(doc)
315
+ gen.render(output="terminal")
316
+ gen.render(output="markdown", path="api_docs.md")
317
+ gen.render(output="html", path="api_docs.html")
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Error Handling
323
+
324
+ soapix raises structured exceptions with actionable context.
325
+
326
+ ```python
327
+ from soapix.exceptions import (
328
+ SoapFaultError, # Server returned a soap:Fault
329
+ HttpError, # HTTP 4xx/5xx or connection failure
330
+ TimeoutError, # Request exceeded the timeout
331
+ SerializationError, # Python value could not be serialised to XML
332
+ WsdlParseError, # WSDL could not be read or parsed
333
+ WsdlNotFoundError, # WSDL URL or path not reachable
334
+ WsdlImportError, # xs:import could not be resolved
335
+ )
336
+ ```
337
+
338
+ ### Exception hierarchy
339
+
340
+ ```
341
+ SoapixError
342
+ ├── WsdlParseError
343
+ │ ├── WsdlNotFoundError
344
+ │ └── WsdlImportError
345
+ ├── SoapCallError
346
+ │ ├── SoapFaultError
347
+ │ ├── HttpError
348
+ │ └── TimeoutError
349
+ └── SerializationError
350
+ ```
351
+
352
+ ### Catching errors
353
+
354
+ ```python
355
+ from soapix.exceptions import SoapFaultError, HttpError, TimeoutError
356
+
357
+ try:
358
+ result = client.service.GetUser(userId=999)
359
+ except SoapFaultError as e:
360
+ print(e.fault_code) # e.g. "Server"
361
+ print(e.fault_string) # e.g. "User not found"
362
+ print(e.detail) # raw XML detail block, if any
363
+ except TimeoutError:
364
+ print("Request timed out — increase timeout or check the endpoint")
365
+ except HttpError as e:
366
+ print(f"HTTP error: {e}")
367
+ ```
368
+
369
+
370
+ Error messages include structured context:
371
+
372
+ ```
373
+ 'GetUser' call failed
374
+
375
+ Service : UserService
376
+ Method : GetUser
377
+ Endpoint : http://service.example.com/
378
+ Sent : {'userId': None}
379
+
380
+ Hint : userId is required (int) — None cannot be sent in strict mode
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Retry & Timeout
386
+
387
+ ```python
388
+ client = SoapClient(
389
+ "http://service.example.com/?wsdl",
390
+ timeout=10.0, # fail fast
391
+ retries=3, # retry up to 3 times on connection/timeout errors
392
+ )
393
+ ```
394
+
395
+ Retries apply to `HttpError` (connection failures) and `TimeoutError`. Server-side SOAP faults (`SoapFaultError`) are not retried.
396
+
397
+ ---
398
+
399
+ ## Comparison
400
+
401
+ | Feature | Zeep | Suds | soapix |
402
+ |---------|------|------|--------|
403
+ | Tolerant validation | No | No | Yes |
404
+ | Namespace tolerance | Partial | Partial | Full |
405
+ | Meaningful errors | No | No | Yes |
406
+ | Auto documentation | No | No | Yes |
407
+ | Async support | Partial | No | Native |
408
+ | WSDL caching | No | No | Yes |
409
+ | Retry & timeout | Manual | Manual | Built-in |
410
+ | Type stubs | Partial | No | Yes |
411
+ | Python 3.10+ | Yes | No | Yes |
412
+
413
+ ---
414
+
415
+ ## License
416
+
417
+ MIT — see [LICENSE](LICENSE).