Customer-facing API reference for the three modules, written from source plus the live test-script job. Scoped to the API: the signals.yml and schedule.yaml contract gets only what a caller needs to read the API correctly, not a full treatment. Three behaviours documented because they are invisible from the source a customer reads: a write batch is all-or-nothing on an unknown id, run() skips logic_func entirely on a failed or empty read, and every method swallows its exceptions so a falsy return conflates failure with empty. The dbutils calls also set no timeout, unlike the signal calls. pyproject.toml is unchanged; README.md is already its readme field.
i7lib
Python library for scripts running in an i7 job container. It gives a script three things: plant signals (read and write) through the job's sidecar, the customer database through the db-controller, and millisecond time helpers.
Availability
Inside a job container the library is already importable — the scheduler mounts the SDK
folder and sets PYTHONPATH. Outside one, install the wheel published to your gitea PyPI
index. The only hard dependency is requests; python-dotenv is optional and loaded if
present, so a local .env can supply the two environment variables below.
Endpoints and environment
| Variable | Default | Injected value in a job |
|---|---|---|
SIDECAR_URL |
http://localhost:80 |
http://sidecar:80 |
DB_CONTROLLER_URL |
http://127.0.0.1:13213 |
http://sidecar:8081 |
The defaults are for running a script on your own machine. In a job both are set for you, so a script never hardcodes them.
Quick start
from i7lib import run
ID_OUTPUT = 49
def logic(plant):
temperature = plant.state.get("oven-temperature", 0.0)
batch = plant.new_write_batch()
batch.add(ID_OUTPUT, temperature * 2.0, "oven-output")
plant.send(batch)
if __name__ == "__main__":
run(logic, interval_ms=1000, offset_ms=200)
from i7lib import ... re-exports the public API; from i7lib.client import run and the
other submodule paths work equally well.
Signals
A signal is reachable from a script only if it is listed in the job's signals.yml, with
read: 1 to appear in plant.state and write: 1 to accept a write. Ids and names both
come from that file and must match the ids configured in i7state.
- { id: 21, name: 'oven-temperature', read: 1, write: 0 }
- { id: 49, name: 'oven-output', read: 0, write: 1 }
A signal missing from signals.yml, or listed with read: 0, is simply absent from
plant.state — it does not raise. Guard with in or .get() rather than assuming a key
exists.
i7lib.client
Plant
Holds the last state read from the sidecar.
| Attribute | Contents |
|---|---|
state |
value by signal id and by signal name, for every signal that has a name |
instants |
timestamp of each value, keyed the same way |
signals |
the raw list returned by the sidecar |
A signal present but carrying no value reads as nan; its instant defaults to 0.
read_all() — reads every subscribed signal once. Returns state, or {} on failure.
read_sync(interval_ms, offset_ms) — blocks until the sidecar's next sample aligned to
interval_ms, offset by offset_ms, then returns state, or {} on failure. This is the
call run uses; it is what keeps a script in step with the sample clock instead of drifting.
new_write_batch() — returns an empty WriteBatch.
send(batch) — posts the batch. Does nothing if the batch is empty.
WriteBatch
add(signal_id, value, name="") — queues one write. signal_id is coerced to int,
value to float, name to str.
A batch is all or nothing. If any id in it is not in the job's signals.yml, the sidecar
rejects the entire batch and none of the writes land. Ids that are known but not marked
write: 1 are skipped without failing the batch. Neither outcome is visible to the caller —
see Error handling below.
run(logic_func, interval_ms=1000, offset_ms=200)
The main loop. Creates a Plant, and on every cycle calls read_sync and then
logic_func(plant). It never returns.
Two behaviours to know. When a read fails or comes back empty the cycle is skipped, the loop
sleeps one second and logic_func is not called at all. And an exception raised inside
logic_func is caught and printed, then the loop sleeps one second and continues — a script
does not die on a bad tick, so a persistent bug shows up as repeated log lines rather than a
crash.
i7lib.dbutils
Database()
Talks to the db-controller at DB_CONTROLLER_URL.
| Method | Returns |
|---|---|
get_row(table, row_id) |
the row, or None |
insert(table, data) |
{'id': <new id>}, or None |
query(table, filters=None, order_by=None, limit=100) |
list of rows, or [] |
update(table, row_id, data) |
True / False |
list_tables() |
list of table names, or [] |
list_columns(table) |
list of columns, or [] |
Table and column names come from the customer schema, not from this library.
These calls set no timeout, unlike the signal calls, so a db-controller that accepts a
connection and then stalls will block the script. Keep database work off the hot path of a
fast interval_ms loop.
i7lib.timeutils
now() — current time in Unix milliseconds, the format used throughout the platform.
now_sec() — the same, floored to the second.
to_datetime(unix_milli) — converts back to a local datetime, for logging.
Error handling
Every method in this library catches its own exceptions, prints a line, and returns an empty or falsy result. Nothing raises, and no method reports which signal or row failed.
The practical consequences: read_all and read_sync returning {} mean "failed" and
"nothing subscribed" alike; query and list_tables returning [] mean "failed" and "no
rows" alike; send returning normally does not mean the writes were applied. A batch
rejected in full for one unknown id looks exactly like a successful send from inside the
script.
So when a value is not moving, check the job's stdout for the printed error first, then the sidecar and i7state logs, which record every write as applied, rejected or unknown. Do not infer success from the absence of an exception.