API clients

Background

Open Forms interfaces over HTTP with a bunch of third party APIs/services, for example to:

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:

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 a soap.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)