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 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.