From 16b9a265d1a77286d24320144aa8c48093ed9d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20Etxaniz?= Date: Tue, 4 Aug 2026 09:36:27 +0200 Subject: [PATCH] Document the i7lib API 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. --- README.md | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eecc690..136e79b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,146 @@ # i7lib -i7 library for scripts \ No newline at end of file +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 + +```python +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. + +```yaml +- { 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': }`, 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.