soapbar 0.5.0__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 (34) hide show
  1. soapbar-0.5.0/PKG-INFO +1064 -0
  2. soapbar-0.5.0/README.md +1028 -0
  3. soapbar-0.5.0/pyproject.toml +80 -0
  4. soapbar-0.5.0/setup.cfg +4 -0
  5. soapbar-0.5.0/src/soapbar/__init__.py +143 -0
  6. soapbar-0.5.0/src/soapbar/client/__init__.py +7 -0
  7. soapbar-0.5.0/src/soapbar/client/client.py +257 -0
  8. soapbar-0.5.0/src/soapbar/client/transport.py +104 -0
  9. soapbar-0.5.0/src/soapbar/core/__init__.py +41 -0
  10. soapbar-0.5.0/src/soapbar/core/binding.py +657 -0
  11. soapbar-0.5.0/src/soapbar/core/envelope.py +407 -0
  12. soapbar-0.5.0/src/soapbar/core/fault.py +274 -0
  13. soapbar-0.5.0/src/soapbar/core/mtom.py +239 -0
  14. soapbar-0.5.0/src/soapbar/core/namespaces.py +58 -0
  15. soapbar-0.5.0/src/soapbar/core/types.py +549 -0
  16. soapbar-0.5.0/src/soapbar/core/wsdl/__init__.py +117 -0
  17. soapbar-0.5.0/src/soapbar/core/wsdl/builder.py +211 -0
  18. soapbar-0.5.0/src/soapbar/core/wsdl/parser.py +437 -0
  19. soapbar-0.5.0/src/soapbar/core/wssecurity.py +895 -0
  20. soapbar-0.5.0/src/soapbar/core/xml.py +208 -0
  21. soapbar-0.5.0/src/soapbar/py.typed +0 -0
  22. soapbar-0.5.0/src/soapbar/server/__init__.py +15 -0
  23. soapbar-0.5.0/src/soapbar/server/application.py +528 -0
  24. soapbar-0.5.0/src/soapbar/server/asgi.py +111 -0
  25. soapbar-0.5.0/src/soapbar/server/service.py +123 -0
  26. soapbar-0.5.0/src/soapbar/server/wsgi.py +76 -0
  27. soapbar-0.5.0/src/soapbar.egg-info/PKG-INFO +1064 -0
  28. soapbar-0.5.0/src/soapbar.egg-info/SOURCES.txt +32 -0
  29. soapbar-0.5.0/src/soapbar.egg-info/dependency_links.txt +1 -0
  30. soapbar-0.5.0/src/soapbar.egg-info/requires.txt +17 -0
  31. soapbar-0.5.0/src/soapbar.egg-info/top_level.txt +1 -0
  32. soapbar-0.5.0/tests/test_interop.py +244 -0
  33. soapbar-0.5.0/tests/test_real_wsdls.py +87 -0
  34. soapbar-0.5.0/tests/test_soapbar.py +6809 -0
soapbar-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,1064 @@
1
+ Metadata-Version: 2.4
2
+ Name: soapbar
3
+ Version: 0.5.0
4
+ Summary: A SOAP framework for Python — client, server, and WSDL handling.
5
+ Author: Hitoshi Yamamoto
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/hitoshyamamoto/soapbar
8
+ Project-URL: Issues, https://github.com/hitoshyamamoto/soapbar/issues
9
+ Keywords: soap,wsdl,web-services,xml,rpc
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: lxml>=5.0
25
+ Provides-Extra: core
26
+ Provides-Extra: server
27
+ Provides-Extra: client
28
+ Requires-Dist: httpx>=0.27; extra == "client"
29
+ Provides-Extra: security
30
+ Requires-Dist: signxml>=3.0; extra == "security"
31
+ Requires-Dist: cryptography>=41.0; extra == "security"
32
+ Provides-Extra: all
33
+ Requires-Dist: httpx>=0.27; extra == "all"
34
+ Requires-Dist: signxml>=3.0; extra == "all"
35
+ Requires-Dist: cryptography>=41.0; extra == "all"
36
+
37
+ # soapbar
38
+
39
+ ![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)
40
+ ![License](https://img.shields.io/badge/license-MIT%20with%20Attribution-green)
41
+ ![Conformance](https://img.shields.io/badge/SOAP%20conformance-100%25-brightgreen)
42
+
43
+ A SOAP framework for Python — client, server, and WSDL handling.
44
+
45
+ soapbar implements SOAP 1.1 and 1.2 with all five binding styles, auto-generates WSDL from Python service classes, parses existing WSDL to drive a typed client, and integrates with any ASGI or WSGI framework via thin adapter classes. The XML parser is hardened against XXE attacks using lxml with `resolve_entities=False`.
46
+
47
+ > **Conformance** — soapbar v0.4.2 passes a full SOAP Protocol Conformance Audit at **100% (46/46 checkpoints)**. All F01–F09 original findings, G01–G11 gap findings, I01–I04 informational observations, and S10 (WS-I BSP X.509 token profile) are resolved.
48
+
49
+ ---
50
+
51
+ ## Table of Contents
52
+
53
+ 1. [Features](#features)
54
+ 2. [Installation](#installation)
55
+ 3. [Quick start — server](#quick-start--server)
56
+ 4. [Binding styles and SOAP encoding](#binding-styles-and-soap-encoding)
57
+ 5. [Defining a service](#defining-a-service)
58
+ 6. [SOAP versions](#soap-versions)
59
+ 7. [Framework compatibility](#framework-compatibility)
60
+ 8. [WSDL](#wsdl)
61
+ 9. [Client](#client)
62
+ 10. [XSD type system](#xsd-type-system)
63
+ 11. [Fault handling](#fault-handling)
64
+ 12. [Security](#security)
65
+ 13. [WS-Security — UsernameToken](#ws-security--usernametoken)
66
+ 14. [MTOM/XOP](#mtomxop)
67
+ 15. [XML Signature and Encryption](#xml-signature-and-encryption)
68
+ 16. [WSDL schema validation](#wsdl-schema-validation)
69
+ 17. [One-way operations](#one-way-operations)
70
+ 18. [SOAP array attributes](#soap-array-attributes)
71
+ 19. [rpc:result (SOAP 1.2)](#rpcresult-soap-12)
72
+ 20. [Interoperability](#interoperability)
73
+ 21. [Architecture](#architecture)
74
+ 22. [Public API](#public-api)
75
+ 23. [Comparison with alternatives](#comparison-with-alternatives)
76
+ 24. [Development setup](#development-setup)
77
+ 25. [Inspired by](#inspired-by)
78
+ 26. [Learn more](#learn-more)
79
+ 27. [Known Limitations](#known-limitations)
80
+ 28. [License](#license)
81
+
82
+ ---
83
+
84
+ ## Features
85
+
86
+ - SOAP 1.1 and 1.2 (auto-detected from envelope namespace; fault codes auto-translated)
87
+ - All 5 WSDL/SOAP binding style combinations (RPC/Encoded, RPC/Literal, Document/Literal, Document/Literal/Wrapped, Document/Encoded)
88
+ - Auto-generates WSDL from service class definitions — no config files needed
89
+ - Parses existing WSDL to drive a typed client
90
+ - ASGI adapter (`AsgiSoapApp`) and WSGI adapter (`WsgiSoapApp`)
91
+ - XXE-safe hardened XML parser (lxml, `resolve_entities=False`, `no_network=True`, `load_dtd=False`)
92
+ - Message size limit (10 MB default) and XML nesting depth limit (100 levels) — DoS protection
93
+ - **WS-Security UsernameToken** — PasswordText and PasswordDigest (SHA-1) on both client and server
94
+ - **XML Signature** — enveloped XML-DSIG signing and verification (`sign_envelope` / `verify_envelope`, requires `signxml`)
95
+ - **XML Encryption** — AES-256-CBC body encryption with RSA-OAEP session-key wrapping (`encrypt_body` / `decrypt_body`, requires `cryptography`)
96
+ - **MTOM/XOP** — send and receive SOAP messages with binary attachments; `SoapClient(use_mtom=True)` + `add_attachment()`; server decodes inbound MTOM automatically
97
+ - **WSDL schema validation** — opt-in Body validation against WSDL-embedded XSD types (`SoapApplication(validate_body_schema=True)`)
98
+ - **One-way MEP** — `@soap_operation(one_way=True)` returns HTTP 202 with empty body
99
+ - **SOAP array attributes** — `enc:itemType`/`enc:arraySize` (SOAP 1.2) and `SOAP-ENC:arrayType` (SOAP 1.1) emitted automatically
100
+ - **Multi-reference encoding** — shared complex objects serialized with `id`/`href` per SOAP 1.1 §5.2.5
101
+ - **rpc:result** — opt-in `@soap_operation(emit_rpc_result=True)` per SOAP 1.2 Part 2 §4.2.1
102
+ - WS-Addressing 1.0 — MessageID, RelatesTo, Action, ReferenceParameters propagated in responses
103
+ - XSD type registry with 27 built-in types
104
+ - Sync and async HTTP client (httpx optional)
105
+ - Interoperable with zeep and spyne out-of-the-box (verified by integration tests)
106
+ - **JSON dual-mode** — any SOAP endpoint returns JSON when client sends `Accept: application/json`; no separate endpoint needed
107
+ - **Non-strict WSDL parsing** — `parse_wsdl(..., strict=False)` silently skips unresolvable imports instead of raising
108
+ - Full type annotations + `py.typed` marker (PEP 561)
109
+ - Python 3.10 – 3.14
110
+
111
+ ---
112
+
113
+ ## Installation
114
+
115
+ ```bash
116
+ pip install soapbar # core + server + WSDL (lxml only)
117
+ pip install soapbar[core] # explicit alias for the above
118
+ pip install soapbar[server] # explicit alias for the above
119
+ pip install soapbar[client] # + httpx for the HTTP client
120
+ pip install soapbar[security] # + signxml + cryptography (XML Sig/Enc)
121
+ pip install soapbar[all] # everything (client + security)
122
+ ```
123
+
124
+ Or with uv:
125
+
126
+ ```bash
127
+ uv add soapbar
128
+ uv add "soapbar[client]"
129
+ uv add "soapbar[security]"
130
+ uv add "soapbar[all]"
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Quick start — server
136
+
137
+ ### Variant A — standalone (bare ASGI, no framework)
138
+
139
+ ```python
140
+ # app.py
141
+ from soapbar import SoapService, soap_operation, SoapApplication, AsgiSoapApp
142
+
143
+
144
+ class CalculatorService(SoapService):
145
+ __service_name__ = "Calculator"
146
+ __tns__ = "http://example.com/calculator"
147
+
148
+ @soap_operation()
149
+ def add(self, a: int, b: int) -> int:
150
+ return a + b
151
+
152
+ @soap_operation()
153
+ def subtract(self, a: int, b: int) -> int:
154
+ return a - b
155
+
156
+
157
+ soap_app = SoapApplication(service_url="http://localhost:8000")
158
+ soap_app.register(CalculatorService())
159
+
160
+ app = AsgiSoapApp(soap_app)
161
+ # Run: uvicorn app:app --port 8000
162
+ # WSDL: GET http://localhost:8000?wsdl
163
+ ```
164
+
165
+ ### Variant B — mounted inside FastAPI
166
+
167
+ ```python
168
+ from fastapi import FastAPI
169
+ from soapbar import SoapApplication, AsgiSoapApp
170
+
171
+ # ... (same CalculatorService class as above) ...
172
+
173
+ soap_app = SoapApplication(service_url="http://localhost:8000/soap")
174
+ soap_app.register(CalculatorService())
175
+
176
+ api = FastAPI()
177
+ api.mount("/soap", AsgiSoapApp(soap_app))
178
+ # Run: uvicorn app:api --port 8000
179
+ # WSDL: GET http://localhost:8000/soap?wsdl
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Binding styles and SOAP encoding
185
+
186
+ ### Background — two dimensions
187
+
188
+ The WSDL `<binding>` element is described by two orthogonal choices:
189
+
190
+ - **Style:** `rpc` or `document` — controls whether the SOAP Body contains a wrapper element named after the operation (`rpc`) or raw parameter elements without a wrapper (`document`).
191
+ - **Use:** `encoded` or `literal` — controls whether each element carries a `xsi:type` attribute with runtime type information (`encoded`) or relies solely on the schema (`literal`).
192
+
193
+ References:
194
+ - [IBM developerWorks — Which WSDL style?](https://developer.ibm.com/articles/ws-whichwsdl/)
195
+ - [DZone — Different SOAP encoding styles](https://dzone.com/articles/different-soap-encoding-styles)
196
+ - [Stack Overflow — Document vs RPC style](https://stackoverflow.com/questions/9062475/what-is-the-difference-between-document-style-and-rpc-style-communication)
197
+
198
+ ### The five combinations
199
+
200
+ `BindingStyle` is importable as `from soapbar import BindingStyle`.
201
+
202
+ | `BindingStyle` enum | WSDL style | WSDL use | WS-I BP | Notes |
203
+ |---|---|---|---|---|
204
+ | `RPC_ENCODED` | rpc | encoded | ✗ | Legacy; params carry `xsi:type`; operation wrapper in Body |
205
+ | `RPC_LITERAL` | rpc | literal | ✓ | No `xsi:type`; operation wrapper in Body |
206
+ | `DOCUMENT_LITERAL` | document | literal | ✓ | Params are direct Body children; no wrapper |
207
+ | `DOCUMENT_LITERAL_WRAPPED` | document | literal | ✓ | **Default & recommended**; single wrapper element named after operation |
208
+ | `DOCUMENT_ENCODED` | document | encoded | ✗ | Params are direct Body children each with `xsi:type` |
209
+
210
+ #### RPC_ENCODED
211
+
212
+ ```xml
213
+ <soapenv:Body>
214
+ <tns:add soapenc:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
215
+ <a xsi:type="xsd:int">3</a>
216
+ <b xsi:type="xsd:int">5</b>
217
+ </tns:add>
218
+ </soapenv:Body>
219
+ ```
220
+
221
+ #### RPC_LITERAL
222
+
223
+ ```xml
224
+ <soapenv:Body>
225
+ <tns:add>
226
+ <a>3</a>
227
+ <b>5</b>
228
+ </tns:add>
229
+ </soapenv:Body>
230
+ ```
231
+
232
+ #### DOCUMENT_LITERAL
233
+
234
+ ```xml
235
+ <soapenv:Body>
236
+ <a>3</a>
237
+ <b>5</b>
238
+ </soapenv:Body>
239
+ ```
240
+
241
+ #### DOCUMENT_LITERAL_WRAPPED (default)
242
+
243
+ ```xml
244
+ <soapenv:Body>
245
+ <tns:add>
246
+ <a>3</a>
247
+ <b>5</b>
248
+ </tns:add>
249
+ </soapenv:Body>
250
+ ```
251
+
252
+ #### DOCUMENT_ENCODED
253
+
254
+ ```xml
255
+ <soapenv:Body>
256
+ <a xsi:type="xsd:int">3</a>
257
+ <b xsi:type="xsd:int">5</b>
258
+ </soapenv:Body>
259
+ ```
260
+
261
+ ### Which to choose?
262
+
263
+ Use `DOCUMENT_LITERAL_WRAPPED` unless you are interoperating with a legacy system that requires `RPC_ENCODED`. `DOCUMENT_LITERAL_WRAPPED` is WS-I Basic Profile compliant, the most widely supported style, and the easiest to validate with schema tools.
264
+
265
+ ---
266
+
267
+ ## Defining a service
268
+
269
+ ```python
270
+ from decimal import Decimal
271
+ from soapbar import SoapService, soap_operation, BindingStyle, SoapVersion, xsd
272
+ from soapbar import OperationParameter
273
+
274
+
275
+ class PricingService(SoapService):
276
+ # Class attributes (all have defaults — only override what you need)
277
+ __service_name__ = "Pricing"
278
+ __tns__ = "http://example.com/pricing"
279
+ __binding_style__ = BindingStyle.DOCUMENT_LITERAL_WRAPPED
280
+ __soap_version__ = SoapVersion.SOAP_11
281
+ __service_url__ = "http://localhost:8000/soap"
282
+
283
+ # Auto-introspection: input/output params derived from type hints
284
+ @soap_operation(documentation="Calculate discounted price")
285
+ def get_price(self, item_id: str, quantity: int) -> Decimal:
286
+ return Decimal("9.99") * quantity
287
+
288
+ # Explicit params: use when hints are insufficient or unavailable
289
+ @soap_operation(
290
+ input_params=[
291
+ OperationParameter(name="item_id", xsd_type=xsd.resolve("string")),
292
+ OperationParameter(name="quantity", xsd_type=xsd.resolve("int")),
293
+ ],
294
+ output_params=[
295
+ OperationParameter(name="price", xsd_type=xsd.resolve("decimal")),
296
+ ],
297
+ )
298
+ def get_price_explicit(self, item_id: str, quantity: int) -> Decimal:
299
+ return Decimal("9.99") * quantity
300
+ ```
301
+
302
+ ### `SoapService` class attribute defaults
303
+
304
+ | Attribute | Default | Notes |
305
+ |---|---|---|
306
+ | `__service_name__` | class name | Used in WSDL `<service name="">` |
307
+ | `__tns__` | `"http://example.com/{name}"` | Target namespace |
308
+ | `__binding_style__` | `BindingStyle.DOCUMENT_LITERAL_WRAPPED` | Recommended default |
309
+ | `__soap_version__` | `SoapVersion.SOAP_11` | Change to `SOAP_12` if needed |
310
+ | `__port_name__` | `"{name}Port"` | WSDL port name |
311
+ | `__service_url__` | `""` | Override or pass to `SoapApplication` |
312
+
313
+ ---
314
+
315
+ ## SOAP versions
316
+
317
+ | | SOAP 1.1 | SOAP 1.2 |
318
+ |---|---|---|
319
+ | Envelope namespace | `http://schemas.xmlsoap.org/soap/envelope/` | `http://www.w3.org/2003/05/soap-envelope` |
320
+ | Content-Type | `text/xml; charset=utf-8` | `application/soap+xml; charset=utf-8` |
321
+ | Action header | `SOAPAction: "..."` (separate header) | `action="..."` in Content-Type |
322
+ | Fault code (client) | `Client` | `Sender` |
323
+ | Fault code (server) | `Server` | `Receiver` |
324
+
325
+ soapbar detects the SOAP version automatically from the envelope namespace and translates fault codes between versions when building responses.
326
+
327
+ ```python
328
+ from soapbar import SoapVersion
329
+
330
+ SoapVersion.SOAP_11 # SOAP 1.1
331
+ SoapVersion.SOAP_12 # SOAP 1.2
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Framework compatibility
337
+
338
+ ### ASGI frameworks (via `AsgiSoapApp`)
339
+
340
+ `AsgiSoapApp` is a standard ASGI application. Mount it anywhere an ASGI app is accepted.
341
+
342
+ | Framework | How to mount |
343
+ |---|---|
344
+ | **FastAPI** | `app.mount("/soap", AsgiSoapApp(soap_app))` |
345
+ | **Starlette** | `routes=[Mount("/soap", app=AsgiSoapApp(soap_app))]` |
346
+ | **Litestar** | `app.mount("/soap", AsgiSoapApp(soap_app))` |
347
+ | **Quart** | Use `asgiref` or serve directly with Hypercorn |
348
+ | **BlackSheep** | `app.mount("/soap", AsgiSoapApp(soap_app))` |
349
+ | **Django** (≥ 3.1 ASGI) | Route in `asgi.py` via URL dispatcher |
350
+
351
+ ASGI servers (Uvicorn, Hypercorn, Daphne) can run `AsgiSoapApp` directly.
352
+
353
+ **FastAPI example:**
354
+
355
+ ```python
356
+ from fastapi import FastAPI
357
+ from soapbar import SoapApplication, AsgiSoapApp
358
+
359
+ soap_app = SoapApplication(service_url="http://localhost:8000/soap")
360
+ soap_app.register(CalculatorService())
361
+
362
+ api = FastAPI()
363
+ api.mount("/soap", AsgiSoapApp(soap_app))
364
+ ```
365
+
366
+ ### WSGI frameworks (via `WsgiSoapApp`)
367
+
368
+ | Framework | How to mount |
369
+ |---|---|
370
+ | **Flask** | `DispatcherMiddleware` or replace `app.wsgi_app` (requires `werkzeug`) |
371
+ | **Django** (classic WSGI) | Mount as sub-application in `urls.py` |
372
+ | **Falcon** | `app.add_sink(WsgiSoapApp(soap_app), "/soap")` |
373
+ | **Bottle** | `app.mount("/soap", WsgiSoapApp(soap_app))` |
374
+ | **Pyramid** | Composable WSGI stack |
375
+
376
+ WSGI servers (Gunicorn, uWSGI, mod_wsgi) can run `WsgiSoapApp` directly.
377
+
378
+ **Flask example:**
379
+
380
+ ```python
381
+ from flask import Flask
382
+ from werkzeug.middleware.dispatcher import DispatcherMiddleware
383
+ from soapbar import SoapApplication, WsgiSoapApp
384
+
385
+ soap_app = SoapApplication(service_url="http://localhost:8000/soap")
386
+ soap_app.register(CalculatorService())
387
+
388
+ flask_app = Flask(__name__)
389
+ flask_app.wsgi_app = DispatcherMiddleware(flask_app.wsgi_app, {
390
+ "/soap": WsgiSoapApp(soap_app),
391
+ })
392
+ ```
393
+
394
+ ---
395
+
396
+ ## WSDL
397
+
398
+ **Auto-generation** — no configuration needed. Register a service and the WSDL is generated automatically:
399
+
400
+ ```python
401
+ wsdl_bytes = soap_app.get_wsdl()
402
+ ```
403
+
404
+ Served automatically at `GET ?wsdl` when using `AsgiSoapApp` or `WsgiSoapApp`.
405
+
406
+ **Parse an existing WSDL** to inspect its structure:
407
+
408
+ ```python
409
+ from soapbar import parse_wsdl, parse_wsdl_file
410
+
411
+ defn = parse_wsdl(wsdl_bytes) # from bytes/str
412
+ defn = parse_wsdl_file("service.wsdl") # from file
413
+ ```
414
+
415
+ **Custom WSDL override** — supply your own WSDL document and skip auto-generation:
416
+
417
+ ```python
418
+ soap_app = SoapApplication(custom_wsdl=open("my_service.wsdl", "rb").read())
419
+ ```
420
+
421
+ **Remote `wsdl:import` — SSRF guard** — `parse_wsdl` blocks outbound HTTP fetches by default. `wsdl:import` elements whose resolved location starts with `http://` or `https://` raise `ValueError` unless you explicitly opt in:
422
+
423
+ ```python
424
+ # Default — safe for untrusted WSDLs; remote imports raise ValueError
425
+ defn = parse_wsdl(wsdl_bytes)
426
+
427
+ # Opt-in — only when the WSDL source is trusted
428
+ defn = parse_wsdl(wsdl_bytes, allow_remote_imports=True)
429
+ ```
430
+
431
+ This prevents Server-Side Request Forgery (SSRF) when parsing WSDLs from user-supplied URLs or untrusted data. The top-level WSDL fetch (e.g. `SoapClient(wsdl_url=...)`) is always explicit; only `wsdl:import` resolution inside the document is guarded.
432
+
433
+ ---
434
+
435
+ ## Client
436
+
437
+ ```python
438
+ import asyncio
439
+ from soapbar import SoapClient, SoapFault
440
+
441
+ # From a live WSDL URL (fetches WSDL over HTTP)
442
+ client = SoapClient(wsdl_url="http://localhost:8000/soap?wsdl")
443
+
444
+ # From a WSDL string/bytes you already have
445
+ client = SoapClient.from_wsdl_string(wsdl_bytes)
446
+
447
+ # From a WSDL file
448
+ client = SoapClient.from_file("service.wsdl")
449
+
450
+ # Manual — no WSDL, specify endpoint and style directly
451
+ from soapbar import BindingStyle, SoapVersion
452
+
453
+ client = SoapClient.manual(
454
+ address="http://localhost:8000/soap",
455
+ binding_style=BindingStyle.DOCUMENT_LITERAL_WRAPPED,
456
+ soap_version=SoapVersion.SOAP_11,
457
+ )
458
+
459
+ # Sync call via service proxy
460
+ try:
461
+ result = client.service.add(a=3, b=5)
462
+ print(result) # 8
463
+ except SoapFault as fault:
464
+ print(fault.faultcode, fault.faultstring)
465
+
466
+ # Direct call by operation name
467
+ result = client.call("add", a=3, b=5)
468
+
469
+ # Async call
470
+ async def main():
471
+ result = await client.call_async("add", a=3, b=5)
472
+ print(result)
473
+
474
+ asyncio.run(main())
475
+ ```
476
+
477
+ ### `HttpTransport` options
478
+
479
+ ```python
480
+ from soapbar import SoapClient, HttpTransport
481
+
482
+ transport = HttpTransport(timeout=60.0, verify_ssl=False)
483
+ client = SoapClient(wsdl_url="http://localhost:8000/soap?wsdl", transport=transport)
484
+ ```
485
+
486
+ ### Advanced: manual client with explicit operation signature
487
+
488
+ Use `register_operation` when you need full control over the operation schema without a WSDL:
489
+
490
+ ```python
491
+ from soapbar import SoapClient, OperationSignature, OperationParameter, BindingStyle, xsd
492
+
493
+ sig = OperationSignature(
494
+ name="Add",
495
+ input_params=[
496
+ OperationParameter("a", xsd.resolve("int")),
497
+ OperationParameter("b", xsd.resolve("int")),
498
+ ],
499
+ output_params=[OperationParameter("return", xsd.resolve("int"))],
500
+ )
501
+
502
+ client = SoapClient.manual("http://host/soap", binding_style=BindingStyle.RPC_LITERAL)
503
+ client.register_operation(sig)
504
+ result = client.call("Add", a=3, b=4) # 7
505
+ ```
506
+
507
+ ---
508
+
509
+ ## XSD type system
510
+
511
+ soapbar includes a registry of 27 built-in XSD types. Types handle serialization to and from XML text.
512
+
513
+ ```python
514
+ from soapbar import xsd
515
+
516
+ # Resolve a type by XSD name
517
+ int_type = xsd.resolve("int") # XsdType for xsd:int
518
+ str_type = xsd.resolve("string") # XsdType for xsd:string
519
+
520
+ # Map a Python type to its XSD equivalent
521
+ xsd_type = xsd.python_to_xsd(int) # -> xsd:int XsdType
522
+ xsd_type = xsd.python_to_xsd(str) # -> xsd:string XsdType
523
+
524
+ # Serialize / deserialize
525
+ int_type.to_xml(42) # "42"
526
+ int_type.from_xml("42") # 42
527
+
528
+ # Inspect all registered types
529
+ all_types = xsd.all_types()
530
+ ```
531
+
532
+ Python → XSD mapping:
533
+
534
+ | Python type | XSD type |
535
+ |---|---|
536
+ | `bool` | `boolean` |
537
+ | `int` | `int` |
538
+ | `float` | `float` |
539
+ | `str` | `string` |
540
+ | `Decimal` | `decimal` |
541
+ | `bytes` | `base64Binary` |
542
+
543
+ ---
544
+
545
+ ## Fault handling
546
+
547
+ ### Raising a fault from a service method
548
+
549
+ ```python
550
+ from soapbar import SoapService, soap_operation, SoapFault
551
+
552
+
553
+ class StrictCalculator(SoapService):
554
+ __service_name__ = "StrictCalculator"
555
+ __tns__ = "http://example.com/calc"
556
+
557
+ @soap_operation()
558
+ def divide(self, a: int, b: int) -> int:
559
+ if b == 0:
560
+ raise SoapFault(
561
+ faultcode="Client",
562
+ faultstring="Division by zero",
563
+ detail="b must be non-zero",
564
+ )
565
+ return a // b
566
+ ```
567
+
568
+ `SoapClient.call()` and `call_async()` automatically raise `SoapFault` when the server returns a fault response.
569
+
570
+ ### Creating and rendering faults manually
571
+
572
+ ```python
573
+ from soapbar import SoapFault
574
+
575
+ # Create a fault
576
+ fault = SoapFault(
577
+ faultcode="Client",
578
+ faultstring="Invalid input: quantity must be positive",
579
+ detail="quantity=-1", # string or lxml _Element
580
+ )
581
+
582
+ # Render as SOAP 1.1 or 1.2 envelope
583
+ envelope_11 = fault.to_soap11_envelope()
584
+ envelope_12 = fault.to_soap12_envelope()
585
+
586
+ # SOAP 1.2 subcodes — each is (namespace_uri, localname) for spec-compliant QName
587
+ fault_12 = SoapFault(
588
+ faultcode="Client",
589
+ faultstring="Validation error",
590
+ subcodes=[("http://example.com/errors", "InvalidQuantity")],
591
+ )
592
+ ```
593
+
594
+ Fault code translation is automatic:
595
+
596
+ | Canonical (used in soapbar) | SOAP 1.1 wire | SOAP 1.2 wire |
597
+ |---|---|---|
598
+ | `Client` | `Client` | `Sender` |
599
+ | `Server` | `Server` | `Receiver` |
600
+
601
+ ---
602
+
603
+ ## Security
604
+
605
+ soapbar uses a hardened lxml parser:
606
+
607
+ ```python
608
+ lxml.etree.XMLParser(
609
+ resolve_entities=False, # XXE prevention
610
+ no_network=True, # SSRF prevention
611
+ load_dtd=False, # DTD injection prevention
612
+ huge_tree=False, # Billion-Laughs prevention
613
+ remove_comments=True, # comment injection prevention
614
+ remove_pis=True,
615
+ )
616
+ ```
617
+
618
+ Entity references (potential XXE payloads) are silently dropped rather than expanded. No network connections are made during parsing. DTDs are not loaded.
619
+
620
+ Additional hardening:
621
+ - **Message size limit**: `SoapApplication(max_body_size=10*1024*1024)` — requests exceeding 10 MB are rejected with a `Client` fault before XML parsing.
622
+ - **XML nesting depth**: requests exceeding 100 levels of nesting are rejected to prevent stack exhaustion.
623
+ - **Error scrubbing**: unhandled exceptions produce `"An internal error occurred."` — no stack traces or exception text are returned to clients.
624
+ - **HTTPS warning**: `SoapApplication` warns at construction time if `service_url` uses plain HTTP.
625
+
626
+ ---
627
+
628
+ ## WS-Security — UsernameToken
629
+
630
+ soapbar supports WS-Security 1.0 UsernameToken (OASIS 2004), both plain-text and SHA-1 digest.
631
+
632
+ ### Client — attaching credentials
633
+
634
+ ```python
635
+ from soapbar import SoapClient
636
+ from soapbar.core.wssecurity import UsernameTokenCredential
637
+
638
+ # Plain-text password
639
+ cred = UsernameTokenCredential(username="alice", password="secret")
640
+
641
+ # SHA-1 PasswordDigest (recommended for non-TLS scenarios)
642
+ cred = UsernameTokenCredential(username="alice", password="secret", use_digest=True)
643
+
644
+ client = SoapClient.manual(
645
+ "https://example.com/soap",
646
+ wss_credential=cred,
647
+ )
648
+ result = client.call("GetData", id=42)
649
+ ```
650
+
651
+ The `wsse:Security` header is injected automatically on every call.
652
+
653
+ ### Server — validating credentials
654
+
655
+ ```python
656
+ from soapbar import SoapApplication
657
+ from soapbar.core.wssecurity import UsernameTokenValidator, SecurityValidationError
658
+
659
+
660
+ class MyValidator(UsernameTokenValidator):
661
+ _users = {"alice": "secret", "bob": "hunter2"}
662
+
663
+ def get_password(self, username: str) -> str | None:
664
+ return self._users.get(username)
665
+
666
+
667
+ app = SoapApplication(
668
+ service_url="https://example.com/soap",
669
+ security_validator=MyValidator(),
670
+ )
671
+ app.register(MyService())
672
+ ```
673
+
674
+ `SecurityValidationError` is converted to a `Client` SOAP fault automatically. Both PasswordText and PasswordDigest token types are verified; Digest requires `wsse:Nonce` and `wsu:Created` to be present.
675
+
676
+ ---
677
+
678
+ ## MTOM/XOP
679
+
680
+ soapbar supports MTOM (Message Transmission Optimization Mechanism, W3C) for sending and receiving SOAP messages with binary attachments. The `multipart/related` MIME packaging is handled transparently — the core envelope sees resolved base64 data; your service code sees plain bytes.
681
+
682
+ ### Client — sending attachments
683
+
684
+ ```python
685
+ from soapbar import SoapClient, BindingStyle
686
+
687
+ client = SoapClient.manual(
688
+ "http://localhost:8000/soap",
689
+ binding_style=BindingStyle.DOCUMENT_LITERAL_WRAPPED,
690
+ use_mtom=True,
691
+ )
692
+
693
+ # Queue a binary attachment and get its Content-ID back
694
+ cid = client.add_attachment(b"\x89PNG...", content_type="image/png")
695
+
696
+ # The call packages the envelope + attachments as multipart/related
697
+ result = client.call("UploadImage", image_cid=cid, filename="logo.png")
698
+ ```
699
+
700
+ ### Server — receiving MTOM
701
+
702
+ No configuration required. `AsgiSoapApp` and `WsgiSoapApp` automatically detect inbound `multipart/related` requests, resolve all `xop:Include` references inline, and pass the reconstructed XML to the dispatcher as a normal SOAP envelope.
703
+
704
+ ### Low-level API
705
+
706
+ ```python
707
+ from soapbar import parse_mtom, build_mtom, MtomAttachment
708
+
709
+ # Parse a raw MTOM HTTP body
710
+ msg = parse_mtom(raw_bytes, content_type_header)
711
+ print(msg.soap_xml) # bytes — envelope with XOP includes resolved
712
+ print(msg.attachments) # list[MtomAttachment]
713
+
714
+ # Build a MTOM HTTP body
715
+ attachments = [MtomAttachment(content_id="part1@host", content_type="image/png", data=png_bytes)]
716
+ body_bytes, content_type = build_mtom(soap_xml_bytes, attachments)
717
+ ```
718
+
719
+ ---
720
+
721
+ ## XML Signature and Encryption
722
+
723
+ Requires `pip install soapbar[security]` (pulls in `signxml` and `cryptography`).
724
+
725
+ ### XML Digital Signature (XML-DSIG)
726
+
727
+ ```python
728
+ from cryptography.hazmat.primitives.asymmetric import rsa
729
+ from cryptography.hazmat.primitives import hashes
730
+ from cryptography.x509 import CertificateBuilder
731
+ from soapbar.core.wssecurity import sign_envelope, verify_envelope, XmlSecurityError
732
+
733
+ # Sign — enveloped RSA-SHA256 XML-DSIG
734
+ signed_bytes = sign_envelope(envelope_bytes, private_key, certificate)
735
+
736
+ # Verify — raises XmlSecurityError on bad signature
737
+ try:
738
+ verified_bytes = verify_envelope(signed_bytes, certificate)
739
+ except XmlSecurityError as exc:
740
+ print("Signature invalid:", exc)
741
+ ```
742
+
743
+ ### XML Encryption (AES-256-CBC + RSA-OAEP)
744
+
745
+ ```python
746
+ from soapbar.core.wssecurity import encrypt_body, decrypt_body, XmlSecurityError
747
+
748
+ # Encrypt SOAP Body — AES-256-CBC session key wrapped with recipient's RSA public key
749
+ encrypted_bytes = encrypt_body(envelope_bytes, recipient_public_key)
750
+
751
+ # Decrypt — extracts and unwraps the session key, restores Body children
752
+ decrypted_bytes = decrypt_body(encrypted_bytes, recipient_private_key)
753
+ ```
754
+
755
+ The `xenc:EncryptedData` element is placed as the sole child of `<soap:Body>`. The AES-256-CBC session key is wrapped with RSA-OAEP (SHA-256) in an `xenc:EncryptedKey` element inside `xenc:KeyInfo`.
756
+
757
+ ### WS-I BSP X.509 Token Profile (S10)
758
+
759
+ For interoperability with WS-I Basic Security Profile 1.1 compliant clients and servers, use the BSP variant which embeds the certificate as a `wsse:BinarySecurityToken` and references it from `ds:Signature/ds:KeyInfo`:
760
+
761
+ ```python
762
+ from soapbar.core.wssecurity import (
763
+ sign_envelope_bsp,
764
+ verify_envelope_bsp,
765
+ build_binary_security_token,
766
+ extract_certificate_from_security,
767
+ )
768
+
769
+ # Sign — adds wsse:BinarySecurityToken + wsse:SecurityTokenReference in KeyInfo
770
+ signed_bytes = sign_envelope_bsp(envelope_bytes, private_key, certificate)
771
+
772
+ # Verify — extracts cert from BST, verifies ds:Signature
773
+ verified_bytes = verify_envelope_bsp(signed_bytes)
774
+
775
+ # Build a standalone BinarySecurityToken element (e.g. to add to an existing header)
776
+ bst = build_binary_security_token(certificate, token_id="MyToken-1")
777
+ ```
778
+
779
+ ---
780
+
781
+ ## WSDL schema validation
782
+
783
+ `SoapApplication` can validate the SOAP Body of each inbound request against the XSD types embedded in the WSDL. Validation is opt-in and disabled by default.
784
+
785
+ ```python
786
+ from soapbar import SoapApplication
787
+
788
+ soap_app = SoapApplication(
789
+ service_url="https://example.com/soap",
790
+ validate_body_schema=True, # X07 — WS-I BP 1.1 R2201
791
+ )
792
+ soap_app.register(MyService())
793
+ ```
794
+
795
+ When enabled, the compiled `lxml.etree.XMLSchema` is built once from the WSDL-embedded `<xs:schema>` elements and cached. Any Body element that fails schema validation results in a `Client` fault with the first schema error message. Requests to services with no embedded schemas pass through unchanged.
796
+
797
+ ---
798
+
799
+ ## One-way operations
800
+
801
+ One-way operations fire-and-forget: the server processes the message and returns HTTP 202 Accepted with an empty body (SOAP 1.2 Part 2 §7.5.1).
802
+
803
+ ```python
804
+ from soapbar import SoapService, soap_operation
805
+
806
+
807
+ class EventService(SoapService):
808
+ __service_name__ = "EventService"
809
+ __tns__ = "http://example.com/events"
810
+
811
+ @soap_operation(one_way=True)
812
+ def publish_event(self, event_type: str, payload: str) -> None:
813
+ # Process asynchronously — no response is sent
814
+ _event_queue.put((event_type, payload))
815
+ ```
816
+
817
+ The client receives `202 Accepted` with no body. `SoapClient.call()` returns `None` for one-way operations.
818
+
819
+ ---
820
+
821
+ ## SOAP array attributes
822
+
823
+ When using encoded binding styles (`RPC_ENCODED`, `DOCUMENT_ENCODED`), array elements are annotated with the correct version-specific attributes automatically.
824
+
825
+ SOAP 1.1 (`SOAP-ENC:arrayType`):
826
+ ```xml
827
+ <names soapenc:arrayType="xsd:string[3]"
828
+ xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
829
+ <item>Alice</item><item>Bob</item><item>Carol</item>
830
+ </names>
831
+ ```
832
+
833
+ SOAP 1.2 (`enc:itemType` + `enc:arraySize`):
834
+ ```xml
835
+ <names enc:itemType="xsd:string" enc:arraySize="3"
836
+ xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
837
+ <item>Alice</item><item>Bob</item><item>Carol</item>
838
+ </names>
839
+ ```
840
+
841
+ The correct attributes are emitted automatically based on the SOAP version in use — no manual configuration needed. The `get_serializer(style, soap_version)` factory handles the selection.
842
+
843
+ ---
844
+
845
+ ## rpc:result (SOAP 1.2)
846
+
847
+ SOAP 1.2 Part 2 §4.2.1 defines a `rpc:result` SHOULD convention for naming the return value in RPC responses. soapbar omits it by default (preserving interoperability with zeep and other strict-mode clients) and offers an opt-in:
848
+
849
+ ```python
850
+ from soapbar import SoapService, soap_operation
851
+
852
+
853
+ class CalcService(SoapService):
854
+ __service_name__ = "Calc"
855
+ __tns__ = "http://example.com/calc"
856
+
857
+ # Default: no rpc:result (interoperable with zeep, WCF, etc.)
858
+ @soap_operation()
859
+ def add(self, a: int, b: int) -> int:
860
+ return a + b
861
+
862
+ # Opt-in: emit rpc:result for strict SOAP 1.2 consumers
863
+ @soap_operation(emit_rpc_result=True)
864
+ def add_strict(self, a: int, b: int) -> int:
865
+ return a + b
866
+ ```
867
+
868
+ When opted in, the response wrapper contains:
869
+ ```xml
870
+ <CalcResponse>
871
+ <rpc:result xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">return</rpc:result>
872
+ <return>8</return>
873
+ </CalcResponse>
874
+ ```
875
+
876
+ ---
877
+
878
+ ## Interoperability
879
+
880
+ soapbar is tested against zeep and spyne via integration tests.
881
+
882
+ - **zeep → soapbar**: a zeep client can call a soapbar server without modification. The WSDL generated by soapbar is zeep-parseable.
883
+ - **soapbar → spyne**: a soapbar client can call a spyne server using RPC/Literal.
884
+ - **soapbar ↔ soapbar**: full round-trip tested for all binding styles and both SOAP versions.
885
+
886
+ ---
887
+
888
+ ## Architecture
889
+
890
+ ```
891
+ HTTP request
892
+
893
+
894
+ ┌─────────────────┐
895
+ │ AsgiSoapApp / │ ← thin ASGI/WSGI adapters
896
+ │ WsgiSoapApp │
897
+ └────────┬────────┘
898
+
899
+
900
+ ┌─────────────────┐
901
+ │ SoapApplication │ ← dispatcher: version detection,
902
+ │ │ operation routing, fault wrapping
903
+ └────────┬────────┘
904
+
905
+
906
+ ┌─────────────────┐
907
+ │ SoapService │ ← your business logic lives here
908
+ │ @soap_operation│
909
+ └────────┬────────┘
910
+ │ calls binding serializer + envelope builder
911
+
912
+ ┌─────────────────┐
913
+ │ core/ │ ← binding.py · envelope.py · types.py
914
+ │ binding/types/ │ wsdl/ · xml.py · fault.py
915
+ │ envelope/wsdl │
916
+ └─────────────────┘
917
+ ```
918
+
919
+ ---
920
+
921
+ ## Public API
922
+
923
+ The most-used symbols are all importable from the top-level `soapbar` namespace:
924
+
925
+ | Symbol | Import | Description |
926
+ |--------|--------|-------------|
927
+ | `SoapService` | `from soapbar import SoapService` | Base class for SOAP services |
928
+ | `soap_operation` | `from soapbar import soap_operation` | Decorator for service methods |
929
+ | `SoapApplication` | `from soapbar import SoapApplication` | SOAP dispatcher/router |
930
+ | `AsgiSoapApp` | `from soapbar import AsgiSoapApp` | ASGI adapter |
931
+ | `WsgiSoapApp` | `from soapbar import WsgiSoapApp` | WSGI adapter |
932
+ | `SoapClient` | `from soapbar import SoapClient` | SOAP client |
933
+ | `HttpTransport` | `from soapbar import HttpTransport` | HTTP transport layer |
934
+ | `SoapFault` | `from soapbar import SoapFault` | SOAP fault exception |
935
+ | `BindingStyle` | `from soapbar import BindingStyle` | Binding style enum |
936
+ | `SoapVersion` | `from soapbar import SoapVersion` | SOAP version enum |
937
+ | `xsd` | `from soapbar import xsd` | XSD type registry |
938
+ | `parse_wsdl` | `from soapbar import parse_wsdl` | Parse WSDL from bytes/str |
939
+ | `parse_wsdl_file` | `from soapbar import parse_wsdl_file` | Parse WSDL from a file path |
940
+ | `build_wsdl_string` | `from soapbar import build_wsdl_string` | Generate WSDL as string |
941
+ | `OperationParameter` | `from soapbar import OperationParameter` | Parameter descriptor for operations |
942
+ | `OperationSignature` | `from soapbar import OperationSignature` | Full operation signature (manual client) |
943
+ | `UsernameTokenCredential` | `from soapbar.core.wssecurity import UsernameTokenCredential` | WS-Security credential for client |
944
+ | `UsernameTokenValidator` | `from soapbar.core.wssecurity import UsernameTokenValidator` | Abstract base for server-side token validation |
945
+ | `SecurityValidationError` | `from soapbar.core.wssecurity import SecurityValidationError` | Raised on authentication failure |
946
+ | `build_security_header` | `from soapbar.core.wssecurity import build_security_header` | Build `wsse:Security` header element |
947
+ | `sign_envelope` | `from soapbar.core.wssecurity import sign_envelope` | Enveloped XML-DSIG signature (RSA-SHA256) |
948
+ | `verify_envelope` | `from soapbar.core.wssecurity import verify_envelope` | Verify and return signed envelope bytes |
949
+ | `encrypt_body` | `from soapbar.core.wssecurity import encrypt_body` | AES-256-CBC body encryption + RSA-OAEP key wrap |
950
+ | `decrypt_body` | `from soapbar.core.wssecurity import decrypt_body` | Decrypt `xenc:EncryptedData` body and restore children |
951
+ | `XmlSecurityError` | `from soapbar.core.wssecurity import XmlSecurityError` | Raised on XML signature/encryption failure |
952
+ | `build_binary_security_token` | `from soapbar.core.wssecurity import build_binary_security_token` | Build WS-I BSP `wsse:BinarySecurityToken` from X.509 cert |
953
+ | `extract_certificate_from_security` | `from soapbar.core.wssecurity import extract_certificate_from_security` | Extract X.509 cert from `wsse:BinarySecurityToken` |
954
+ | `sign_envelope_bsp` | `from soapbar.core.wssecurity import sign_envelope_bsp` | BSP-compliant signing with `wsse:SecurityTokenReference` |
955
+ | `verify_envelope_bsp` | `from soapbar.core.wssecurity import verify_envelope_bsp` | Verify BSP-signed envelope using embedded BST cert |
956
+ | `MtomAttachment` | `from soapbar import MtomAttachment` | MTOM attachment descriptor (content_id, content_type, data) |
957
+ | `MtomMessage` | `from soapbar import MtomMessage` | Parsed MTOM message (soap_xml + attachments list) |
958
+ | `parse_mtom` | `from soapbar import parse_mtom` | Parse a raw `multipart/related` MTOM body |
959
+ | `build_mtom` | `from soapbar import build_mtom` | Build a `multipart/related` MTOM body |
960
+
961
+ ---
962
+
963
+ ## Comparison with alternatives
964
+
965
+ | Capability | **soapbar** | zeep | spyne | fastapi-soap |
966
+ |---|---|---|---|---|
967
+ | SOAP client | ✓ | ✓ | ✗ | ✗ |
968
+ | SOAP server | ✓ | ✗ | ✓ | ✓ |
969
+ | All 5 binding styles | ✓ | ✓ (client) | ✓ | Partial |
970
+ | SOAP 1.1 + 1.2 | ✓ | ✓ | ✓ | 1.1 only |
971
+ | ASGI frameworks | ✓ | ✗ | ✗ | FastAPI only |
972
+ | WSGI frameworks | ✓ | ✗ | ✓ | ✗ |
973
+ | Auto WSDL generation | ✓ | ✗ | ✓ | ✓ |
974
+ | WSDL-driven client | ✓ | ✓ | ✗ | ✗ |
975
+ | XXE hardened by default | ✓ | ? | ? | ? |
976
+ | Message size + depth limits | ✓ | ✗ | ✗ | ✗ |
977
+ | WS-Security UsernameToken | ✓ | ✓ (client) | ✓ | ✗ |
978
+ | XML Signature / Encryption | ✓ ([security]) | ✗ | Partial | ✗ |
979
+ | MTOM/XOP | ✓ | ✓ | ✓ | ✗ |
980
+ | WS-Addressing 1.0 | ✓ | ✓ | Partial | ✗ |
981
+ | One-way MEP (HTTP 202) | ✓ | ✓ | ✓ | ✗ |
982
+ | SOAP array attributes | ✓ | ✓ | ✓ | ✗ |
983
+ | 100% SOAP protocol audit | ✓ | — | — | — |
984
+ | Core dependency | lxml | lxml, requests | lxml | fastapi, lxml |
985
+ | Async HTTP client | httpx (optional) | httpx (optional) | — | — |
986
+ | Python versions | 3.10–3.14 | 3.8+ | 3.8+ | 3.8+ |
987
+
988
+ soapbar is the only Python library that covers both client and server, works with any ASGI or WSGI framework, supports SOAP 1.1 and 1.2, is hardened against XXE/DoS attacks out of the box, and has passed a full SOAP Protocol Conformance Audit at 100% (46/46 checkpoints).
989
+
990
+ ---
991
+
992
+ ## Development setup
993
+
994
+ ```bash
995
+ git clone https://github.com/hitoshyamamoto/soapbar
996
+ cd soapbar
997
+ uv sync --group dev --group lint --group type
998
+
999
+ # Run tests
1000
+ uv run pytest tests/ -v
1001
+
1002
+ # Lint
1003
+ uv run ruff check src/ tests/
1004
+
1005
+ # Type check
1006
+ uv run mypy src/
1007
+ ```
1008
+
1009
+ Run the example server (requires FastAPI + uvicorn):
1010
+
1011
+ ```bash
1012
+ pip install fastapi uvicorn
1013
+ uvicorn examples.calculator_fastapi:app --reload --port 8000
1014
+ ```
1015
+
1016
+ Then fetch the WSDL: `curl http://localhost:8000/soap?wsdl`
1017
+
1018
+ ---
1019
+
1020
+ ## Inspired by
1021
+
1022
+ - **[Spyne](https://github.com/arskom/spyne)** — the original comprehensive Python SOAP/RPC framework; inspired the service-class model and binding style abstractions.
1023
+ - **[zeep](https://github.com/mvantellingen/python-zeep)** — the de facto modern Python SOAP client; inspired the WSDL-driven client approach and XSD type mapping.
1024
+ - **[fastapi-soap](https://github.com/rezashahnazar/fastapi-soap)** — demonstrated clean FastAPI/ASGI integration for SOAP endpoints; inspired the ASGI adapter design.
1025
+
1026
+ ---
1027
+
1028
+ ## Learn more
1029
+
1030
+ **SOAP protocol**
1031
+ - [Wikipedia — SOAP](https://pt.wikipedia.org/wiki/SOAP)
1032
+ - [W3Schools — XML/SOAP intro](https://www.w3schools.com/XML/)
1033
+ - [GeeksForGeeks — Basics of SOAP](https://www.geeksforgeeks.org/computer-networks/basics-of-soap-simple-object-access-protocol/)
1034
+ - [Oracle — SOAP API reference](https://docs.oracle.com/en/cloud/saas/applications-common/25a/biacc/soap-api.html)
1035
+
1036
+ **WSDL**
1037
+ - [TutorialsPoint — WSDL](https://www.tutorialspoint.com/wsdl/index.htm)
1038
+ - [GeeksForGeeks — WSDL introduction](https://www.geeksforgeeks.org/software-engineering/wsdl-introduction/)
1039
+
1040
+ **Binding styles and encoding**
1041
+ - [IBM developerWorks — Which WSDL style?](https://developer.ibm.com/articles/ws-whichwsdl/)
1042
+ - [DZone — Different SOAP encoding styles](https://dzone.com/articles/different-soap-encoding-styles)
1043
+ - [Stack Overflow — Document vs RPC style](https://stackoverflow.com/questions/9062475/what-is-the-difference-between-document-style-and-rpc-style-communication)
1044
+
1045
+ ---
1046
+
1047
+ ## Known Limitations
1048
+
1049
+ The following features are intentionally out-of-scope for the current release. Behaviour is well-defined in each case (documented exception or graceful exposure).
1050
+
1051
+ | Area | Status | Notes |
1052
+ |------|--------|-------|
1053
+ | **MTOM/XOP** | Fully implemented | `parse_mtom` / `build_mtom` handle `multipart/related` MIME packaging and XOP Include resolution. `AsgiSoapApp` and `WsgiSoapApp` decode inbound MTOM automatically. `SoapClient` sends MTOM when `use_mtom=True`. |
1054
+ | **WS-Security** | Fully implemented | `UsernameTokenCredential` / `UsernameTokenValidator` for PasswordText and PasswordDigest. `sign_envelope` / `verify_envelope` for XML-DSIG. `encrypt_body` / `decrypt_body` for XML Encryption (AES-256-CBC + RSA-OAEP). `sign_envelope_bsp` / `verify_envelope_bsp` + `build_binary_security_token` for WS-I BSP X.509 token profile (S10). All require `soapbar[security]`. |
1055
+ | **WS-Addressing** | Fully parsed + response headers generated | Inbound headers (`MessageID`, `To`, `Action`, `ReplyTo`, `FaultTo`, `ReferenceParameters`) are parsed into `WsaHeaders`. Response headers (`MessageID`, `RelatesTo`, `Action`, ReferenceParameters) are generated automatically when `use_wsa=True`. |
1056
+ | **SOAP 1.2 `relay` attribute** | Parsed and exposed on `SoapHeaderBlock` | The `relay` boolean is available on each `SoapHeaderBlock` instance. Full SOAP intermediary forwarding (actually relaying the message) is not implemented. |
1057
+ | **`xsd:complexType` / `xsd:array` / `xsd:choice`** | Fully supported for round-trip serialization | Recursive (`self-referencing`) complex types are resolved lazily. `xsd:complexContent/restriction` for SOAP-encoded arrays is also parsed from WSDL. |
1058
+ | **External schema `xsd:import`** | Not followed | `wsdl:import` (document-level) is resolved with an SSRF guard (`allow_remote_imports=False` by default). `xsd:import` elements *inside* a `<types>` schema are silently ignored; type resolution falls back to built-in primitives. |
1059
+
1060
+ ---
1061
+
1062
+ ## License
1063
+
1064
+ MIT with Attribution