labcode 0.1.5__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {labcode-0.1.5 → labcode-0.1.7}/.github/workflows/ci.yml +7 -7
  2. {labcode-0.1.5 → labcode-0.1.7}/.github/workflows/publish.yml +5 -5
  3. labcode-0.1.7/PKG-INFO +242 -0
  4. labcode-0.1.7/README.md +201 -0
  5. {labcode-0.1.5 → labcode-0.1.7}/SPECIFICATIONS.md +55 -9
  6. {labcode-0.1.5 → labcode-0.1.7}/examples/README.md +73 -39
  7. {labcode-0.1.5 → labcode-0.1.7}/examples/outputs/sila2_plate_cycle.boundary.yaml +2 -2
  8. {labcode-0.1.5 → labcode-0.1.7}/examples/outputs/sila2_plate_cycle.observation.yaml +20 -20
  9. {labcode-0.1.5 → labcode-0.1.7}/examples/outputs/sila2_plate_cycle.plan.yaml +19 -19
  10. labcode-0.1.7/examples/outputs/sila2_plate_cycle.svg +2 -0
  11. {labcode-0.1.5 → labcode-0.1.7}/examples/render_sila2_plate_cycle.py +1 -1
  12. {labcode-0.1.5 → labcode-0.1.7}/examples/run_sila2_plate_cycle.py +1 -1
  13. {labcode-0.1.5 → labcode-0.1.7}/examples/run_sila2_seal.py +1 -1
  14. {labcode-0.1.5 → labcode-0.1.7}/examples/sila2_plate_cycle.boundary.yaml +1 -1
  15. {labcode-0.1.5 → labcode-0.1.7}/examples/sila2_plate_cycle.wrapped.env.yaml +108 -72
  16. {labcode-0.1.5 → labcode-0.1.7}/examples/sila2_seal.boundary.yaml +1 -1
  17. {labcode-0.1.5 → labcode-0.1.7}/examples/sila2_seal.env.yaml +40 -26
  18. {labcode-0.1.5 → labcode-0.1.7}/examples/sila2_seal.wrapped.env.yaml +31 -17
  19. {labcode-0.1.5 → labcode-0.1.7}/labcode/_child.py +14 -1
  20. {labcode-0.1.5 → labcode-0.1.7}/labcode/backend.py +249 -18
  21. {labcode-0.1.5 → labcode-0.1.7}/labcode/cli.py +1 -1
  22. {labcode-0.1.5 → labcode-0.1.7}/labcode/dialect.py +8 -2
  23. {labcode-0.1.5 → labcode-0.1.7}/labcode/extension.py +59 -1
  24. labcode-0.1.7/labcode/otel.py +318 -0
  25. labcode-0.1.7/labcode/otel_sila2.py +68 -0
  26. labcode-0.1.7/labcode/record.py +298 -0
  27. {labcode-0.1.5 → labcode-0.1.7}/labcode/run_cli.py +146 -13
  28. {labcode-0.1.5 → labcode-0.1.7}/labcode/runner.py +74 -8
  29. labcode-0.1.7/labcode/sila2_instrument.py +537 -0
  30. labcode-0.1.7/labcode.egg-info/PKG-INFO +242 -0
  31. {labcode-0.1.5 → labcode-0.1.7}/labcode.egg-info/SOURCES.txt +10 -0
  32. labcode-0.1.7/labcode.egg-info/requires.txt +21 -0
  33. {labcode-0.1.5 → labcode-0.1.7}/labcode.egg-info/scm_file_list.json +10 -0
  34. labcode-0.1.7/labcode.egg-info/scm_version.json +8 -0
  35. {labcode-0.1.5 → labcode-0.1.7}/pyproject.toml +30 -9
  36. {labcode-0.1.5 → labcode-0.1.7}/tests/test_backend.py +54 -2
  37. {labcode-0.1.5 → labcode-0.1.7}/tests/test_dialect.py +29 -0
  38. labcode-0.1.7/tests/test_otel.py +230 -0
  39. labcode-0.1.7/tests/test_otel_child.py +169 -0
  40. labcode-0.1.7/tests/test_otel_sila2.py +165 -0
  41. labcode-0.1.7/tests/test_record.py +164 -0
  42. labcode-0.1.7/tests/test_recording.py +327 -0
  43. {labcode-0.1.5 → labcode-0.1.7}/tests/test_run_cli.py +226 -0
  44. labcode-0.1.7/tests/test_sila2_instrument.py +492 -0
  45. labcode-0.1.5/PKG-INFO +0 -104
  46. labcode-0.1.5/README.md +0 -69
  47. labcode-0.1.5/examples/outputs/sila2_plate_cycle.svg +0 -2
  48. labcode-0.1.5/labcode.egg-info/PKG-INFO +0 -104
  49. labcode-0.1.5/labcode.egg-info/requires.txt +0 -14
  50. labcode-0.1.5/labcode.egg-info/scm_version.json +0 -8
  51. {labcode-0.1.5 → labcode-0.1.7}/.gitignore +0 -0
  52. {labcode-0.1.5 → labcode-0.1.7}/LICENSE +0 -0
  53. {labcode-0.1.5 → labcode-0.1.7}/MANIFEST.in +0 -0
  54. {labcode-0.1.5 → labcode-0.1.7}/examples/outputs/plate_line.boundary.yaml +0 -0
  55. {labcode-0.1.5 → labcode-0.1.7}/examples/outputs/plate_line.observation.yaml +0 -0
  56. {labcode-0.1.5 → labcode-0.1.7}/examples/outputs/plate_line.plan.yaml +0 -0
  57. {labcode-0.1.5 → labcode-0.1.7}/examples/outputs/plate_line.svg +0 -0
  58. {labcode-0.1.5 → labcode-0.1.7}/examples/plate_line.boundary.yaml +0 -0
  59. {labcode-0.1.5 → labcode-0.1.7}/examples/plate_line.env.yaml +0 -0
  60. {labcode-0.1.5 → labcode-0.1.7}/examples/plate_line.workflow.yaml +0 -0
  61. {labcode-0.1.5 → labcode-0.1.7}/examples/render_plate_line.py +0 -0
  62. {labcode-0.1.5 → labcode-0.1.7}/examples/run_all_sila2_examples.py +0 -0
  63. {labcode-0.1.5 → labcode-0.1.7}/examples/sila2_plate_cycle.workflow.yaml +0 -0
  64. {labcode-0.1.5 → labcode-0.1.7}/examples/sila2_seal.workflow.yaml +0 -0
  65. {labcode-0.1.5 → labcode-0.1.7}/labcode/__init__.py +0 -0
  66. {labcode-0.1.5 → labcode-0.1.7}/labcode/__main__.py +0 -0
  67. {labcode-0.1.5 → labcode-0.1.7}/labcode/idgen.py +0 -0
  68. {labcode-0.1.5 → labcode-0.1.7}/labcode/objectid.py +0 -0
  69. {labcode-0.1.5 → labcode-0.1.7}/labcode/probe.py +0 -0
  70. {labcode-0.1.5 → labcode-0.1.7}/labcode/py.typed +0 -0
  71. {labcode-0.1.5 → labcode-0.1.7}/labcode/sila2.py +0 -0
  72. {labcode-0.1.5 → labcode-0.1.7}/labcode/sila2_commands.py +0 -0
  73. {labcode-0.1.5 → labcode-0.1.7}/labcode.egg-info/dependency_links.txt +0 -0
  74. {labcode-0.1.5 → labcode-0.1.7}/labcode.egg-info/entry_points.txt +0 -0
  75. {labcode-0.1.5 → labcode-0.1.7}/labcode.egg-info/top_level.txt +0 -0
  76. {labcode-0.1.5 → labcode-0.1.7}/setup.cfg +0 -0
  77. {labcode-0.1.5 → labcode-0.1.7}/tests/fixtures/device_script.env.yaml +0 -0
  78. {labcode-0.1.5 → labcode-0.1.7}/tests/fixtures/device_script.workflow.yaml +0 -0
  79. {labcode-0.1.5 → labcode-0.1.7}/tests/fixtures/reroute_device.env.yaml +0 -0
  80. {labcode-0.1.5 → labcode-0.1.7}/tests/fixtures/reroute_transporter.env.yaml +0 -0
  81. {labcode-0.1.5 → labcode-0.1.7}/tests/fixtures/transport.env.yaml +0 -0
  82. {labcode-0.1.5 → labcode-0.1.7}/tests/fixtures/transport.workflow.yaml +0 -0
  83. {labcode-0.1.5 → labcode-0.1.7}/tests/test_cli.py +0 -0
  84. {labcode-0.1.5 → labcode-0.1.7}/tests/test_objectid.py +0 -0
  85. {labcode-0.1.5 → labcode-0.1.7}/tests/test_probe.py +0 -0
  86. {labcode-0.1.5 → labcode-0.1.7}/tests/test_sila2.py +0 -0
  87. {labcode-0.1.5 → labcode-0.1.7}/tests/test_sila2_commands.py +0 -0
@@ -14,8 +14,8 @@ jobs:
14
14
  matrix:
15
15
  python-version: ["3.10", "3.11", "3.12", "3.13"]
16
16
  steps:
17
- - uses: actions/checkout@v4
18
- - uses: actions/setup-python@v5
17
+ - uses: actions/checkout@v7
18
+ - uses: actions/setup-python@v7
19
19
  with:
20
20
  python-version: ${{ matrix.python-version }}
21
21
  cache: pip
@@ -25,8 +25,8 @@ jobs:
25
25
  lint:
26
26
  runs-on: ubuntu-latest
27
27
  steps:
28
- - uses: actions/checkout@v4
29
- - uses: actions/setup-python@v5
28
+ - uses: actions/checkout@v7
29
+ - uses: actions/setup-python@v7
30
30
  with:
31
31
  python-version: "3.12"
32
32
  cache: pip
@@ -38,8 +38,8 @@ jobs:
38
38
  typecheck:
39
39
  runs-on: ubuntu-latest
40
40
  steps:
41
- - uses: actions/checkout@v4
42
- - uses: actions/setup-python@v5
41
+ - uses: actions/checkout@v7
42
+ - uses: actions/setup-python@v7
43
43
  with:
44
44
  python-version: "3.12"
45
45
  cache: pip
@@ -56,6 +56,6 @@ jobs:
56
56
  lock:
57
57
  runs-on: ubuntu-latest
58
58
  steps:
59
- - uses: actions/checkout@v4
59
+ - uses: actions/checkout@v7
60
60
  - uses: astral-sh/setup-uv@v7
61
61
  - run: uv lock --check
@@ -29,11 +29,11 @@ jobs:
29
29
  build:
30
30
  runs-on: ubuntu-latest
31
31
  steps:
32
- - uses: actions/checkout@v4
32
+ - uses: actions/checkout@v7
33
33
  with:
34
34
  # setuptools-scm derives the version from tags + history.
35
35
  fetch-depth: 0
36
- - uses: actions/setup-python@v5
36
+ - uses: actions/setup-python@v7
37
37
  with:
38
38
  python-version: "3.12"
39
39
  - run: python -m pip install --upgrade build twine
@@ -54,7 +54,7 @@ jobs:
54
54
  echo "::error::built version $built != tag $tag (ambiguous or misplaced tag?)"
55
55
  exit 1
56
56
  fi
57
- - uses: actions/upload-artifact@v4
57
+ - uses: actions/upload-artifact@v7
58
58
  with:
59
59
  name: dist
60
60
  path: dist/
@@ -69,7 +69,7 @@ jobs:
69
69
  permissions:
70
70
  id-token: write
71
71
  steps:
72
- - uses: actions/download-artifact@v4
72
+ - uses: actions/download-artifact@v8
73
73
  with:
74
74
  name: dist
75
75
  path: dist/
@@ -87,7 +87,7 @@ jobs:
87
87
  permissions:
88
88
  id-token: write
89
89
  steps:
90
- - uses: actions/download-artifact@v4
90
+ - uses: actions/download-artifact@v8
91
91
  with:
92
92
  name: dist
93
93
  path: dist/
labcode-0.1.7/PKG-INFO ADDED
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: labcode
3
+ Version: 0.1.7
4
+ Summary: labcode -- a dialect wrapper over the Object-Flow Programming Language toolchain
5
+ Author-email: Kazunari Kaizu <kwaizu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ofplang/labcode
8
+ Project-URL: Repository, https://github.com/ofplang/labcode
9
+ Keywords: ofplang,labcode,dataflow,workflow,cli
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Compilers
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: ofplang-validate>=0.1.3
24
+ Requires-Dist: ofplang-schedule>=0.1.3
25
+ Requires-Dist: ofplang-run>=0.1.13
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=7.0; extra == "test"
28
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.16; extra == "dev"
32
+ Requires-Dist: mypy>=1.11; extra == "dev"
33
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "dev"
34
+ Provides-Extra: sila2
35
+ Requires-Dist: sila2>=0.14; extra == "sila2"
36
+ Provides-Extra: otel
37
+ Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
38
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
39
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20; extra == "otel"
40
+ Dynamic: license-file
41
+
42
+ # labcode
43
+
44
+ [![CI](https://github.com/ofplang/labcode/actions/workflows/ci.yml/badge.svg)](https://github.com/ofplang/labcode/actions/workflows/ci.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/labcode.svg)](https://pypi.org/project/labcode/)
46
+
47
+ The **`lc`** command-line interface for the **labcode** dialect of the
48
+ **Object-flow Programming Language**. Installing this one package pulls in the
49
+ ofplang toolchain and exposes it under a single command:
50
+
51
+ ```sh
52
+ lc validate ... # check a workflow is well-formed portable v0
53
+ lc schedule ... # compute a schedule for a workflow
54
+ lc run ... # execute a workflow on the labcode backend
55
+ ```
56
+
57
+ labcode is where a site-specific dialect and a custom runner (real lab hardware)
58
+ are developed on top of the ofplang toolchain. `lc validate` and `lc schedule`
59
+ forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's own
60
+ runner**: it drives the workflow on the labcode backend, running each device
61
+ operation's script — supplied in the environment as an `x-labcode.script` extension
62
+ on a process mode — out-of-process on a wall clock, so a long-running real operation
63
+ never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
64
+ `x-labcode` extension.
65
+
66
+ ```yaml
67
+ # in the execution environment: how a (process, mode) is carried out
68
+ processes:
69
+ measure_od:
70
+ modes:
71
+ - id: v0
72
+ duration: 45
73
+ x-labcode:
74
+ script:
75
+ language: python
76
+ code: |
77
+ return {"od": read_plate(plate)}
78
+ ```
79
+
80
+ ## Install
81
+
82
+ ```sh
83
+ pip install labcode
84
+ ```
85
+
86
+ Requires Python 3.10+. `lc validate` and `lc schedule` are dispatched to the ofplang
87
+ sibling packages unchanged; `lc run` is this package's own runner, built on them:
88
+
89
+ - [`ofplang-validate`](https://github.com/ofplang/validate) — the validator
90
+ - [`ofplang-schedule`](https://github.com/ofplang/schedule) — the scheduler
91
+ - [`ofplang-run`](https://github.com/ofplang/run) — the runner / simulator
92
+
93
+ The language is defined in the [ofplang/spec](https://github.com/ofplang/spec)
94
+ repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.md).
95
+
96
+ What `lc run` brings of its own, beyond dispatching:
97
+
98
+ - **the labcode backend** — each device operation's `x-labcode.script` runs
99
+ out-of-process on a wall clock (§1.2, §1.3), so a real operation that takes minutes
100
+ does not block the replan loop, and an operation that never returns is stopped by
101
+ **`op_timeout`** (§1.8; `--op-timeout` / `--no-op-timeout`).
102
+ - **the dialect front door** — the environment's `x-labcode` extension is validated
103
+ before anything runs, on top of the portable-v0 check `lc validate` performs (§1, §2).
104
+ - **availability probing** — each machine is checked as often as its `probe` policy says,
105
+ and one that cannot be reached is taken out of the environment the scheduler plans
106
+ against, so the run routes around it (§1.5; `--no-probe`).
107
+ - **object identity** — the reserved `_id` view key is declared on Object types and minted
108
+ per object, so a physical thing can be followed through a run (§4).
109
+ - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
110
+ (§1.6). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
111
+ installed into whichever interpreter runs the scripts.
112
+ - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
113
+ traces: one trace per run, a span per operation, and a span per SiLA2 connection and
114
+ command measured inside the process that issued it. Off by default; the extra is
115
+ `pip install labcode[otel]`.
116
+
117
+ ## Usage
118
+
119
+ Each subcommand keeps its own options, exit codes, and `--help`:
120
+
121
+ ```sh
122
+ lc --help # top-level help
123
+ lc <command> --help # command-specific options
124
+ lc --version
125
+ ```
126
+
127
+ `lc` can also be run as a module: `python -m labcode <command> ...`.
128
+
129
+ `lc validate` and `lc schedule` are the ofplang siblings' own CLIs unchanged, so their
130
+ options are documented in those repositories. `lc run` is this package's own, and is
131
+ described below.
132
+
133
+ ### `lc run`
134
+
135
+ ```sh
136
+ lc run <workflow> --env <env>
137
+ [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
138
+ [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
139
+ [--trace] [--mission-id ID] [--object-ids seeded|real]
140
+ ```
141
+
142
+ - `<workflow>` — the portable v0 workflow: *what* happens.
143
+ - `--env` (required) — the labcode environment: the execution environment (spec §5) plus
144
+ the `x-labcode` extension saying *how* each operation is carried out.
145
+ - `--boundary DOC` — the whole-workflow I/O as one document: a `boundary:` mapping with a
146
+ `{spot, view}` descriptor per entry input / final output port. `spot` says where a
147
+ boundary Object sits; `view` supplies an input's value. A workflow with Object-bearing
148
+ entry inputs needs one, since each must be placed on a spot and only the operator knows
149
+ where the labware is.
150
+ - `-o OUT` — write the final execution status (spec §6/§7) here; the default is stdout.
151
+ - `--boundary-out FILE` — write the result boundary: the same schema as `--boundary`, with
152
+ each produced output's `view` filled in, including the `_id` its Object was minted with
153
+ (§4) — which is how one checks that the plate that came back is the plate that went in.
154
+ - `--observation-out FILE` — stream the observation document: each *completed* activity's
155
+ concrete input / output view values, appended as it finishes. What the instruments
156
+ reported, as against the status document's timings.
157
+ - `--seconds-per-tick S` — real seconds per environment time tick (default 20). Durations
158
+ in the environment are counted in ticks, and this is what maps them onto the wall clock.
159
+ The default is deliberately coarse, so that a real operation's dispatch → running →
160
+ completed reads as discrete, observable steps; a demo against a fast mock wants a small
161
+ value.
162
+ - `--op-timeout S` / `--no-op-timeout` — how long one operation may run before it is
163
+ stopped and failed (§1.8). The default is the environment root's `x-labcode.op_timeout`,
164
+ else 7200 real seconds. The two forms exclude each other.
165
+ - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
166
+ machine as reachable (§1.5). The documents are still validated.
167
+ - `--trace` — record what the run did (see below). Off by default.
168
+ - `--mission-id ID` — the campaign this run belongs to. Recorded with the run and given no
169
+ meaning by labcode: several runs may share one, and nothing here reads it back.
170
+ - `--object-ids seeded|real` — how Object `_id`s are minted: `seeded` is reproducible (the
171
+ same workflow yields the same ids every run, which is what keeps the examples' recorded
172
+ output stable), `real` is unique per run. Unset, it follows `--trace`.
173
+
174
+ Exit codes: `0` the run completed, `1` it failed (an activity failed, a contract was
175
+ violated, an operation timed out, or a replan became infeasible), `2` a usage or input
176
+ error — including a workflow or an `x-labcode` extension the front doors reject.
177
+
178
+ A complete invocation, against this repository's `examples/` (`--seconds-per-tick` small
179
+ because that example's scripts return instantly):
180
+
181
+ ```sh
182
+ lc run examples/plate_line.workflow.yaml --env examples/plate_line.env.yaml \
183
+ --boundary examples/plate_line.boundary.yaml --seconds-per-tick 0.2
184
+ ```
185
+
186
+ The remaining options tune the replan loop rather than describe the run —
187
+ `--poll-interval`, `--margin`, `--seed`, `--speed`, `--max-ticks`, `--no-validate` — and
188
+ are covered by `lc run --help`.
189
+
190
+ ### Recording a run
191
+
192
+ `--trace` records what the run did as OpenTelemetry traces. It needs the extra, in the
193
+ interpreter that drives the run — which is also the one that runs the scripts, since
194
+ labcode launches each with `sys.executable`:
195
+
196
+ ```sh
197
+ pip install 'labcode[otel]'
198
+ lc run <workflow> --env <env> --trace --mission-id M-2026-001
199
+ ```
200
+
201
+ One run is one trace, and the id it can be found by is printed to stderr as the run starts:
202
+
203
+ ```text
204
+ run mission.id, and the failure if it stopped on one
205
+ ├─ process Seal which node, process and mode; the plan's interval;
206
+ │ │ which Objects it handled
207
+ │ ├─ sila2.connect the address, measured in the process that connected
208
+ │ └─ sila2 SealerControl.Seal the command, from its start to its real completion
209
+ └─ transport the route and the transporter
210
+ ```
211
+
212
+ An operation's span is opened when it is dispatched and closed when a poll finds it
213
+ finished, so its end is late by up to one poll period; what an instrument actually spent is
214
+ in the command spans, which are measured where the commands are issued. `ofp.object.id`
215
+ lists the `_id`s an operation handled, including one it created — which is what makes
216
+ "everything that happened to this plate" a single query.
217
+
218
+ Where the record goes is configured by the **standard `OTEL_*` environment variables**
219
+ (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME`, …), so
220
+ labcode adds no settings of its own; `service.name` falls back to `labcode` if nothing sets
221
+ it. `LC_TRACE_FILE=path` additionally writes the spans to a file as JSON lines, one file per
222
+ process (the name gets the process id), which is how a record can be read without standing
223
+ up a collector.
224
+
225
+ Two things to know before pointing a run at a collector. **`--trace` makes Object `_id`s
226
+ real rather than reproducible** unless `--object-ids` says otherwise, since a reproducible id
227
+ is the same on every run and would collapse several runs' plates into one. And **a collector
228
+ that is configured but does not answer delays each operation**, by as long as
229
+ OpenTelemetry's own export timeout allows: labcode sets no timeout of its own, so
230
+ `OTEL_EXPORTER_OTLP_TIMEOUT` is the knob, and it belongs to whoever pointed the run there.
231
+
232
+ ## Examples
233
+
234
+ [`examples/`](examples/README.md) holds three worked runs: `plate_line`, an
235
+ Object-bearing line driven entirely by environment scripts and runnable with no
236
+ hardware, and `sila2_seal` and `sila2_plate_cycle`, which drive the reference lab's
237
+ SiLA2 servers for real. `sila2_seal` also walks through what a run does when a
238
+ machine stops answering — before an operation, and in the middle of one.
239
+
240
+ ## License
241
+
242
+ MIT
@@ -0,0 +1,201 @@
1
+ # labcode
2
+
3
+ [![CI](https://github.com/ofplang/labcode/actions/workflows/ci.yml/badge.svg)](https://github.com/ofplang/labcode/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/labcode.svg)](https://pypi.org/project/labcode/)
5
+
6
+ The **`lc`** command-line interface for the **labcode** dialect of the
7
+ **Object-flow Programming Language**. Installing this one package pulls in the
8
+ ofplang toolchain and exposes it under a single command:
9
+
10
+ ```sh
11
+ lc validate ... # check a workflow is well-formed portable v0
12
+ lc schedule ... # compute a schedule for a workflow
13
+ lc run ... # execute a workflow on the labcode backend
14
+ ```
15
+
16
+ labcode is where a site-specific dialect and a custom runner (real lab hardware)
17
+ are developed on top of the ofplang toolchain. `lc validate` and `lc schedule`
18
+ forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's own
19
+ runner**: it drives the workflow on the labcode backend, running each device
20
+ operation's script — supplied in the environment as an `x-labcode.script` extension
21
+ on a process mode — out-of-process on a wall clock, so a long-running real operation
22
+ never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
23
+ `x-labcode` extension.
24
+
25
+ ```yaml
26
+ # in the execution environment: how a (process, mode) is carried out
27
+ processes:
28
+ measure_od:
29
+ modes:
30
+ - id: v0
31
+ duration: 45
32
+ x-labcode:
33
+ script:
34
+ language: python
35
+ code: |
36
+ return {"od": read_plate(plate)}
37
+ ```
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ pip install labcode
43
+ ```
44
+
45
+ Requires Python 3.10+. `lc validate` and `lc schedule` are dispatched to the ofplang
46
+ sibling packages unchanged; `lc run` is this package's own runner, built on them:
47
+
48
+ - [`ofplang-validate`](https://github.com/ofplang/validate) — the validator
49
+ - [`ofplang-schedule`](https://github.com/ofplang/schedule) — the scheduler
50
+ - [`ofplang-run`](https://github.com/ofplang/run) — the runner / simulator
51
+
52
+ The language is defined in the [ofplang/spec](https://github.com/ofplang/spec)
53
+ repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.md).
54
+
55
+ What `lc run` brings of its own, beyond dispatching:
56
+
57
+ - **the labcode backend** — each device operation's `x-labcode.script` runs
58
+ out-of-process on a wall clock (§1.2, §1.3), so a real operation that takes minutes
59
+ does not block the replan loop, and an operation that never returns is stopped by
60
+ **`op_timeout`** (§1.8; `--op-timeout` / `--no-op-timeout`).
61
+ - **the dialect front door** — the environment's `x-labcode` extension is validated
62
+ before anything runs, on top of the portable-v0 check `lc validate` performs (§1, §2).
63
+ - **availability probing** — each machine is checked as often as its `probe` policy says,
64
+ and one that cannot be reached is taken out of the environment the scheduler plans
65
+ against, so the run routes around it (§1.5; `--no-probe`).
66
+ - **object identity** — the reserved `_id` view key is declared on Object types and minted
67
+ per object, so a physical thing can be followed through a run (§4).
68
+ - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
69
+ (§1.6). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
70
+ installed into whichever interpreter runs the scripts.
71
+ - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
72
+ traces: one trace per run, a span per operation, and a span per SiLA2 connection and
73
+ command measured inside the process that issued it. Off by default; the extra is
74
+ `pip install labcode[otel]`.
75
+
76
+ ## Usage
77
+
78
+ Each subcommand keeps its own options, exit codes, and `--help`:
79
+
80
+ ```sh
81
+ lc --help # top-level help
82
+ lc <command> --help # command-specific options
83
+ lc --version
84
+ ```
85
+
86
+ `lc` can also be run as a module: `python -m labcode <command> ...`.
87
+
88
+ `lc validate` and `lc schedule` are the ofplang siblings' own CLIs unchanged, so their
89
+ options are documented in those repositories. `lc run` is this package's own, and is
90
+ described below.
91
+
92
+ ### `lc run`
93
+
94
+ ```sh
95
+ lc run <workflow> --env <env>
96
+ [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
97
+ [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
98
+ [--trace] [--mission-id ID] [--object-ids seeded|real]
99
+ ```
100
+
101
+ - `<workflow>` — the portable v0 workflow: *what* happens.
102
+ - `--env` (required) — the labcode environment: the execution environment (spec §5) plus
103
+ the `x-labcode` extension saying *how* each operation is carried out.
104
+ - `--boundary DOC` — the whole-workflow I/O as one document: a `boundary:` mapping with a
105
+ `{spot, view}` descriptor per entry input / final output port. `spot` says where a
106
+ boundary Object sits; `view` supplies an input's value. A workflow with Object-bearing
107
+ entry inputs needs one, since each must be placed on a spot and only the operator knows
108
+ where the labware is.
109
+ - `-o OUT` — write the final execution status (spec §6/§7) here; the default is stdout.
110
+ - `--boundary-out FILE` — write the result boundary: the same schema as `--boundary`, with
111
+ each produced output's `view` filled in, including the `_id` its Object was minted with
112
+ (§4) — which is how one checks that the plate that came back is the plate that went in.
113
+ - `--observation-out FILE` — stream the observation document: each *completed* activity's
114
+ concrete input / output view values, appended as it finishes. What the instruments
115
+ reported, as against the status document's timings.
116
+ - `--seconds-per-tick S` — real seconds per environment time tick (default 20). Durations
117
+ in the environment are counted in ticks, and this is what maps them onto the wall clock.
118
+ The default is deliberately coarse, so that a real operation's dispatch → running →
119
+ completed reads as discrete, observable steps; a demo against a fast mock wants a small
120
+ value.
121
+ - `--op-timeout S` / `--no-op-timeout` — how long one operation may run before it is
122
+ stopped and failed (§1.8). The default is the environment root's `x-labcode.op_timeout`,
123
+ else 7200 real seconds. The two forms exclude each other.
124
+ - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
125
+ machine as reachable (§1.5). The documents are still validated.
126
+ - `--trace` — record what the run did (see below). Off by default.
127
+ - `--mission-id ID` — the campaign this run belongs to. Recorded with the run and given no
128
+ meaning by labcode: several runs may share one, and nothing here reads it back.
129
+ - `--object-ids seeded|real` — how Object `_id`s are minted: `seeded` is reproducible (the
130
+ same workflow yields the same ids every run, which is what keeps the examples' recorded
131
+ output stable), `real` is unique per run. Unset, it follows `--trace`.
132
+
133
+ Exit codes: `0` the run completed, `1` it failed (an activity failed, a contract was
134
+ violated, an operation timed out, or a replan became infeasible), `2` a usage or input
135
+ error — including a workflow or an `x-labcode` extension the front doors reject.
136
+
137
+ A complete invocation, against this repository's `examples/` (`--seconds-per-tick` small
138
+ because that example's scripts return instantly):
139
+
140
+ ```sh
141
+ lc run examples/plate_line.workflow.yaml --env examples/plate_line.env.yaml \
142
+ --boundary examples/plate_line.boundary.yaml --seconds-per-tick 0.2
143
+ ```
144
+
145
+ The remaining options tune the replan loop rather than describe the run —
146
+ `--poll-interval`, `--margin`, `--seed`, `--speed`, `--max-ticks`, `--no-validate` — and
147
+ are covered by `lc run --help`.
148
+
149
+ ### Recording a run
150
+
151
+ `--trace` records what the run did as OpenTelemetry traces. It needs the extra, in the
152
+ interpreter that drives the run — which is also the one that runs the scripts, since
153
+ labcode launches each with `sys.executable`:
154
+
155
+ ```sh
156
+ pip install 'labcode[otel]'
157
+ lc run <workflow> --env <env> --trace --mission-id M-2026-001
158
+ ```
159
+
160
+ One run is one trace, and the id it can be found by is printed to stderr as the run starts:
161
+
162
+ ```text
163
+ run mission.id, and the failure if it stopped on one
164
+ ├─ process Seal which node, process and mode; the plan's interval;
165
+ │ │ which Objects it handled
166
+ │ ├─ sila2.connect the address, measured in the process that connected
167
+ │ └─ sila2 SealerControl.Seal the command, from its start to its real completion
168
+ └─ transport the route and the transporter
169
+ ```
170
+
171
+ An operation's span is opened when it is dispatched and closed when a poll finds it
172
+ finished, so its end is late by up to one poll period; what an instrument actually spent is
173
+ in the command spans, which are measured where the commands are issued. `ofp.object.id`
174
+ lists the `_id`s an operation handled, including one it created — which is what makes
175
+ "everything that happened to this plate" a single query.
176
+
177
+ Where the record goes is configured by the **standard `OTEL_*` environment variables**
178
+ (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME`, …), so
179
+ labcode adds no settings of its own; `service.name` falls back to `labcode` if nothing sets
180
+ it. `LC_TRACE_FILE=path` additionally writes the spans to a file as JSON lines, one file per
181
+ process (the name gets the process id), which is how a record can be read without standing
182
+ up a collector.
183
+
184
+ Two things to know before pointing a run at a collector. **`--trace` makes Object `_id`s
185
+ real rather than reproducible** unless `--object-ids` says otherwise, since a reproducible id
186
+ is the same on every run and would collapse several runs' plates into one. And **a collector
187
+ that is configured but does not answer delays each operation**, by as long as
188
+ OpenTelemetry's own export timeout allows: labcode sets no timeout of its own, so
189
+ `OTEL_EXPORTER_OTLP_TIMEOUT` is the knob, and it belongs to whoever pointed the run there.
190
+
191
+ ## Examples
192
+
193
+ [`examples/`](examples/README.md) holds three worked runs: `plate_line`, an
194
+ Object-bearing line driven entirely by environment scripts and runnable with no
195
+ hardware, and `sila2_seal` and `sila2_plate_cycle`, which drive the reference lab's
196
+ SiLA2 servers for real. `sila2_seal` also walks through what a run does when a
197
+ machine stops answering — before an operation, and in the middle of one.
198
+
199
+ ## License
200
+
201
+ MIT
@@ -306,7 +306,7 @@ x-labcode:
306
306
  - transporter: arm
307
307
  from: plateloc.stage
308
308
  to: thermal_cycler.block
309
- duration: 8
309
+ duration: 43
310
310
  x-labcode:
311
311
  script:
312
312
  language: python
@@ -317,9 +317,10 @@ x-labcode:
317
317
 
318
318
  cycler = sila2_clients["thermal_cycler"].AutomatedThermalCyclerController
319
319
  settle(cycler.OpenLid(), "OpenLid")
320
- arm = sila2_client.TrolleyArmProvider # the transporter: still the first client
321
- arm.Pick(LocationSpecifier="plateloc.stage")
322
- arm.Place(LocationSpecifier="thermal-cycler.block")
320
+ # The transporter is still the first client. Its own names for the places it
321
+ # serves are stations, not `device.spot`, so a route writes them out.
322
+ labware = sila2_client.LabwareService
323
+ settle(labware.Transfer(SourceStation="Base4", DestinationStation="Base6"), "Transfer")
323
324
  ```
324
325
  - **Connections last one operation**, opened before the code runs and closed after it — on
325
326
  any exit, including a `return` or an exception, and including a *later* connection
@@ -367,8 +368,11 @@ equally available to a `raw` script, and stays visible in the code that depends
367
368
  `settle` that times out fails the *operation* while the instrument carries on. Whatever
368
369
  state that leaves the lab in is the operator's to restore, as for any operation that
369
370
  failed part way. The default timeout is therefore generous rather than tight: its purpose
370
- is to turn a hang into a diagnosable failure, since nothing else in the stack bounds an
371
- operation's running time.
371
+ is to turn a hang into a diagnosable failure.
372
+ - **It is the inner of two limits.** This one is per command, chosen by the script that
373
+ knows what it is waiting for, and its failure can name the command that hung. The outer
374
+ one (§1.8) is per operation and lab-wide, and catches the hangs no script is watching
375
+ for. The outer default is looser than this one, so where both apply this is what fires.
372
376
  - **Its timeout is in real seconds**, and is unrelated to the mode's `duration` — which is
373
377
  an *estimate*, in environment time, for scheduling. A schedule's estimate is not a
374
378
  deadline, and `--seconds-per-tick` does not rescale the timeout.
@@ -379,8 +383,9 @@ equally available to a `raw` script, and stays visible in the code that depends
379
383
 
380
384
  ### 1.7 Where an `x-labcode` may appear
381
385
 
382
- The positions of §1 are the only ones: the environment **root** (`probe` defaults only),
383
- `processes.<p>.modes[]`, `transports[]`, `devices[]` and `transporters[]`. An `x-labcode`
386
+ The positions of §1 are the only ones: the environment **root** (`probe` defaults and
387
+ `op_timeout`), `processes.<p>.modes[]`, `transports[]`, `devices[]` and
388
+ `transporters[]`. An `x-labcode`
384
389
  anywhere else in the environment — on a process, beside `time` — is an **error**, as is a
385
390
  key at a position that does not define it (a `connection` at the root, a `probe` on a mode).
386
391
  Nothing would read it, and `ofplang-schedule` tolerates an `x-` key at *every* position
@@ -390,6 +395,45 @@ This rule covers the environment only. An `x-labcode` in the **workflow** is not
390
395
  that document is portable v0, read by other implementations, and what extension keys it
391
396
  carries is not labcode's business.
392
397
 
398
+ ### 1.8 Operation timeout — `op_timeout`
399
+
400
+ How long **one operation** may run before labcode stops waiting for it, in **real
401
+ seconds**. It lives at the environment root and nowhere else:
402
+
403
+ ```yaml
404
+ x-labcode:
405
+ op_timeout: 7200 # a positive number of real seconds (the default)
406
+ # op_timeout: null # or: wait as long as it takes
407
+ ```
408
+
409
+ - `op_timeout` MUST be a positive, finite number, or `null` for **no limit**. `0` is not a
410
+ way to say "no limit" and is an error; a machine may not declare one (a per-machine key
411
+ is an unknown key, §1.1).
412
+ - **One value for the whole lab.** The fine-grained waits belong to the scripts, which know
413
+ what they are waiting for (`settle`, §1.6.1); this value only has to clear the longest
414
+ operation the lab legitimately runs. Its default (7200 s) is twice the `settle` default,
415
+ so where both apply the inner one — which can name the command — fires first.
416
+ - **The clock is real seconds**, from the moment the operation starts, covering everything
417
+ it does: connecting, every command it issues, and its own waiting. It is unrelated to the
418
+ mode's `duration` (an *estimate*, in environment time, for scheduling) and is not
419
+ rescaled by `--seconds-per-tick`.
420
+ - **What happens when it expires**: the operation's child process is stopped and the
421
+ operation **fails** with the reason code `op_timeout`. That is an ordinary graceful
422
+ failure — the run stops, the status document is written, the reason is reported, the exit
423
+ code is 1 — which is the point: without a limit, an instrument that stops answering
424
+ leaves a run polling with *no* status document and no reason at all.
425
+ - **A timeout is not a cancel**, exactly as in §1.6.1: nothing here can stop a command the
426
+ instrument has already accepted. It keeps running, and the state that leaves behind —
427
+ including material a transport was part way through moving — is the operator's to
428
+ restore. The run stops there, so labcode's own picture of the lab is not relied on
429
+ afterwards.
430
+ - The machine that hung is **not** treated as unavailable: `op_timeout` does not add it to
431
+ the down machines (§1.5), because "not answering" is not "not there", and re-routing work
432
+ onto other machines while this one is still physically running its command would make the
433
+ lab less consistent, not more.
434
+ - `lc run` overrides it for one run: `--op-timeout SECONDS`, or `--no-op-timeout` for no
435
+ limit at all. The order is flag, then document, then default.
436
+
393
437
  ## 2. Code source resolution and exclusivity
394
438
 
395
439
  For a dispatched `(process, mode)`, labcode resolves the code to run in this order:
@@ -412,7 +456,9 @@ Each dispatched operation runs in its own child process (real, wall-clock-paced)
412
456
  runner discovers completion by polling, so a multi-minute computation never blocks it.
413
457
  The advisory `duration` is the scheduler's estimate; the real duration is the script's.
414
458
  A script error (an exception, a wrong/ missing output name, a non-conformant value) is a
415
- graceful runtime failure (§22.2): the operation ends `failed` and the run stops.
459
+ graceful runtime failure (§22.2): the operation ends `failed` and the run stops. An
460
+ operation that never finishes at all ends the same way once it passes `op_timeout` (§1.8) —
461
+ polling for completion is not the same as waiting forever for it.
416
462
 
417
463
  Cadence: the nominal poll period is `poll_interval × seconds_per_tick`. labcode defaults
418
464
  `seconds_per_tick` to ~20 s (so a real op is polled at an observable cadence, not