Migrating from zeep or spyne¶
This page is a map, not an argument: it assumes you have already decided to try soapbar and shows where each thing you use in zeep or spyne lives here. Where zeep or spyne does something soapbar does not, the page says so.
zeep → soapbar (client)¶
| zeep | soapbar |
|---|---|
zeep.Client(wsdl_url) |
SoapClient(wsdl_url=...) |
zeep.Client("service.wsdl") |
SoapClient(wsdl_url="service.wsdl") (a path works) |
| WSDL held in memory | SoapClient.from_wsdl_string(wsdl_bytes) |
client.service.Op(a=1) |
client.service.Op(a=1) (or client.call("Op", a=1)) |
zeep.AsyncClient |
await client.call_async("Op", a=1) |
Transport(session=...) for TLS/timeouts |
HttpTransport(timeout=..., verify_ssl=..., client_cert=..., ca_bundle=...) |
Settings(strict=False) |
parse_wsdl(..., strict=False) for lenient import resolution |
Settings(forbid_external=True) |
the default — see the callout below |
from soapbar import SoapClient, HttpTransport
transport = HttpTransport(timeout=60.0, client_cert=("cert.pem", "key.pem"))
client = SoapClient(wsdl_url="https://example.com/service?wsdl", transport=transport)
result = client.service.Add(a=3, b=4)
Seeing the raw XML. zeep's HistoryPlugin has no direct equivalent;
soapbar logs full request and response envelopes at DEBUG on the
soapbar.client.client logger, with credentials redacted before they reach
the log record:
import logging
logging.getLogger("soapbar.client").setLevel(logging.DEBUG)
Plugins. zeep's ingress/egress plugin pipeline has no soapbar
equivalent. If you rewrite envelopes in a plugin today, the migration path is
wrapping HttpTransport.send (subclass it) — there is no hook API.
The _binding._operations workaround. When zeep cannot parse a response,
a common workaround reaches into client.service._binding._operations to
patch the operation. soapbar's supported form of the same move is explicit:
build the signature yourself and register it.
from soapbar import SoapClient
from soapbar.core.binding import OperationParameter, OperationSignature
from soapbar.core.types import xsd
client = SoapClient.manual("https://example.com/soap") # no WSDL at all
client.register_operation(OperationSignature(
name="Add",
input_params=[OperationParameter("a", xsd.resolve("int")),
OperationParameter("b", xsd.resolve("int"))],
output_params=[OperationParameter("return", xsd.resolve("int"))],
soap_action="Add",
))
What zeep has that soapbar does not: a WSDL/schema cache
(SqliteCache), the plugin pipeline, and a deeper XSD object model for
attribute-heavy schemas. If your integration leans on those, check the
known limitations page before committing.
spyne → soapbar (server)¶
| spyne | soapbar |
|---|---|
ServiceBase |
SoapService |
@rpc(Integer, Integer, _returns=Integer) |
@soap_operation() + Python type hints |
Application(services, tns, in_protocol=Soap11(...), ...) |
SoapApplication(service_url=...) + app.register(Svc()) |
WsgiApplication(app) |
WsgiSoapApp(soap_app) |
| ASGI | AsgiSoapApp(soap_app) (spyne has no ASGI adapter) |
?wsdl |
?wsdl (served by both adapters) |
from soapbar import AsgiSoapApp, SoapApplication, SoapService, soap_operation
class Calculator(SoapService):
__service_name__ = "Calculator"
__tns__ = "http://example.com/calc"
@soap_operation()
def add(self, a: int, b: int) -> int:
return a + b
soap_app = SoapApplication(service_url="https://example.com/soap")
soap_app.register(Calculator())
app = AsgiSoapApp(soap_app) # or WsgiSoapApp(soap_app)
Types come from annotations (int, str, float, bool, Decimal,
bytes, datetime/date/time) instead of spyne's model classes; complex
types are declared as ComplexXsdType rather than ComplexModel subclasses.
A verifiable fact rather than a pitch: spyne does not import on Python 3.12+ (its 2.14 release line predates the removal of long-deprecated stdlib APIs), so a spyne service cannot follow its interpreter past 3.11 today. soapbar supports Python 3.10–3.14.
What spyne has that soapbar does not: the multi-protocol dispatch
(the same service over HTTP-RPC, JSON-RPC, MessagePack). soapbar is a SOAP
library; the only non-SOAP surface is opt-in JSON responses for clients
sending Accept: application/json.
Behaviour differences that bite¶
- Remote WSDL imports are blocked by default.
parse_wsdlraises on anywsdl:import/xsd:importthat resolves tohttp(s)://unless you passallow_remote_imports=True— a WSDL that "just worked" in zeep can raiseValueErrorhere on first parse. This is deliberate (SSRF guard; see SECURITY.md); the opt-in restores the zeep behaviour for trusted sources. Redirects are never followed during import resolution, and remote fetches time out after 30 s. - Default binding style is document/literal wrapped (zeep's most common
case); RPC/literal, RPC/encoded, and bare document/literal are explicit
opt-ins via
__binding_style__. rpc:resultis off by default on RPC responses, because strict zeep rejects it;@soap_operation(emit_rpc_result=True)turns it on for peers that require SOAP 1.2 §4.2.1 form.- Serialized children are unqualified (
elementFormDefault="unqualified", declared honestly in the generated WSDL). Peers generated from the WSDL interoperate; hand-written payloads that qualify children via a defaultxmlnsare rejected oncevalidate_body_schema=Trueis on. - Fault status codes follow WS-I: SOAP 1.1 faults return HTTP 500
(including
Clientfaults, per R1126); SOAP 1.2Senderfaults return 400.
The client-side snippets on this page run against zeep 4.3.3; the spyne snippets follow its 2.14 documentation and are not executed by soapbar's CI, since spyne does not import on the interpreters the suite runs on (its own suite is exercised on Python ≤ 3.11 only).