labcode 0.1.6__tar.gz → 0.1.8__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 (88) hide show
  1. {labcode-0.1.6 → labcode-0.1.8}/.github/workflows/ci.yml +7 -7
  2. {labcode-0.1.6 → labcode-0.1.8}/.github/workflows/publish.yml +5 -5
  3. labcode-0.1.8/PKG-INFO +258 -0
  4. labcode-0.1.8/README.md +214 -0
  5. {labcode-0.1.6 → labcode-0.1.8}/SPECIFICATIONS.md +5 -4
  6. {labcode-0.1.6 → labcode-0.1.8}/examples/README.md +61 -39
  7. {labcode-0.1.6 → labcode-0.1.8}/examples/outputs/sila2_plate_cycle.boundary.yaml +2 -2
  8. {labcode-0.1.6 → labcode-0.1.8}/examples/outputs/sila2_plate_cycle.observation.yaml +20 -20
  9. {labcode-0.1.6 → labcode-0.1.8}/examples/outputs/sila2_plate_cycle.plan.yaml +19 -19
  10. labcode-0.1.8/examples/outputs/sila2_plate_cycle.svg +2 -0
  11. {labcode-0.1.6 → labcode-0.1.8}/examples/render_sila2_plate_cycle.py +1 -1
  12. {labcode-0.1.6 → labcode-0.1.8}/examples/run_sila2_plate_cycle.py +1 -1
  13. {labcode-0.1.6 → labcode-0.1.8}/examples/run_sila2_seal.py +1 -1
  14. {labcode-0.1.6 → labcode-0.1.8}/examples/sila2_plate_cycle.boundary.yaml +1 -1
  15. {labcode-0.1.6 → labcode-0.1.8}/examples/sila2_plate_cycle.wrapped.env.yaml +108 -72
  16. {labcode-0.1.6 → labcode-0.1.8}/examples/sila2_seal.boundary.yaml +1 -1
  17. {labcode-0.1.6 → labcode-0.1.8}/examples/sila2_seal.env.yaml +40 -26
  18. {labcode-0.1.6 → labcode-0.1.8}/examples/sila2_seal.wrapped.env.yaml +31 -17
  19. {labcode-0.1.6 → labcode-0.1.8}/labcode/_child.py +14 -1
  20. {labcode-0.1.6 → labcode-0.1.8}/labcode/backend.py +220 -19
  21. {labcode-0.1.6 → labcode-0.1.8}/labcode/cli.py +1 -1
  22. labcode-0.1.8/labcode/otel.py +322 -0
  23. labcode-0.1.8/labcode/otel_sila2.py +182 -0
  24. labcode-0.1.8/labcode/record.py +298 -0
  25. {labcode-0.1.6 → labcode-0.1.8}/labcode/run_cli.py +102 -11
  26. {labcode-0.1.6 → labcode-0.1.8}/labcode/runner.py +62 -5
  27. labcode-0.1.8/labcode/sila2_instrument.py +585 -0
  28. labcode-0.1.8/labcode.egg-info/PKG-INFO +258 -0
  29. {labcode-0.1.6 → labcode-0.1.8}/labcode.egg-info/SOURCES.txt +11 -0
  30. labcode-0.1.8/labcode.egg-info/requires.txt +24 -0
  31. {labcode-0.1.6 → labcode-0.1.8}/labcode.egg-info/scm_file_list.json +11 -0
  32. labcode-0.1.8/labcode.egg-info/scm_version.json +8 -0
  33. {labcode-0.1.6 → labcode-0.1.8}/pyproject.toml +45 -4
  34. {labcode-0.1.6 → labcode-0.1.8}/tests/test_backend.py +2 -2
  35. labcode-0.1.8/tests/test_otel.py +230 -0
  36. labcode-0.1.8/tests/test_otel_child.py +209 -0
  37. labcode-0.1.8/tests/test_otel_grpc.py +153 -0
  38. labcode-0.1.8/tests/test_otel_sila2.py +165 -0
  39. labcode-0.1.8/tests/test_record.py +164 -0
  40. labcode-0.1.8/tests/test_recording.py +327 -0
  41. {labcode-0.1.6 → labcode-0.1.8}/tests/test_run_cli.py +150 -0
  42. labcode-0.1.8/tests/test_sila2_instrument.py +542 -0
  43. labcode-0.1.6/PKG-INFO +0 -104
  44. labcode-0.1.6/README.md +0 -69
  45. labcode-0.1.6/examples/outputs/sila2_plate_cycle.svg +0 -2
  46. labcode-0.1.6/labcode.egg-info/PKG-INFO +0 -104
  47. labcode-0.1.6/labcode.egg-info/requires.txt +0 -14
  48. labcode-0.1.6/labcode.egg-info/scm_version.json +0 -8
  49. {labcode-0.1.6 → labcode-0.1.8}/.gitignore +0 -0
  50. {labcode-0.1.6 → labcode-0.1.8}/LICENSE +0 -0
  51. {labcode-0.1.6 → labcode-0.1.8}/MANIFEST.in +0 -0
  52. {labcode-0.1.6 → labcode-0.1.8}/examples/outputs/plate_line.boundary.yaml +0 -0
  53. {labcode-0.1.6 → labcode-0.1.8}/examples/outputs/plate_line.observation.yaml +0 -0
  54. {labcode-0.1.6 → labcode-0.1.8}/examples/outputs/plate_line.plan.yaml +0 -0
  55. {labcode-0.1.6 → labcode-0.1.8}/examples/outputs/plate_line.svg +0 -0
  56. {labcode-0.1.6 → labcode-0.1.8}/examples/plate_line.boundary.yaml +0 -0
  57. {labcode-0.1.6 → labcode-0.1.8}/examples/plate_line.env.yaml +0 -0
  58. {labcode-0.1.6 → labcode-0.1.8}/examples/plate_line.workflow.yaml +0 -0
  59. {labcode-0.1.6 → labcode-0.1.8}/examples/render_plate_line.py +0 -0
  60. {labcode-0.1.6 → labcode-0.1.8}/examples/run_all_sila2_examples.py +0 -0
  61. {labcode-0.1.6 → labcode-0.1.8}/examples/sila2_plate_cycle.workflow.yaml +0 -0
  62. {labcode-0.1.6 → labcode-0.1.8}/examples/sila2_seal.workflow.yaml +0 -0
  63. {labcode-0.1.6 → labcode-0.1.8}/labcode/__init__.py +0 -0
  64. {labcode-0.1.6 → labcode-0.1.8}/labcode/__main__.py +0 -0
  65. {labcode-0.1.6 → labcode-0.1.8}/labcode/dialect.py +0 -0
  66. {labcode-0.1.6 → labcode-0.1.8}/labcode/extension.py +0 -0
  67. {labcode-0.1.6 → labcode-0.1.8}/labcode/idgen.py +0 -0
  68. {labcode-0.1.6 → labcode-0.1.8}/labcode/objectid.py +0 -0
  69. {labcode-0.1.6 → labcode-0.1.8}/labcode/probe.py +0 -0
  70. {labcode-0.1.6 → labcode-0.1.8}/labcode/py.typed +0 -0
  71. {labcode-0.1.6 → labcode-0.1.8}/labcode/sila2.py +0 -0
  72. {labcode-0.1.6 → labcode-0.1.8}/labcode/sila2_commands.py +0 -0
  73. {labcode-0.1.6 → labcode-0.1.8}/labcode.egg-info/dependency_links.txt +0 -0
  74. {labcode-0.1.6 → labcode-0.1.8}/labcode.egg-info/entry_points.txt +0 -0
  75. {labcode-0.1.6 → labcode-0.1.8}/labcode.egg-info/top_level.txt +0 -0
  76. {labcode-0.1.6 → labcode-0.1.8}/setup.cfg +0 -0
  77. {labcode-0.1.6 → labcode-0.1.8}/tests/fixtures/device_script.env.yaml +0 -0
  78. {labcode-0.1.6 → labcode-0.1.8}/tests/fixtures/device_script.workflow.yaml +0 -0
  79. {labcode-0.1.6 → labcode-0.1.8}/tests/fixtures/reroute_device.env.yaml +0 -0
  80. {labcode-0.1.6 → labcode-0.1.8}/tests/fixtures/reroute_transporter.env.yaml +0 -0
  81. {labcode-0.1.6 → labcode-0.1.8}/tests/fixtures/transport.env.yaml +0 -0
  82. {labcode-0.1.6 → labcode-0.1.8}/tests/fixtures/transport.workflow.yaml +0 -0
  83. {labcode-0.1.6 → labcode-0.1.8}/tests/test_cli.py +0 -0
  84. {labcode-0.1.6 → labcode-0.1.8}/tests/test_dialect.py +0 -0
  85. {labcode-0.1.6 → labcode-0.1.8}/tests/test_objectid.py +0 -0
  86. {labcode-0.1.6 → labcode-0.1.8}/tests/test_probe.py +0 -0
  87. {labcode-0.1.6 → labcode-0.1.8}/tests/test_sila2.py +0 -0
  88. {labcode-0.1.6 → labcode-0.1.8}/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.8/PKG-INFO ADDED
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: labcode
3
+ Version: 0.1.8
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
+ Requires-Dist: opentelemetry-instrumentation-grpc[instruments]>=0.41b0; extra == "test"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.16; extra == "dev"
33
+ Requires-Dist: mypy>=1.11; extra == "dev"
34
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "dev"
35
+ Requires-Dist: opentelemetry-instrumentation-grpc[instruments]>=0.41b0; extra == "dev"
36
+ Provides-Extra: sila2
37
+ Requires-Dist: sila2>=0.14; extra == "sila2"
38
+ Provides-Extra: otel
39
+ Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
40
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
41
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20; extra == "otel"
42
+ Requires-Dist: opentelemetry-instrumentation-grpc>=0.41b0; extra == "otel"
43
+ Dynamic: license-file
44
+
45
+ # labcode
46
+
47
+ [![CI](https://github.com/ofplang/labcode/actions/workflows/ci.yml/badge.svg)](https://github.com/ofplang/labcode/actions/workflows/ci.yml)
48
+ [![PyPI](https://img.shields.io/pypi/v/labcode.svg)](https://pypi.org/project/labcode/)
49
+
50
+ The **`lc`** command-line interface for the **labcode** dialect of the
51
+ **Object-flow Programming Language**. Installing this one package pulls in the
52
+ ofplang toolchain and exposes it under a single command:
53
+
54
+ ```sh
55
+ lc validate ... # check a workflow is well-formed portable v0
56
+ lc schedule ... # compute a schedule for a workflow
57
+ lc run ... # execute a workflow on the labcode backend
58
+ ```
59
+
60
+ labcode is where a site-specific dialect and a custom runner (real lab hardware)
61
+ are developed on top of the ofplang toolchain. `lc validate` and `lc schedule`
62
+ forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's own
63
+ runner**: it drives the workflow on the labcode backend, running each device
64
+ operation's script — supplied in the environment as an `x-labcode.script` extension
65
+ on a process mode — out-of-process on a wall clock, so a long-running real operation
66
+ never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
67
+ `x-labcode` extension.
68
+
69
+ ```yaml
70
+ # in the execution environment: how a (process, mode) is carried out
71
+ processes:
72
+ measure_od:
73
+ modes:
74
+ - id: v0
75
+ duration: 45
76
+ x-labcode:
77
+ script:
78
+ language: python
79
+ code: |
80
+ return {"od": read_plate(plate)}
81
+ ```
82
+
83
+ ## Install
84
+
85
+ ```sh
86
+ pip install labcode
87
+ ```
88
+
89
+ Requires Python 3.10+. `lc validate` and `lc schedule` are dispatched to the ofplang
90
+ sibling packages unchanged; `lc run` is this package's own runner, built on them:
91
+
92
+ - [`ofplang-validate`](https://github.com/ofplang/validate) — the validator
93
+ - [`ofplang-schedule`](https://github.com/ofplang/schedule) — the scheduler
94
+ - [`ofplang-run`](https://github.com/ofplang/run) — the runner / simulator
95
+
96
+ The language is defined in the [ofplang/spec](https://github.com/ofplang/spec)
97
+ repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.md).
98
+
99
+ What `lc run` brings of its own, beyond dispatching:
100
+
101
+ - **the labcode backend** — each device operation's `x-labcode.script` runs
102
+ out-of-process on a wall clock (§1.2, §1.3), so a real operation that takes minutes
103
+ does not block the replan loop, and an operation that never returns is stopped by
104
+ **`op_timeout`** (§1.8; `--op-timeout` / `--no-op-timeout`).
105
+ - **the dialect front door** — the environment's `x-labcode` extension is validated
106
+ before anything runs, on top of the portable-v0 check `lc validate` performs (§1, §2).
107
+ - **availability probing** — each machine is checked as often as its `probe` policy says,
108
+ and one that cannot be reached is taken out of the environment the scheduler plans
109
+ against, so the run routes around it (§1.5; `--no-probe`).
110
+ - **object identity** — the reserved `_id` view key is declared on Object types and minted
111
+ per object, so a physical thing can be followed through a run (§4).
112
+ - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
113
+ (§1.6). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
114
+ installed into whichever interpreter runs the scripts.
115
+ - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
116
+ traces: one trace per run, a span per operation, and — measured inside the process that
117
+ issued them — a span per SiLA2 connection, per command, and per gRPC call underneath
118
+ either. Off by default; the extra is `pip install labcode[otel]`.
119
+
120
+ ## Usage
121
+
122
+ Each subcommand keeps its own options, exit codes, and `--help`:
123
+
124
+ ```sh
125
+ lc --help # top-level help
126
+ lc <command> --help # command-specific options
127
+ lc --version
128
+ ```
129
+
130
+ `lc` can also be run as a module: `python -m labcode <command> ...`.
131
+
132
+ `lc validate` and `lc schedule` are the ofplang siblings' own CLIs unchanged, so their
133
+ options are documented in those repositories. `lc run` is this package's own, and is
134
+ described below.
135
+
136
+ ### `lc run`
137
+
138
+ ```sh
139
+ lc run <workflow> --env <env>
140
+ [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
141
+ [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
142
+ [--trace] [--mission-id ID] [--object-ids seeded|real]
143
+ ```
144
+
145
+ - `<workflow>` — the portable v0 workflow: *what* happens.
146
+ - `--env` (required) — the labcode environment: the execution environment (spec §5) plus
147
+ the `x-labcode` extension saying *how* each operation is carried out.
148
+ - `--boundary DOC` — the whole-workflow I/O as one document: a `boundary:` mapping with a
149
+ `{spot, view}` descriptor per entry input / final output port. `spot` says where a
150
+ boundary Object sits; `view` supplies an input's value. A workflow with Object-bearing
151
+ entry inputs needs one, since each must be placed on a spot and only the operator knows
152
+ where the labware is.
153
+ - `-o OUT` — write the final execution status (spec §6/§7) here; the default is stdout.
154
+ - `--boundary-out FILE` — write the result boundary: the same schema as `--boundary`, with
155
+ each produced output's `view` filled in, including the `_id` its Object was minted with
156
+ (§4) — which is how one checks that the plate that came back is the plate that went in.
157
+ - `--observation-out FILE` — stream the observation document: each *completed* activity's
158
+ concrete input / output view values, appended as it finishes. What the instruments
159
+ reported, as against the status document's timings.
160
+ - `--seconds-per-tick S` — real seconds per environment time tick (default 20). Durations
161
+ in the environment are counted in ticks, and this is what maps them onto the wall clock.
162
+ The default is deliberately coarse, so that a real operation's dispatch → running →
163
+ completed reads as discrete, observable steps; a demo against a fast mock wants a small
164
+ value.
165
+ - `--op-timeout S` / `--no-op-timeout` — how long one operation may run before it is
166
+ stopped and failed (§1.8). The default is the environment root's `x-labcode.op_timeout`,
167
+ else 7200 real seconds. The two forms exclude each other.
168
+ - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
169
+ machine as reachable (§1.5). The documents are still validated.
170
+ - `--trace` — record what the run did (see below). Off by default.
171
+ - `--mission-id ID` — the campaign this run belongs to. Recorded with the run and given no
172
+ meaning by labcode: several runs may share one, and nothing here reads it back.
173
+ - `--object-ids seeded|real` — how Object `_id`s are minted: `seeded` is reproducible (the
174
+ same workflow yields the same ids every run, which is what keeps the examples' recorded
175
+ output stable), `real` is unique per run. Unset, it follows `--trace`.
176
+
177
+ Exit codes: `0` the run completed, `1` it failed (an activity failed, a contract was
178
+ violated, an operation timed out, or a replan became infeasible), `2` a usage or input
179
+ error — including a workflow or an `x-labcode` extension the front doors reject.
180
+
181
+ A complete invocation, against this repository's `examples/` (`--seconds-per-tick` small
182
+ because that example's scripts return instantly):
183
+
184
+ ```sh
185
+ lc run examples/plate_line.workflow.yaml --env examples/plate_line.env.yaml \
186
+ --boundary examples/plate_line.boundary.yaml --seconds-per-tick 0.2
187
+ ```
188
+
189
+ The remaining options tune the replan loop rather than describe the run —
190
+ `--poll-interval`, `--margin`, `--seed`, `--speed`, `--max-ticks`, `--no-validate` — and
191
+ are covered by `lc run --help`.
192
+
193
+ ### Recording a run
194
+
195
+ `--trace` records what the run did as OpenTelemetry traces. It needs the extra, in the
196
+ interpreter that drives the run — which is also the one that runs the scripts, since
197
+ labcode launches each with `sys.executable`:
198
+
199
+ ```sh
200
+ pip install 'labcode[otel]'
201
+ lc run <workflow> --env <env> --trace --mission-id M-2026-001
202
+ ```
203
+
204
+ One run is one trace, and the id it can be found by is printed to stderr as the run
205
+ starts (`lc run: recording this run as trace …`). What it holds:
206
+
207
+ ```text
208
+ run mission.id, and the failure if it stopped on one
209
+ ├─ process Seal which node, process and mode; the plan's interval;
210
+ │ │ which Objects it handled
211
+ │ ├─ sila2.connect the address, measured in the process that connected
212
+ │ │ └─ /…/SiLAService/GetFeatureDefinition one per feature, × however many
213
+ │ └─ sila2 SealerControl.Seal the command, from its start to its real completion
214
+ │ └─ /…/SealerControl/Seal the round trip that started it
215
+ └─ transport the route and the transporter
216
+ ```
217
+
218
+ An operation's span is opened when it is dispatched and closed when a poll finds it
219
+ finished, so its end is late by up to one poll period; what an instrument actually spent is
220
+ in the command spans, which are measured where the commands are issued. `ofp.object.id`
221
+ lists the `_id`s an operation handled, including one it created — which is what makes
222
+ "everything that happened to this plate" a single query.
223
+
224
+ The innermost layer is the gRPC calls themselves, each under the connection or the command
225
+ that issued it — so what a connection spends is broken down into the feature definitions it
226
+ had to fetch, and a command's span separates its round trip from the time the instrument
227
+ then took. It needs `grpcio`, which arrives with the `sila2` extra; without it the record is
228
+ the same minus that layer. Two things it does not do: an observable command's
229
+ execution-info subscription is **not** recorded (it is read on a thread of `sila2`'s own,
230
+ where it would land in a trace of its own, and its duration is the command's anyway), and
231
+ each recorded call **sends the trace context to the instrument** in its gRPC metadata, which
232
+ a SiLA2 server ignores as it does any key that is not SiLA Client Metadata.
233
+
234
+ Where the record goes is configured by the **standard `OTEL_*` environment variables**
235
+ (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME`, …), so
236
+ labcode adds no settings of its own; `service.name` falls back to `labcode` if nothing sets
237
+ it. `LC_TRACE_FILE=path` additionally writes the spans to a file as JSON lines, one file per
238
+ process (the name gets the process id), which is how a record can be read without standing
239
+ up a collector.
240
+
241
+ Two things to know before pointing a run at a collector. **`--trace` makes Object `_id`s
242
+ real rather than reproducible** unless `--object-ids` says otherwise, since a reproducible id
243
+ is the same on every run and would collapse several runs' plates into one. And **a collector
244
+ that is configured but does not answer delays each operation**, by as long as
245
+ OpenTelemetry's own export timeout allows: labcode sets no timeout of its own, so
246
+ `OTEL_EXPORTER_OTLP_TIMEOUT` is the knob, and it belongs to whoever pointed the run there.
247
+
248
+ ## Examples
249
+
250
+ [`examples/`](examples/README.md) holds three worked runs: `plate_line`, an
251
+ Object-bearing line driven entirely by environment scripts and runnable with no
252
+ hardware, and `sila2_seal` and `sila2_plate_cycle`, which drive the reference lab's
253
+ SiLA2 servers for real. `sila2_seal` also walks through what a run does when a
254
+ machine stops answering — before an operation, and in the middle of one.
255
+
256
+ ## License
257
+
258
+ MIT
@@ -0,0 +1,214 @@
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 — measured inside the process that
73
+ issued them — a span per SiLA2 connection, per command, and per gRPC call underneath
74
+ either. Off by default; the extra is `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
161
+ starts (`lc run: recording this run as trace …`). What it holds:
162
+
163
+ ```text
164
+ run mission.id, and the failure if it stopped on one
165
+ ├─ process Seal which node, process and mode; the plan's interval;
166
+ │ │ which Objects it handled
167
+ │ ├─ sila2.connect the address, measured in the process that connected
168
+ │ │ └─ /…/SiLAService/GetFeatureDefinition one per feature, × however many
169
+ │ └─ sila2 SealerControl.Seal the command, from its start to its real completion
170
+ │ └─ /…/SealerControl/Seal the round trip that started it
171
+ └─ transport the route and the transporter
172
+ ```
173
+
174
+ An operation's span is opened when it is dispatched and closed when a poll finds it
175
+ finished, so its end is late by up to one poll period; what an instrument actually spent is
176
+ in the command spans, which are measured where the commands are issued. `ofp.object.id`
177
+ lists the `_id`s an operation handled, including one it created — which is what makes
178
+ "everything that happened to this plate" a single query.
179
+
180
+ The innermost layer is the gRPC calls themselves, each under the connection or the command
181
+ that issued it — so what a connection spends is broken down into the feature definitions it
182
+ had to fetch, and a command's span separates its round trip from the time the instrument
183
+ then took. It needs `grpcio`, which arrives with the `sila2` extra; without it the record is
184
+ the same minus that layer. Two things it does not do: an observable command's
185
+ execution-info subscription is **not** recorded (it is read on a thread of `sila2`'s own,
186
+ where it would land in a trace of its own, and its duration is the command's anyway), and
187
+ each recorded call **sends the trace context to the instrument** in its gRPC metadata, which
188
+ a SiLA2 server ignores as it does any key that is not SiLA Client Metadata.
189
+
190
+ Where the record goes is configured by the **standard `OTEL_*` environment variables**
191
+ (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME`, …), so
192
+ labcode adds no settings of its own; `service.name` falls back to `labcode` if nothing sets
193
+ it. `LC_TRACE_FILE=path` additionally writes the spans to a file as JSON lines, one file per
194
+ process (the name gets the process id), which is how a record can be read without standing
195
+ up a collector.
196
+
197
+ Two things to know before pointing a run at a collector. **`--trace` makes Object `_id`s
198
+ real rather than reproducible** unless `--object-ids` says otherwise, since a reproducible id
199
+ is the same on every run and would collapse several runs' plates into one. And **a collector
200
+ that is configured but does not answer delays each operation**, by as long as
201
+ OpenTelemetry's own export timeout allows: labcode sets no timeout of its own, so
202
+ `OTEL_EXPORTER_OTLP_TIMEOUT` is the knob, and it belongs to whoever pointed the run there.
203
+
204
+ ## Examples
205
+
206
+ [`examples/`](examples/README.md) holds three worked runs: `plate_line`, an
207
+ Object-bearing line driven entirely by environment scripts and runnable with no
208
+ hardware, and `sila2_seal` and `sila2_plate_cycle`, which drive the reference lab's
209
+ SiLA2 servers for real. `sila2_seal` also walks through what a run does when a
210
+ machine stops answering — before an operation, and in the middle of one.
211
+
212
+ ## License
213
+
214
+ 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