Config tables
Translate each party's codes into yours and validate accepted values.
Every party you integrate with has its own codes for the same things. One wellness application records a body weight measurement as WEIGHT-KG, another records it as BODY-WT, and the standard records it as 29463-7. One mobile operator reports an active subscriber as IN_SERVICE, another as ACTIVE.
Config tables are the .ini files where you keep those mappings, plus two methods that use them. They are ordinary configuration files in config/user-conf, reached through self.config like any other - hot-reloaded, editable in the dashboard and behaving like Python dictionaries.
The two methods answer the questions that come up whenever codes cross a boundary:
- Is this value one we accept? -
validate - What does this party's value mean here? -
translate
The two kinds of file
A file with a section called [codes] is a list - the values you accept, each with what it means. This is your own vocabulary, or a standard one you have loaded:
# config/user-conf/statuses.ini
[codes]
ACTIVE = Active and billable
SUSPENDED = Suspended by billing
DEACTIVATED = Deactivated, number released
A file with any other section names is a mapping table - one section per source, holding that source's codes and what each of them means to you:
# config/user-conf/partners.ini
[NORDIC_MOBILE]
ACTIVE = ACTIVE
BARRED = SUSPENDED
CLOSED = DEACTIVATED
[ALPINE_TELECOM]
IN_SERVICE = ACTIVE
NON_PAYMENT = SUSPENDED
CANCELLED = DEACTIVATED
[codes] is the only section name Zato treats specially, and it is what tells the two kinds of file apart. Everything else is a name you choose.
Validating a value
validate answers whether a value is in the list, and nothing else:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class UpdateSubscriber(Service):
def handle(self):
status = self.request.payload['status']
if not self.config.statuses.validate(status):
self.logger.warning('Status `%s` is not one we accept', status)
return
self.logger.info('Status `%s` accepted', status)
The meaning of a code needs no method of its own - the list is a plain configuration section, so you read it the way you read any other:
Translating a value
translate takes the source the value came in from and the value itself, and hands back what it means to you:
self.config.partners- which table to readsource- which section of it, i.e. whose codes these arecode- the value to look up
Add codes and the result is checked against a list before you get it, which is how you make sure a mapping table cannot quietly produce a value the rest of your system does not know:
status = self.config.partners.translate(source='ALPINE_TELECOM', code='NON_PAYMENT', codes='statuses')
Leave codes out and nothing is checked, which is what you want when the translation is all you are after.
Translating for a target
A value that came in from one party usually goes out to another, and that other party has codes of its own. Add target and the same table is read the other way round - the value is looked up among what the target sends, and you get the code the target knows it by:
status = self.config.partners.translate(source='ALPINE_TELECOM', code='NON_PAYMENT', target='NORDIC_MOBILE')
ALPINE_TELECOM sends NON_PAYMENT, the file has SUSPENDED for it, NORDIC_MOBILE has SUSPENDED under BARRED, so BARRED is what comes back. Both parties are ordinary sections of the same file, so nothing is configured twice - the section you read forwards is the section someone else reads backwards.
The values in the middle are whatever the file itself uses. They may be your own vocabulary, they may be a standard, and with two systems and nothing else in the file they may be the words of one of the two:
# config/user-conf/systems.ini
[SYSTEM_1]
A1 = ACTIVE
B2 = SUSPENDED
[SYSTEM_2]
ACT = ACTIVE
SUS = SUSPENDED
SUS comes back, and adding a third system later is a section of its own rather than a change to the two that are already there.
You get None when the target has no section in the table and when nothing in its section holds the value, exactly as without a target.
One value under several codes
A target may keep one value under more than one of its own codes:
SUSPENDED is now both BARRED and FROZEN, and there is no way to tell which one that party expects, so translate says so instead of picking one:
# Zato
from zato.common.user_config import AmbiguousTarget
try:
status = self.config.partners.translate(source='ALPINE_TELECOM', code='NON_PAYMENT', target='NORDIC_MOBILE')
except AmbiguousTarget as e:
self.logger.warning('Cannot translate: %s', e)
The message names the file, the target, the value and the codes it turned out to be under:
Either the target's section keeps one code for the value, or the service reads the section itself and decides which of them to use. In the dashboard, the same lookup shows every code the value is under, so the conflict is visible while the file is being edited.
Values that are not there
There are two outcomes and no third one - either you get a value you can use, or you get None. You get None when the source has no section in the table, when the code is not in that section, and when codes was given and the translated value is not in that list:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class ProcessStatusUpdate(Service):
def handle(self):
partner = self.request.payload['partner']
incoming_status = self.request.payload['status']
status = self.config.partners.translate(source=partner, code=incoming_status, codes='statuses')
if status is None:
self.logger.warning('No mapping for `%s` from `%s`', incoming_status, partner)
self.response.payload = {'is_accepted': False}
return
self.logger.info('`%s` from `%s` is `%s` here', incoming_status, partner, status)
self.response.payload = {'is_accepted': True, 'status': status}
Because the original value is never overwritten - you keep it in a variable of its own and the translation goes into another - what arrived is still there to log, to store, or to send back in an error message.
Healthcare - wellness measurements in your own terms
A measurement recorded at a routine check-up arrives in an HL7 v2 message as whatever the sending application calls it. The standard you record in is LOINC, so you keep the LOINC codes you use as a list, and one section per sending application as the mapping table:
# config/user-conf/loinc.ini
[codes]
8302-2 = Body height
29463-7 = Body weight
39156-5 = Body mass index
8867-4 = Heart rate
# config/user-conf/observations.ini
[WELLNESS_APP]
HEIGHT-CM = 8302-2
WEIGHT-KG = 29463-7
BMI = 39156-5
PULSE-BPM = 8867-4
[CHECKUP_KIOSK]
BODY-HT = 8302-2
BODY-WT = 29463-7
In a service handling the message, the sending application in MSH-3 is the source, and the observation identifier in OBX-3 is the code:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
if 0:
from zato.hl7v2.base import HL7Message
class RecordObservation(Service):
def handle(self) -> 'None':
message:'HL7Message' = self.request.input
# Who sent the message is who the codes belong to
sending_application = message.msh.sending_application
local_code = message.obx.observation_identifier.identifier
loinc_code = self.config.observations.translate(
source=sending_application, code=local_code, codes='loinc')
if loinc_code is None:
self.logger.warning('`%s` from `%s` has no LOINC code', local_code, sending_application)
return
loinc_name = self.config.loinc.codes[loinc_code]
self.logger.info('`%s` is `%s` (%s)', local_code, loinc_code, loinc_name)
Adding an application is adding a section to observations.ini. No service changes, no deployment.
Telecommunications - subscriber status from roaming partners
The same two files do the same job in a network operator's back office. Your own subscriber states are the list, each roaming partner's states are a section:
# config/user-conf/statuses.ini
[codes]
ACTIVE = Active and billable
SUSPENDED = Suspended by billing
DEACTIVATED = Deactivated, number released
# config/user-conf/partners.ini
[NORDIC_MOBILE]
ACTIVE = ACTIVE
BARRED = SUSPENDED
CLOSED = DEACTIVATED
[ALPINE_TELECOM]
IN_SERVICE = ACTIVE
NON_PAYMENT = SUSPENDED
CANCELLED = DEACTIVATED
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class SyncSubscriberStatus(Service):
def handle(self):
partner = self.request.payload['partner_id']
subscriber_id = self.request.payload['subscriber_id']
partner_status = self.request.payload['status']
status = self.config.partners.translate(source=partner, code=partner_status, codes='statuses')
if status is None:
self.logger.warning('Partner `%s` sent status `%s`, which is not mapped',
partner, partner_status)
self.response.payload = {'is_accepted': False, 'received_status': partner_status}
return
# Billing only ever sees your own vocabulary
conn = self.rest['Billing API']
_ = conn.post(self.cid, {'subscriber_id': subscriber_id, 'status': status})
self.response.payload = {'is_accepted': True, 'status': status}
A partner that renames its states, or a new partner joining, is a change to one file and to nothing else.
Edit the files in the dashboard
Config tables are managed in the dashboard under Services > Config tables, and Config tables in the dashboard describes the screen in full. The files are listed on the left and the one you are looking at fills the rest of the page - what a service reads it as, what it holds, the file itself, and a strip that runs a value through it so you can see the answer before you save.

Files too large to work on in a browser are downloaded, changed in your own tools and uploaded again. Saving a file makes it live on all servers without a restart, exactly like any other config/user-conf file.
See also
| Feature | What it does |
|---|---|
| Config files | The .ini files config tables are built on |
| Config tables in the dashboard | The editor screen described in full |
| Messages | Build the payloads the translated codes go into |