REST adapter

The RESTAdapter base class - declarative API calls with response mapping and no boilerplate.

The REST adapter is a base class for services that call external REST APIs declaratively - a subclass of RESTAdapter names its connection and HTTP method as class attributes and implements no handle method. The adapter manages the connection, authentication, request serialization, response parsing and retries.

Basic usage

Subclass RESTAdapter and set conn_name to the name of an outgoing REST connection - this is the only required attribute. The adapter invokes the configured endpoint when the service runs and the response becomes self.response.payload:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import RESTAdapter

class GetFlight(RESTAdapter):
    """ Retrieves flight data from the scheduling system.
    """
    name = 'airport.adapter.get-flight'
    conn_name = 'flight.ops'

The default method is GET. For POST, PUT, DELETE or another HTTP method, set the method attribute:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import RESTAdapter

class UpdateRunwayStatus(RESTAdapter):
    """ Updates runway availability status.
    """
    name = 'airport.adapter.update-runway-status'
    conn_name = 'runway.ops'
    method = 'POST'

An adapter is used in two ways:

  • Invoke it from another service: an orchestration service calls the adapter through self.invoke, combines its result with data from other sources and returns one response - each adapter stays focused on one external system
  • Expose it on a REST channel: external clients call your endpoint, the adapter forwards the request to the external system, transforms the response and returns it, with no code beyond the adapter class
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetFlightStatus(Service):
    name = 'demo.rest.get-flight-status'

    def handle(self) -> 'None':
        flight = self.invoke('airport.adapter.get-flight', id=self.request.input.flight_id)
        self.response.payload = flight

Dynamic configuration

When request parameters depend on input data or configuration, override the matching get_* method - the adapter calls it on each invocation and uses what it returns:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import RESTAdapter

# The airport the weather API reports on when the caller names none
_default_airport = 'BIKF'

class GetWeatherData(RESTAdapter):
    """ Fetches weather data for airport operations.
    """
    name = 'airport.adapter.get-weather-data'
    conn_name = 'weather.api'

    def get_headers(self):
        airport_code = self.request.input.get('airport_code', _default_airport)
        return {
            'X-Request-ID': self.cid,
            'X-Airport-Code': airport_code,
        }

    def get_query_string(self, params):
        params['unit'] = 'metric'
        return params

Response mapping with models

The adapter deserializes JSON responses into data models. Set the model attribute to your Model class and the adapter parses the response into an instance of it, with type-safe access to every field:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Model, RESTAdapter

class AircraftModel(Model):
    id: int
    registration: str
    type_code: str

class GetAircraft(RESTAdapter):
    name = 'airport.adapter.get-aircraft'
    conn_name = 'fleet.api'
    model = AircraftModel

Custom response transformation

When the external format differs from your own - other field names, codes or structures - implement map_response. The method receives the parsed response data, already converted to your model class when one is set, and returns your canonical model:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Model, RESTAdapter

class ExternalFlightData(Model):
    flight_id: int
    carrier: str
    dep_time_utc: str
    status_code: str

class Flight(Model):
    id: int
    airline: str
    departure_time: str
    status: str

class GetFlightDetails(RESTAdapter):
    name = 'airport.adapter.get-flight-details'

    input = 'id'
    output = Flight

    has_query_string_id = True
    conn_name = 'Flight.Schedule'
    model = ExternalFlightData

    def map_response(self, data:'ExternalFlightData') -> 'Flight':

        # Configuration translates the external codes ..
        status = self.config.airport.status_codes[data.status_code]
        airline = self.config.airport.carriers[data.carrier]

        # .. and the canonical model carries the translated values.
        out = Flight()
        out.id = data.flight_id
        out.airline = airline
        out.departure_time = data.dep_time_utc
        out.status = status

        return out

Path parameters

When the outgoing connection's URL contains placeholders like /terminals/{terminal_id}/gates/{gate_id}, implement get_path_params to return the values. The adapter substitutes them into the URL, and parameters the path does not use become query string parameters:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import RESTAdapter

class GetGateAssignment(RESTAdapter):
    name = 'airport.adapter.get-gate-assignment'

    input = 'terminal_id', 'gate_id'
    conn_name = 'Gate.Management'
    method = 'GET'

    def get_path_params(self, params):
        return {
            'terminal_id': self.request.input.terminal_id,
            'gate_id': self.request.input.gate_id,
        }

Request data

For POST, PUT and PATCH requests, implement get_request to return the request body - the adapter serializes dicts and Model objects to JSON:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import RESTAdapter

class SubmitCargoManifest(RESTAdapter):
    name = 'airport.adapter.submit-cargo-manifest'

    input = 'manifest'
    conn_name = 'Cargo.System'
    method = 'POST'

    def get_request(self):
        return self.request.input.manifest

Base adapters

When several adapters share configuration - the same authentication, headers or conventions - move it into a base class the others inherit from. When the external system changes its authentication, you update the base class and every adapter follows:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import RESTAdapter

class BaseAirportAdapter(RESTAdapter):
    sec_def_name = 'Airport.OAuth.Token'

class GetRunway(BaseAirportAdapter):
    name = 'airport.adapter.get-runway'
    conn_name = 'Airfield.Runways'
    input = 'id'
    has_query_string_id = True

class GetTaxiway(BaseAirportAdapter):
    name = 'airport.adapter.get-taxiway'
    conn_name = 'Airfield.Taxiways'
    input = 'id'
    has_query_string_id = True

Invoke adapter services

Adapter services are regular Zato services - other services invoke them through self.invoke, with input parameters arriving through the standard self.request.input. The calling service receives the mapped result directly:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class SyncGateAssignments(Service):
    """ Assigns available gates to flights that have none.
    """
    name = 'demo.rest.sync-gate-assignments'

    def handle(self) -> 'None':

        flight_list = self.invoke('airport.adapter.get-flight-list')
        gate_list = self.invoke('airport.adapter.get-gate-list', status='available')

        for flight in flight_list:
            if flight.gate_id not in gate_list:
                self.invoke('airport.adapter.assign-gate', flight_id=flight.id)

Adapter services can also be exposed on REST channels, so external clients receive a transformed view of an external API directly.

All configuration options

Connection and method

NameDefaultDescription
conn_name''Name of the outgoing REST connection to use
method'GET'HTTP method (GET, POST, PUT, DELETE, PATCH, etc.)
get_conn_nameNoneCallable returning the connection name dynamically
get_methodNoneCallable returning the HTTP method dynamically

Request data

NameDefaultDescription
get_requestNoneCallable returning the request body data
get_query_stringNoneCallable returning query string parameters as a dict
get_path_paramsNoneCallable returning path parameters as a dict
get_headersNoneCallable returning HTTP headers as a dict
has_query_string_idFalseIf True, automatically adds an 'id' parameter to query string from input
query_string_id_paramNoneCustom name for the query string ID parameter (default: 'id')
has_json_idFalseIf True, uses JSON ID parameter
json_id_paramNoneCustom name for the JSON ID parameter

Authentication

NameDefaultDescription
sec_def_nameNoneName of the security definition to use
auth_scopes''OAuth scopes to request
get_sec_def_nameNoneCallable returning the security definition name dynamically
get_auth_scopesNoneCallable returning auth scopes dynamically
get_auth_bearerNoneCallable returning a bearer token to add to the Authorization header

Response handling

NameDefaultDescription
modelNoneA Model class to map the response to
map_responseNoneCallable to transform the response data
log_responseFalseWhether to log the response
needs_raw_responseFalseIf True, returns a RESTAdapterResponse with both parsed data and raw response

Retry configuration

NameDefaultDescription
max_retriesNoneHow many times a failed invocation is retried, a failure being a timeout, a connection error or an HTTP 429 response. The effective default is 0, i.e. no retries.
retry_sleep_timeNoneHow many seconds to sleep before the first retry. The effective default is 2.
retry_backoff_multiplierNoneEach retry sleeps this many times longer than the previous one, up to 8 seconds per a single sleep. The effective default is 2.
retry_backoff_thresholdNoneA cap on the total time spent sleeping between retries, in seconds - once reached, no more retries take place. It is not a retry count. The effective default is 60.

When an adapter leaves these attributes as None, the retry config of the underlying outgoing connection applies - the one set in the Dashboard under "More options" or through enmasse. Only when the connection has no retry config of its own do the defaults above take effect. Setting any of the attributes in an adapter subclass overrides both the connection's config and the defaults. To learn how the settings work, with examples, and how a Retry-After header is honoured, see retries.

See also

PageWhat it covers
Outgoing connectionsThe connections adapters name in conn_name
Data mappingThe transformation patterns map_response implements
Calling REST APIsInvoking connections directly, without an adapter
REST channelsExposing adapter services to external clients

Learn more