Files
i7lib/README.md
T

6.5 KiB

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.

Versioning and the pyproject template

There is no pyproject.toml in this repository. It is generated from pyproject.toml.tmpl, and its version comes from the git tag that triggered the build:

version = "{{vless .TagName}}"

gitea-sidecar renders every *.tmpl file in the source to its name minus the suffix before building the wheel, so a tag of v0.1.1 produces version 0.1.1vless strips the leading v, which git wants and PEP 440 does not. .TagName and .CommitID are also available for anything else worth stamping.

The tag is therefore the only place a version is written. Do not add a hardcoded version back into the template: the build would keep producing that one version, and publishing a second tag would fail because the package file already exists.

Building locally needs the same render step, since the file the build tools look for is not in the repository:

sed 's/{{vless .TagName}}/0.0.0.dev0/' pyproject.toml.tmpl > pyproject.toml

pyproject.toml is gitignored, so a local copy never reaches a build.

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

Not every signal is available to every job. plant.state holds only the signals the job's sidecar exposes to it, keyed by both id and name. Which those are is configured for the job; a script does not declare them.

A signal that is not exposed is simply absent from plant.state — reading it does not raise. Guard with in or .get() rather than assuming a key is there.

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 the sidecar does not recognise one of the ids in it, the entire batch is rejected and none of the writes land. An id it does recognise but which is not writable is 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 own output first — that is where this library prints its errors. If nothing appears there, the write reached the platform and was rejected or dropped further in, which is visible only in the platform-side logs. Do not infer success from the absence of an exception.