API clients¶
Background¶
Open Forms interfaces over HTTP with a bunch of third party APIs/services, for example to:
fetch prefill data
register submission data
perform service fetch
There are different flavours of interaction - JSON services (including REST), but also SOAP (XML), StUF (SOAP/XML-based standard) and unknowns that the future may bring.
In the Dutch (local) government landscape, there are some common patterns in how you connect with these services:
mutual TLS (mTLS)
basic auth credentials (username/password)
API key
Oauth2-based flows
IP allowlists
Combinations of these patterns are possible too!
Open Forms uses an abstraction that accounts for these variations while allowing developers to focus on the actual consuming of the service, packaged into the library ape-pie.
Because it extends the core requests
API, usage should feel familiar.
You are encouraged to define your own service-specific subclasses to modify behaviour where needed.
Configuration factories¶
Configuration factories are a small abstraction that allow you to instantiate clients with the appropriate configuration/presets from sources holding the configuration details - for example database records.
Such a factory must implemented the ape_pie.ConfigAdapter
protocol.
Some examples that can serve as a reference:
soap.client.session_factory.SessionFactory
stuf.service_client_factory.ServiceClientFactory
Reference¶
ZGW-consumers (JSON-based/RESTful services)¶
See the zgw-consumers documentation.
Zeep (SOAP client)¶
Zeep supports a session
keyword argument for its transport, which is plug and play
with our base client.
- class soap.client.SOAPSession(base_url: str, request_kwargs: Optional[dict[str, Any]] = None, **kwargs: Any)¶
- to_absolute_url(maybe_relative_url: str) str ¶
Disable base URL validation.
SOAP services are typically configured with a WSDL which describes the bindings, and the base URL specified is maybe not relevant at all.
- soap.client.build_client(service: soap.models.SoapService, transport_factory=<class 'zeep.transports.Transport'>, client_factory=<class 'zeep.client.Client'>, **kwargs) zeep.client.Client ¶
Build a
zeep.Client
instance from asoap.models.SoapService
conf.The mTLS and authentication parameters are taken from the service configuration and configured on the session, which is then used as transport for the zeep client.
Any additional kwargs are passed through to the
zeep.Client
instantiation.
StUF (template based SOAP/XML)¶
Provide a StUF client base class.
StUF is an information exchange message format defined by VNG/GEMMA. It extends SOAP/XML, in particular by providing a base XML schema and XSDs to validate these schema’s. Domain “koppelvlakken” use StUF as base by further extending it with domain-specific schema’s for the actual content/information being exchanged.
The base class here provides the shared mechanisms for StUF v3 that are domain-agnostic. Whenever you are implementing a particular StUF integration, you are expected to subclass the base class and implement your domain specific logic in your own class.
- class stuf.client.BaseClient(base_url: str, request_kwargs: typing.Optional[dict[str, typing.Any]] = None, *, soap_version: soap.constants.SOAPVersion = SOAPVersion.soap12, endpoints: dict[stuf.constants.EndpointType | str, str], wss_security: stuf.stuf.WSSecurity, stuurgegevens: stuf.stuf.StuurGegevens, request_log_hook: stuf.client.LoggingHook = <function noop_log>, response_log_hook: stuf.client.LoggingHook = <function noop_log>)¶
A base client with
requests.Session
’s interface.The base client provides the mechanisms to support connection pooling. Opt-in to this behaviour by using the client as a context manager:
>>> with MyClient.configure_from(stuf_service) as client: >>> client.do_the_thing()
This client provides the generic template context for the StUF/SOAP envelopes. Ideally, you would render an XML template which extends the base template, focusing on your sector/domain specific markup. For example:
{% extends "stuf/soap_envelope.xml" %}{% load stuf %} {% block body %} <SN:operation xmlns:SN="sector-namespace" {additionalnamespaces used} > <SN:stuurgegevens> <StUF:berichtcode>Lk01</StUF:berichtcode> {% render_stuurgegevens stuurgegevens referentienummer %} <StUF:entiteittype>ZAK</StUF:entiteittype> </SN:stuurgegevens> ... </SN:operation> {% endblock %}
- build_base_context() dict[str, Any] ¶
Create the base context derived from the dynamic service configuration.
- sector_alias: str = ''¶
The sector/domain code for you concrete subclass.
Must be set by the subclass, example value are ‘bg’ or ‘zkn’. This is used in building up the
SOAPAction
HTTP header.
- soap_security_expires_minutes: int¶
Specify how long a SOAP request is valid after creation.
Used in the Security element of the SOAP envelope. Must be set by subclass.
- templated_request(soap_action: str, template: str, context: Optional[dict[str, Any]] = None, endpoint_type: stuf.constants.EndpointType = EndpointType.vrije_berichten) requests.models.Response ¶
Make a request by templating out a template with the provided context.
The context is merged with the base context and the resolved template is rendered into a string, suitable to be passed down to
request()
.
- class stuf.client.LoggingHook(*args, **kwargs)¶