Zato lets you keep your own configuration in .ini files that your services can access instantly from memory. This is where you store API endpoints, mapping tables, business rules, and any other values that shouldn't be hardcoded.
Configuration files go in the config/user-conf directory of your project on the host machine. When you deploy with Docker, this directory is mounted into the container, so Zato can read the files at startup. Each file becomes accessible through self.config in your services, using the file name (without extension) as the first key.
For instance, if you create a file called airports.ini, in your service it will be is accessible as self.config.airports, and sections will additional keys, so [api] with timeout = 30 becomes self.config.airports.api.timeout.
On your host machine, create an .ini file in your project's config/user-conf directory. For example, config/user-conf/airports.ini:
[api]
base_url = https://api.airport-ops.example.com
timeout = 30
max_retries = 3
[security]
token_sec_def_name = Airport.Ops.Token
[terminals]
domestic = T1
international = T2
cargo = T3
[[gate_prefixes]]
T1 = A
T2 = B
T3 = C
The file uses a simple structure:
[section_name][[subsection]], triple [[[deeper]]], and so on for arbitrary nesting depthkey = value#In your service, access the configuration through self.config using dot notation. The first key is the file name (without .ini), followed by section names and keys:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class GetFlightInfo(Service):
def handle(self):
# Access simple values
base_url = self.config.airports.api.base_url
timeout = self.config.airports.api.timeout
# Access nested sections
t1_prefix = self.config.airports.terminals.gate_prefixes.T1
self.logger.info('Using API at %s with timeout %s', base_url, timeout)
self.logger.info('Terminal 1 gate prefix: %s', t1_prefix)
You can nest sections as deep as you need. Each level of nesting in the .ini file becomes another level of dot access in your code.
Each configuration section also behaves like a Python dictionary. You can iterate over keys, look up values with square brackets, check for key existence, and use all the standard dict methods:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class ListTerminals(Service):
def handle(self):
terminals = self.config.airports.terminals
# Iterate over all keys
for terminal_name in terminals:
terminal_code = terminals[terminal_name]
self.logger.info('Terminal %s -> %s', terminal_name, terminal_code)
# Check if a key exists
if 'cargo' in terminals:
cargo_code = terminals['cargo']
self.logger.info('Cargo terminal: %s', cargo_code)
# Get with default
vip_terminal = terminals.get('vip', 'T1')
# Get all keys
all_terminal_names = list(terminals.keys())
# Get all values
all_terminal_codes = list(terminals.values())
# Get all items
for name, code in terminals.items():
self.logger.info('%s = %s', name, code)
This means you can use configuration sections both for direct access when you know the key names, and for dynamic lookups when you need to iterate or search.
Zato automatically converts configuration values to appropriate Python types.
For instance, let's say you have this config:
[api]
base_url = https://api.airport-ops.example.com
timeout = 30
max_retries = 3
debug_mode = False
[terminals]
active_gates = A1, A2, A3, B1, B2
gate_capacity = {'A1': 200, 'A2': 150, 'B1': 300}
Here's how it would be converted:
timeout = 30 becomes Python int 30debug_mode = False becomes Python bool Falseactive_gates = A1, A2, A3, B1, B2 becomes Python list ['A1', 'A2', 'A3', 'B1', 'B2']gate_capacity = {'A1': 200, 'A2': 150, 'B1': 300} becomes Python dictbase_url = https://api.airport-ops.example.com remains a Python strNested sections become nested objects. Each level of brackets adds another level of dot access, so you can organize complex configuration hierarchically and access it naturally in code.
[terminal_t1]
[[gates]]
[[[gate_a1]]]
capacity = 200
airlines = FI, AF
[[[gate_a2]]]
capacity = 150
airlines = BA
In your service:
self.config.airports.terminal_t1 - the [terminal_t1] sectionself.config.airports.terminal_t1.gates - the [[gates]] subsectionself.config.airports.terminal_t1.gates.gate_a1 - the [[[gate_a1]]] subsectionself.config.airports.terminal_t1.gates.gate_a1.capacity - the value 200 (as int)self.config.airports.terminal_t1.gates.gate_a1.airlines - the list ['FI', 'AF']A common pattern is storing mapping tables - translating codes from external systems to your internal values:
[mappings]
[[aircraft_types]]
738 = Boeing 737-800
763 = Boeing 767-300
388 = Airbus A380-800
320 = Airbus A320
321 = Airbus A321
[[delay_reasons]]
WX = Weather
TC = Technical
OP = Operational
SC = Security
CR = Crew
[[airline_codes]]
FI = Icelandair
AF = Air France
BA = British Airways
Use these mappings in your service:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class TranslateFlightData(Service):
def handle(self):
# Get the mapping tables
aircraft_types = self.config.airports.mappings.aircraft_types
delay_reasons = self.config.airports.mappings.delay_reasons
# Translate codes
aircraft_code = '738'
aircraft_name = aircraft_types[aircraft_code]
delay_code = 'WX'
delay_description = delay_reasons[delay_code]
self.logger.info('Aircraft: %s', aircraft_name)
self.logger.info('Delay reason: %s', delay_description)
For sensitive values like API keys, passwords, and tokens, don't put them directly in the config file. Instead, reference environment variables using the $ prefix:
[credentials]
api_key = $Airport_API_Key
client_secret = $Airport_Client_Secret
[database]
password = $Airport_DB_Password
When Zato loads the configuration, it automatically replaces $Variable_Name with the value from the corresponding environment variable. If the variable isn't set, a warning is logged.
In your service, you access these values exactly the same way as any other config value:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class ConnectToAirportAPI(Service):
def handle(self):
# These come from environment variables, but you access them the same way
api_key = self.config.airports.credentials.api_key
client_secret = self.config.airports.credentials.client_secret
# Use them in your API calls
self.logger.info('Connecting with API key: %s...', api_key[:4])
When deploying with Docker, you pass environment variables to the container and mount your configuration directory. The DevOps deployment tutorial covers this in detail.
The typical setup:
/opt/hot-deploy/yourproject/config/user-conf inside the container-e flags or through an env.ini file mounted at /opt/hot-deploy/enmasse/env.iniExample from a deployment script:
docker run \
--mount type=bind,source=$host_root_dir/config/user-conf,target=$target/myproject/config/user-conf \
--mount type=bind,source=$host_root_dir/config/auto-generated/env.ini,target=$target/enmasse/env.ini \
$package_address
The env.ini file contains the environment variables that your config files reference:
[env]
Airport_API_Key=your-api-key-here
Airport_Client_Secret=your-secret-here
Airport_DB_Password=your-password-here
This separation means your configuration structure is in version control, but the actual secrets are injected at deployment time from your secrets management system.
Here's a more complete example showing how configuration fits into a real service that integrates with an airport operations API:
[api]
base_url = https://api.airport-ops.example.com
timeout = 30
token_sec_def_name = Airport.Ops.Token
[defaults]
days_to_check = 7
max_results = 100
[terminals]
domestic = T1
international = T2
[[gate_ranges]]
T1 = A1-A20
T2 = B1-B40
[credentials]
api_key = $Airport_API_Key
# -*- coding: utf-8 -*-
# stdlib
from dataclasses import dataclass
# Zato
from zato.server.service import Model, Service
@dataclass(init=False)
class FlightSearchRequest(Model):
terminal: str
date: str
@dataclass(init=False)
class Flight(Model):
flight_id: str
gate: str
status: str
class SearchFlights(Service):
input = FlightSearchRequest
def handle(self):
request:'FlightSearchRequest' = self.request.input
# Get configuration
config = self.config.airports
# Build API request
base_url = config.api.base_url
timeout = int(config.api.timeout)
max_results = int(config.defaults.max_results)
# Translate terminal name to code
terminal_code = config.terminals[request.terminal]
# Get gate range for this terminal
gate_range = config.terminals.gate_ranges[terminal_code]
self.logger.info('Searching flights in terminal %s (gates %s)',
terminal_code, gate_range)
# Make the API call
conn = self.out.rest['Airport Ops API'].conn
api_response = conn.get(self.cid, params={
'terminal': terminal_code,
'date': request.date,
'limit': max_results
})
flights_data = api_response.data
# Process results
flights = []
for item in flights_data:
flight = Flight()
flight.flight_id = item['FlightID']
flight.gate = item['Gate']
flight.status = item['Status']
flights.append(flight)
self.response.payload = {'flights': flights}