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.
- soapbar-0.5.0/PKG-INFO +1064 -0
- soapbar-0.5.0/README.md +1028 -0
- soapbar-0.5.0/pyproject.toml +80 -0
- soapbar-0.5.0/setup.cfg +4 -0
- soapbar-0.5.0/src/soapbar/__init__.py +143 -0
- soapbar-0.5.0/src/soapbar/client/__init__.py +7 -0
- soapbar-0.5.0/src/soapbar/client/client.py +257 -0
- soapbar-0.5.0/src/soapbar/client/transport.py +104 -0
- soapbar-0.5.0/src/soapbar/core/__init__.py +41 -0
- soapbar-0.5.0/src/soapbar/core/binding.py +657 -0
- soapbar-0.5.0/src/soapbar/core/envelope.py +407 -0
- soapbar-0.5.0/src/soapbar/core/fault.py +274 -0
- soapbar-0.5.0/src/soapbar/core/mtom.py +239 -0
- soapbar-0.5.0/src/soapbar/core/namespaces.py +58 -0
- soapbar-0.5.0/src/soapbar/core/types.py +549 -0
- soapbar-0.5.0/src/soapbar/core/wsdl/__init__.py +117 -0
- soapbar-0.5.0/src/soapbar/core/wsdl/builder.py +211 -0
- soapbar-0.5.0/src/soapbar/core/wsdl/parser.py +437 -0
- soapbar-0.5.0/src/soapbar/core/wssecurity.py +895 -0
- soapbar-0.5.0/src/soapbar/core/xml.py +208 -0
- soapbar-0.5.0/src/soapbar/py.typed +0 -0
- soapbar-0.5.0/src/soapbar/server/__init__.py +15 -0
- soapbar-0.5.0/src/soapbar/server/application.py +528 -0
- soapbar-0.5.0/src/soapbar/server/asgi.py +111 -0
- soapbar-0.5.0/src/soapbar/server/service.py +123 -0
- soapbar-0.5.0/src/soapbar/server/wsgi.py +76 -0
- soapbar-0.5.0/src/soapbar.egg-info/PKG-INFO +1064 -0
- soapbar-0.5.0/src/soapbar.egg-info/SOURCES.txt +32 -0
- soapbar-0.5.0/src/soapbar.egg-info/dependency_links.txt +1 -0
- soapbar-0.5.0/src/soapbar.egg-info/requires.txt +17 -0
- soapbar-0.5.0/src/soapbar.egg-info/top_level.txt +1 -0
- soapbar-0.5.0/tests/test_interop.py +244 -0
- soapbar-0.5.0/tests/test_real_wsdls.py +87 -0
- 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
|
+

|
|
40
|
+

|
|
41
|
+

|
|
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
|