Skip to content

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 releases
  • tlm-cli pms devices set-model and tlm-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:

  1. Current working directory
  2. ~/.config/tlm-cli/
  3. /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 registryConfigRegistry.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 layerBaseApiInterface (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 orchestrationDeviceProvisioner (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.