Architecture¶
How tlm-cli connects to the ToloMEO Manager platform and how the codebase is structured.
Overview¶
tlm-cli is a command-line front-end for the ToloMEO Manager platform. It communicates
with several backend services, each responsible for a distinct domain. A shared authentication
layer and a single configuration load serve all clients.
Backend services¶
PMS — Product Management System¶
The PMS is the manufacturing record of truth. It holds:
- Parts and part families
- Devices (serial numbers, batch IDs, production data)
- Customers (with tenant IDs)
- Public and internal events
- Provisioning records (ConnHex IDs)
tlm-cli talks to the PMS via a JSON:API interface using the PMSClient class.
All tlm-cli pms commands route through this client.
Base URL: https://apis.<instance>/manufacturing
FM — Fleet Manager¶
The FM tracks deployed devices that have been claimed from the PMS. It holds:
- Claimed devices (serial numbers, names)
- Device status (firmware version, last seen, connectivity state)
tlm-cli uses the FMClient class for JSON:API access, and a separate ClaimClient for the
ownership (claiming) REST endpoint.
All tlm-cli fm commands route through these clients.
Base URL: https://apis.<instance>/fm
ToloMEO Manager (REST)¶
The ToloMEO Manager is the platform-level REST API. It handles:
- Customer creation and updates
- User and team management
- Permission management
- Device model assignments
- Bulk device provisioning and unprovisioning
All tlm-cli admin commands and the provisioning commands use this client.
Base URL: https://apis.<instance>/tolomeo
IoT API¶
The IoT API manages firmware models, firmware releases, and device things — the platform-side device identity that includes the init key. It is accessed via a plain REST client.
Used by:
tlm-cli pms models— list and show firmware models and releasestlm-cli pms devices set-modelandtlm-cli pms parts set-model— resolve model names to IDs- Provisioning — retrieve the init key for writing
agent.env
Base URL: https://apis.<instance>/iot
Authentication¶
All services share the same session, authenticated via Ory Kratos (the account service at
https://accounts.<instance>). On the first request, the KratosTokenProvider exchanges the
credentials from the configuration file for a session token. The token is cached to
~/.tlm-cli/token.json by FileTokenStorage and reused on subsequent calls, so authentication
happens at most once per session.
Configuration loading¶
On startup, tlm-cli searches for .config.toml in:
- Current working directory
~/.config/tlm-cli//etc/tlm-cli/
The file is read once and cached in ConfigRegistry — a process-level singleton. Every module
reads configuration through ConfigRegistry.get() without touching the filesystem again. The
instance hostname from [application] is used to derive all API base URLs.
See the Configuration File Reference for the full file format.
Code structure¶
tlm_cli/
├── main.py # Entry point — initialises the root Typer app
├── cli/ # Command definitions (Typer)
│ ├── app.py # Root app: registers admin, pms, fm, cs sub-apps
│ ├── context.py # Shared Typer context — lazily creates all API clients
│ ├── interactive.py # Interactive paginator (fetch 200, display 25 at a time)
│ ├── output.py # Rich-based output formatting helpers
│ ├── admin/ # tlm-cli admin commands (customers)
│ ├── pms/ # tlm-cli pms commands (devices, parts, events, models, provisioning)
│ ├── fm/ # tlm-cli fm commands (devices)
│ └── cs/ # tlm-cli cs commands (planned — no commands yet)
│
├── config/ # Configuration loading and registry
│ ├── loader.py # File search logic — cwd → ~/.config/tlm-cli/ → /etc/tlm-cli/
│ ├── models.py # AppConfig Pydantic model ([application] + [auth] sections)
│ └── registry.py # ConfigRegistry singleton — load once, read anywhere
│
├── auth/ # Authentication
│ ├── provider.py # KratosTokenProvider — authenticates against Ory Kratos
│ ├── storage.py # FileTokenStorage — caches token to ~/.tlm-cli/token.json
│ └── models.py # Token data models
│
├── http/ # Low-level authenticated HTTP (httpx)
│ ├── client.py # AuthenticatedClient + _SelfInitializingClient (lazy singleton)
│ ├── factory.py # Factory functions returning one client per backend endpoint
│ └── protocols.py # RestClientProtocol interface
│
├── core/ # Shared abstractions for JSON:API domains
│ ├── jsonapi/base.py # BaseApiModel — base JSON:API resource class
│ ├── interfaces/base.py # BaseApiInterface — generic CRUD interface (get, get_list, exists…)
│ └── handlers/base.py # BaseAttributeHandler — maps dict → JSON:API model attributes
│
├── builders/ # Query construction helpers for JSON:API requests
│ ├── filter.py # FilterBuilder — fluent builder for filter[] parameters
│ │ # supports MATCH, FUZZY_MATCH, MIN, MAX, EXISTS with AND/OR logic
│ └── query.py # ResourceQueryBuilder — wraps FilterBuilder + pagination + sorting
│
├── manufacturing/ # PMS domain
│ ├── client.py # PMSClient — typed entry point (device, customer, part, event…)
│ ├── auth/ # JSON:API session authenticated against Ory Kratos
│ ├── jsonapi/ # JSON:API resource classes (one file per resource type)
│ ├── models/ # Pydantic data models for PMS resources
│ ├── interfaces/ # Concrete BaseApiInterface subclasses for each resource
│ ├── handlers/ # Attribute handler implementations
│ └── provisioning/ # DeviceProvisioner — orchestrates PMS + IoT + ToloMEO clients
│
├── fleet/ # FM domain
│ ├── client.py # FMClient — typed entry point (claimed_device, status)
│ ├── auth/ # JSON:API session authenticated against Ory Kratos
│ ├── jsonapi/ # JSON:API resource classes (claimed_device, claimed_device_status)
│ ├── models/ # Pydantic data models for FM resources
│ ├── interfaces/ # Concrete BaseApiInterface subclasses
│ └── handlers/ # Attribute handler implementations
│
├── tolomeo/ # Pydantic models for ToloMEO Manager REST responses
└── iot/ # Pydantic models for IoT API responses
Key patterns¶
Config registry — ConfigRegistry.initialize() is called once in the root Typer callback.
All subsequent calls use ConfigRegistry.get(), so TOML parsing happens exactly once regardless
of how many commands are composed.
Self-initializing clients — _SelfInitializingClient in http/client.py is a lazy
singleton: it reads config and authenticates only on the first API call. The CLI context in
cli/context.py creates one instance per backend endpoint; commands receive them via the Typer
context object.
JSON:API interface layer — BaseApiInterface (in core/interfaces/) provides a uniform
get / get_list / exists / set_attributes API over any JSON:API resource. The
manufacturing/ and fleet/ packages each extend these abstractions with domain-specific
resource classes.
Provisioning as orchestration — DeviceProvisioner (in manufacturing/provisioning/) is
the one place where multiple clients converge. A single provision() call touches three
backends: PMS (JSON:API) to create the device record, ToloMEO Manager (REST) for bulk
provisioning, and the IoT API (REST) to retrieve the init key for the agent.env file.