labcode 0.3.0__tar.gz → 0.4.0__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.
- {labcode-0.3.0 → labcode-0.4.0}/PKG-INFO +14 -4
- {labcode-0.3.0 → labcode-0.4.0}/README.md +11 -1
- {labcode-0.3.0 → labcode-0.4.0}/SPECIFICATIONS.md +36 -6
- {labcode-0.3.0 → labcode-0.4.0}/examples/README.md +9 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/backend.py +2 -2
- {labcode-0.3.0 → labcode-0.4.0}/labcode/dialect.py +66 -20
- {labcode-0.3.0 → labcode-0.4.0}/labcode/extension.py +32 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/run_cli.py +12 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode.egg-info/PKG-INFO +14 -4
- {labcode-0.3.0 → labcode-0.4.0}/labcode.egg-info/requires.txt +2 -2
- labcode-0.4.0/labcode.egg-info/scm_version.json +8 -0
- {labcode-0.3.0 → labcode-0.4.0}/pyproject.toml +14 -5
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_backend.py +34 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_dialect.py +82 -0
- labcode-0.3.0/labcode.egg-info/scm_version.json +0 -8
- {labcode-0.3.0 → labcode-0.4.0}/.github/workflows/ci.yml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/.github/workflows/publish.yml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/.gitignore +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/LICENSE +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/MANIFEST.in +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/plate_line.boundary.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/plate_line.observation.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/plate_line.plan.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/plate_line.svg +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/outputs/sila2_plate_cycle.svg +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/plate_line.boundary.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/plate_line.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/plate_line.workflow.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/preflight_sila2_env.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/render_plate_line.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/render_sila2_plate_cycle.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/run_all_sila2_examples.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/run_sila2_plate_cycle.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/run_sila2_plate_cycle_no_atc.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/run_sila2_seal.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_plate_cycle.boundary.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_plate_cycle.workflow.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_plate_cycle.wrapped.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_plate_cycle_no_atc.boundary.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_plate_cycle_no_atc.workflow.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_seal.boundary.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_seal.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_seal.workflow.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/examples/sila2_seal.wrapped.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/__init__.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/__main__.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/_child.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/cli.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/idgen.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/objectid.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/otel.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/otel_sila2.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/probe.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/py.typed +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/record.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/runner.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/sila2.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/sila2_commands.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode/sila2_instrument.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode.egg-info/SOURCES.txt +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode.egg-info/dependency_links.txt +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode.egg-info/entry_points.txt +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode.egg-info/scm_file_list.json +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/labcode.egg-info/top_level.txt +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/setup.cfg +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/fixtures/device_script.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/fixtures/device_script.workflow.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/fixtures/replenishment.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/fixtures/reroute_device.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/fixtures/reroute_transporter.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/fixtures/transport.env.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/fixtures/transport.workflow.yaml +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_cli.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_objectid.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_otel.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_otel_child.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_otel_grpc.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_otel_sila2.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_probe.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_record.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_recording.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_run_cli.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_sila2.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_sila2_commands.py +0 -0
- {labcode-0.3.0 → labcode-0.4.0}/tests/test_sila2_instrument.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: labcode -- a dialect wrapper over the Object-Flow Programming Language toolchain
|
|
5
5
|
Author-email: Kazunari Kaizu <kwaizu@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,8 +21,8 @@ Requires-Python: >=3.10
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: ofplang-validate<0.3,>=0.2
|
|
24
|
-
Requires-Dist: ofplang-schedule<0.
|
|
25
|
-
Requires-Dist: ofplang-run<0.
|
|
24
|
+
Requires-Dist: ofplang-schedule<0.9,>=0.8
|
|
25
|
+
Requires-Dist: ofplang-run<0.8,>=0.7
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
28
28
|
Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
|
|
@@ -143,7 +143,8 @@ described below.
|
|
|
143
143
|
lc run <workflow> --env <env>
|
|
144
144
|
[--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
|
|
145
145
|
[--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
|
|
146
|
-
[--ignore-resources] [--trace] [--mission-id ID]
|
|
146
|
+
[--max-transport-legs N] [--ignore-resources] [--trace] [--mission-id ID]
|
|
147
|
+
[--object-ids seeded|real]
|
|
147
148
|
```
|
|
148
149
|
|
|
149
150
|
- `<workflow>` — the portable v0 workflow: *what* happens.
|
|
@@ -176,6 +177,15 @@ lc run <workflow> --env <env>
|
|
|
176
177
|
else 7200 real seconds. The two forms exclude each other.
|
|
177
178
|
- `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
|
|
178
179
|
machine as reachable (§1.6). The documents are still validated.
|
|
180
|
+
- `--max-transport-legs N` — how many transport activities one Object-bearing arc may
|
|
181
|
+
be carried in (schedule spec §6.4.1), joined by **relay** activities. It is 1 by
|
|
182
|
+
default: the single hop. Raise it where the transporter cannot reach an instrument
|
|
183
|
+
from where the plate is — a bench it serves from one position only, or a hand-off
|
|
184
|
+
station a plate has to cross — and the move is planned as the shortest chain between
|
|
185
|
+
the two spots instead of being reported unreachable. Only the fewest possible moves
|
|
186
|
+
are offered, so a plate one move away is never sent round by way of somewhere else.
|
|
187
|
+
This describes the laboratory's reach rather than tuning the loop, which is why it is
|
|
188
|
+
here and not among the replan knobs below.
|
|
179
189
|
- `--ignore-resources` — switch the consumable model off. The environment's resource
|
|
180
190
|
declarations are still checked for shape but none is applied, so a bench whose devices
|
|
181
191
|
declare stocks nobody is tracking runs without the boundary saying what they held.
|
|
@@ -99,7 +99,8 @@ described below.
|
|
|
99
99
|
lc run <workflow> --env <env>
|
|
100
100
|
[--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
|
|
101
101
|
[--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
|
|
102
|
-
[--ignore-resources] [--trace] [--mission-id ID]
|
|
102
|
+
[--max-transport-legs N] [--ignore-resources] [--trace] [--mission-id ID]
|
|
103
|
+
[--object-ids seeded|real]
|
|
103
104
|
```
|
|
104
105
|
|
|
105
106
|
- `<workflow>` — the portable v0 workflow: *what* happens.
|
|
@@ -132,6 +133,15 @@ lc run <workflow> --env <env>
|
|
|
132
133
|
else 7200 real seconds. The two forms exclude each other.
|
|
133
134
|
- `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
|
|
134
135
|
machine as reachable (§1.6). The documents are still validated.
|
|
136
|
+
- `--max-transport-legs N` — how many transport activities one Object-bearing arc may
|
|
137
|
+
be carried in (schedule spec §6.4.1), joined by **relay** activities. It is 1 by
|
|
138
|
+
default: the single hop. Raise it where the transporter cannot reach an instrument
|
|
139
|
+
from where the plate is — a bench it serves from one position only, or a hand-off
|
|
140
|
+
station a plate has to cross — and the move is planned as the shortest chain between
|
|
141
|
+
the two spots instead of being reported unreachable. Only the fewest possible moves
|
|
142
|
+
are offered, so a plate one move away is never sent round by way of somewhere else.
|
|
143
|
+
This describes the laboratory's reach rather than tuning the loop, which is why it is
|
|
144
|
+
here and not among the replan knobs below.
|
|
135
145
|
- `--ignore-resources` — switch the consumable model off. The environment's resource
|
|
136
146
|
declarations are still checked for shape but none is applied, so a bench whose devices
|
|
137
147
|
declare stocks nobody is tracking runs without the boundary saying what they held.
|
|
@@ -55,7 +55,9 @@ still validates and schedules as plain v0. Only labcode interprets it.
|
|
|
55
55
|
- `endpoints` (**transport routes only**, optional, default `false`): MUST be a boolean —
|
|
56
56
|
whether this move is also given clients for the devices at either **end** of its route,
|
|
57
57
|
not only its `transporter` (§1.7). A process mode may not declare it: a mode's machines
|
|
58
|
-
are the ones it lists.
|
|
58
|
+
are the ones it lists. A `sila2` script on a route with **no transporter** (§1.3) MUST
|
|
59
|
+
declare it `true`: the ends are then the only machines there are, and a script is never
|
|
60
|
+
given a machine it did not ask for.
|
|
59
61
|
|
|
60
62
|
**Unknown keys are an error** — in `x-labcode` at every position, and in the mappings it
|
|
61
63
|
holds. A key this version does not know is either a typo or a feature it does not have;
|
|
@@ -124,6 +126,24 @@ output is verified. Success is "it ran without raising"; an exception is a grace
|
|
|
124
126
|
A route with no `x-labcode.script` runs as a plain timed move — the runner's material
|
|
125
127
|
bookkeeping only, with no device command (a warned no-op for a real move, from != to).
|
|
126
128
|
|
|
129
|
+
**A route with no transporter.** An environment may declare a route that needs no
|
|
130
|
+
transporter at all, by writing `transporter: null` (ofplang-schedule §4.6 / §5.4) — a
|
|
131
|
+
device shifting material between its own spots, a chute. The plan then reports the move
|
|
132
|
+
with `transporter: null` too, and this dialect matches it like any other: routes are keyed
|
|
133
|
+
by `(transporter, from, to)`, so a null one matches a null one, and the script's
|
|
134
|
+
`transporter` local is `None`.
|
|
135
|
+
|
|
136
|
+
Such a move is performed by the **devices at either end of the route** — for a move within
|
|
137
|
+
one device, that device. So a `flavor: sila2` script on such a route must declare
|
|
138
|
+
**`endpoints: true`** (§1.6), and at least one of those devices must declare a
|
|
139
|
+
`connection`; either missing is a front-door error. The `endpoints` request is *required*
|
|
140
|
+
rather than inferred: it is the author's statement of which machines the script drives, and
|
|
141
|
+
reading it for them would decide that on exactly the routes where it matters most. A `raw`
|
|
142
|
+
script, or no script at all, is unaffected — neither is handed clients.
|
|
143
|
+
|
|
144
|
+
`sila2_client` is then the first machine connected to, which is the **source device**: the
|
|
145
|
+
one performing the move, exactly as it is the transporter on a carried route (§1.7).
|
|
146
|
+
|
|
127
147
|
### 1.4 `x-labcode` on a replenishment route
|
|
128
148
|
|
|
129
149
|
An environment `replenishments[]` route may carry an `x-labcode` with a `script`: the
|
|
@@ -211,14 +231,18 @@ a script uses it.
|
|
|
211
231
|
- a mode script with `flavor: sila2` requires **at least one** of that mode's `devices[]`
|
|
212
232
|
to declare a `connection`;
|
|
213
233
|
- a transport script with `flavor: sila2` requires that route's `transporter` to declare
|
|
214
|
-
one.
|
|
234
|
+
one — or, on a route with **no transporter** (§1.3), requires `endpoints: true` and at
|
|
235
|
+
least one of the devices at its ends to declare one, those being the machines that
|
|
236
|
+
perform such a move.
|
|
215
237
|
|
|
216
238
|
A transport that declares `endpoints: true` is also handed the clients of the devices at
|
|
217
239
|
either **end** of its route (§1.7), but those are *not* required to declare a `connection`:
|
|
218
240
|
a route through a plain holding location is ordinary, and the end without an address is
|
|
219
241
|
simply not connected to (a **warning** when *neither* end has one, since then the request
|
|
220
|
-
does nothing
|
|
221
|
-
|
|
242
|
+
does nothing — an **error** on a route with no transporter, which has nothing else to
|
|
243
|
+
drive). The transporter is the one that must be reachable, because it is the machine that
|
|
244
|
+
does the moving — and the one `sila2_client` names; where there is none, that is the source
|
|
245
|
+
device, for the same reason. Asking a `raw` script for endpoint
|
|
222
246
|
clients is an **error**: a raw script is handed no clients at all, so the request cannot be
|
|
223
247
|
honoured.
|
|
224
248
|
|
|
@@ -320,8 +344,8 @@ of the input ports of §1.2 (or the transport locals of §1.3), the code sees:
|
|
|
320
344
|
|
|
321
345
|
| name | meaning |
|
|
322
346
|
|---|---|
|
|
323
|
-
| `sila2_clients` | the clients by **machine id**, in the order the operation names its machines: a mode's `devices[]` order, or — for a transport — its `transporter
|
|
324
|
-
| `sila2_client` | the first of them — for a transport
|
|
347
|
+
| `sila2_clients` | the clients by **machine id**, in the order the operation names its machines: a mode's `devices[]` order, or — for a transport — its `transporter` (absent on a route that has none, §1.3), followed by the devices at either **end of the route** when it declares `endpoints: true`. Named, not held: a mode declaring `device_access: false` (ofplang-schedule §4.4.2) rests on its devices rather than occupying them, and its script is still handed their clients |
|
|
348
|
+
| `sila2_client` | the first of them — for a transport its `transporter`, or the **source device** on a route with none (§1.3); the one name a single-machine operation needs |
|
|
325
349
|
|
|
326
350
|
```yaml
|
|
327
351
|
x-labcode:
|
|
@@ -350,6 +374,12 @@ x-labcode:
|
|
|
350
374
|
property of the move, not of the lab. A route that does not ask still *holds* both ends, so
|
|
351
375
|
reaching for one is answered with what to add rather than with silence.
|
|
352
376
|
|
|
377
|
+
On a route with **no transporter** (§1.3) a `sila2` script must ask: the ends are the only
|
|
378
|
+
machines there are, so not asking leaves nothing to open, and the front door says so rather
|
|
379
|
+
than letting the move fail when it runs. It is still asked for, not assumed — which
|
|
380
|
+
machines a script drives is the author's to state, and nowhere more so than where the
|
|
381
|
+
machine doing the moving is also the one holding the material.
|
|
382
|
+
|
|
353
383
|
```yaml
|
|
354
384
|
transports:
|
|
355
385
|
- transporter: arm
|
|
@@ -57,6 +57,15 @@ and writes its artifacts under [`outputs/`](outputs/):
|
|
|
57
57
|
python examples/render_plate_line.py
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
🔴 **These are a record of one run, not a reproducible golden.** The labcode backend
|
|
61
|
+
runs its operations out of process against a **wall clock** (`seconds_per_tick`), so
|
|
62
|
+
which tick a subprocess finishes on varies between runs: re-rendering the same commit
|
|
63
|
+
gives a final `now` of 10 one time and 13 the next. Do not treat a difference here as a
|
|
64
|
+
regression, and do not gate a change on these files being byte-identical — the way the
|
|
65
|
+
scheduler's and the runner's own artifacts can be, both of those being driven by a
|
|
66
|
+
virtual clock. What *is* stable is the plan the scheduler produces from a given input,
|
|
67
|
+
which the tests cover.
|
|
68
|
+
|
|
60
69
|
- [`outputs/plate_line.plan.yaml`](outputs/plate_line.plan.yaml) — the **final execution
|
|
61
70
|
schedule** (the §6/§7 status document: every activity, `completed`).
|
|
62
71
|
- [`outputs/plate_line.observation.yaml`](outputs/plate_line.observation.yaml) — the
|
|
@@ -57,6 +57,7 @@ from labcode.extension import (
|
|
|
57
57
|
script_endpoints,
|
|
58
58
|
script_flavor,
|
|
59
59
|
spot_device,
|
|
60
|
+
transport_label,
|
|
60
61
|
transporter_connections,
|
|
61
62
|
)
|
|
62
63
|
from labcode.idgen import DEFAULT_ID_GENERATOR, IdGenerator
|
|
@@ -245,8 +246,7 @@ def make_transport_resolver(environment: dict) -> Callable:
|
|
|
245
246
|
code = _flavored(
|
|
246
247
|
code, script,
|
|
247
248
|
_transport_machines(transport, transporters, devices, script),
|
|
248
|
-
|
|
249
|
-
f"{transport.get('from')} -> {transport.get('to')}",
|
|
249
|
+
transport_label(transport),
|
|
250
250
|
)
|
|
251
251
|
routes[key] = code
|
|
252
252
|
|
|
@@ -59,8 +59,11 @@ from labcode.extension import (
|
|
|
59
59
|
parse_connection,
|
|
60
60
|
parse_op_timeout,
|
|
61
61
|
parse_probe,
|
|
62
|
+
route_transporter,
|
|
63
|
+
script_endpoints,
|
|
62
64
|
script_flavor,
|
|
63
65
|
spot_device,
|
|
66
|
+
transport_label,
|
|
64
67
|
unknown_key_messages,
|
|
65
68
|
)
|
|
66
69
|
from labcode.objectid import RESERVED_ID, reserved_collisions
|
|
@@ -464,11 +467,8 @@ def _validate_replenishments(environment: dict, errors: list, warnings: list) ->
|
|
|
464
467
|
)
|
|
465
468
|
|
|
466
469
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
f"transport {transport.get('transporter')!r} "
|
|
470
|
-
f"{transport.get('from')} -> {transport.get('to')}"
|
|
471
|
-
)
|
|
470
|
+
# `transport_label` (extension.py) is shared with the backend, so the same route is named
|
|
471
|
+
# the same way in a front-door diagnostic and in a failing operation.
|
|
472
472
|
|
|
473
473
|
|
|
474
474
|
def _validate_transports(
|
|
@@ -485,7 +485,7 @@ def _validate_transports(
|
|
|
485
485
|
for transport in environment.get("transports") or []:
|
|
486
486
|
if not isinstance(transport, dict):
|
|
487
487
|
continue
|
|
488
|
-
label =
|
|
488
|
+
label = transport_label(transport)
|
|
489
489
|
extension = transport.get(EXTENSION_KEY)
|
|
490
490
|
if extension is None:
|
|
491
491
|
if transport.get("from") != transport.get("to"): # a real move
|
|
@@ -505,7 +505,9 @@ def _validate_transports(
|
|
|
505
505
|
continue
|
|
506
506
|
_check_transport_endpoints(transport, script, label, devices, errors, warnings)
|
|
507
507
|
if script_flavor(script) == FLAVOR_SILA2:
|
|
508
|
-
_check_transport_connection(
|
|
508
|
+
_check_transport_connection(
|
|
509
|
+
transport, script, label, transporters, devices, errors
|
|
510
|
+
)
|
|
509
511
|
|
|
510
512
|
|
|
511
513
|
def _check_transport_endpoints(
|
|
@@ -519,7 +521,12 @@ def _check_transport_endpoints(
|
|
|
519
521
|
clients at all (§1.6), so the request cannot be honoured and the author expects
|
|
520
522
|
something that will not happen. Asking when neither end has an address is a **warning**
|
|
521
523
|
-- the route still works through its transporter, and an environment written before its
|
|
522
|
-
instruments have addresses is a legitimate intermediate state (as with `probe`).
|
|
524
|
+
instruments have addresses is a legitimate intermediate state (as with `probe`).
|
|
525
|
+
|
|
526
|
+
That last one holds only for a route that *has* a transporter to fall back on. A route
|
|
527
|
+
with no transporter (§4.6/§5.4) has nothing else to drive, so the same situation is an
|
|
528
|
+
error there -- reported by `_check_transport_connection`, which is where the rest of
|
|
529
|
+
that route's connection requirement lives, so that one problem yields one message."""
|
|
523
530
|
endpoints = script.get("endpoints")
|
|
524
531
|
prefix = f"{label}: x-labcode.script.endpoints"
|
|
525
532
|
if endpoints is None:
|
|
@@ -535,8 +542,9 @@ def _check_transport_endpoints(
|
|
|
535
542
|
f"{script_flavor(script)!r}: only a 'sila2' script is handed clients (§1.6)"
|
|
536
543
|
)
|
|
537
544
|
return
|
|
538
|
-
|
|
539
|
-
|
|
545
|
+
if route_transporter(transport) is None:
|
|
546
|
+
return # nothing to fall back on; _check_transport_connection reports it as an error
|
|
547
|
+
named = _route_ends(transport)
|
|
540
548
|
if not any(device in devices.with_connection for device in named):
|
|
541
549
|
warnings.append(
|
|
542
550
|
f"{prefix} is true, but neither end of the route ({', '.join(named)}) declares "
|
|
@@ -544,16 +552,54 @@ def _check_transport_endpoints(
|
|
|
544
552
|
)
|
|
545
553
|
|
|
546
554
|
|
|
555
|
+
def _route_ends(transport: dict) -> list[str]:
|
|
556
|
+
"""The devices at either end of a route, named and deduplicated in route order.
|
|
557
|
+
|
|
558
|
+
A move within one device names it once: it is one machine, and one client."""
|
|
559
|
+
ends = [spot_device(transport.get(end)) for end in ("from", "to")]
|
|
560
|
+
return list(dict.fromkeys(device for device in ends if device is not None))
|
|
561
|
+
|
|
562
|
+
|
|
547
563
|
def _check_transport_connection(
|
|
548
|
-
transport: dict,
|
|
564
|
+
transport: dict,
|
|
565
|
+
script: dict,
|
|
566
|
+
label: str,
|
|
567
|
+
transporters: _NodeIndex,
|
|
568
|
+
devices: _NodeIndex,
|
|
569
|
+
errors: list,
|
|
549
570
|
) -> None:
|
|
550
|
-
"""
|
|
551
|
-
|
|
552
|
-
|
|
571
|
+
"""Where a `sila2` transport script's connection has to be.
|
|
572
|
+
|
|
573
|
+
Normally the transporter: it is the machine that carries the move, and the one
|
|
574
|
+
`sila2_client` names.
|
|
575
|
+
|
|
576
|
+
A route with **no transporter** (`transporter: null`, ofplang-schedule §4.6/§5.4) is
|
|
577
|
+
performed by the devices at its ends -- a cycler loading its own block -- so those are
|
|
578
|
+
what the script must be able to reach. It receives them only if it asks (`endpoints`,
|
|
579
|
+
§1.6), and it is made to ask rather than being read as asking: `endpoints` is the
|
|
580
|
+
author's statement of which machines the script drives, and inferring it here would take
|
|
581
|
+
that statement away from them for exactly the routes where it matters most. The cost of
|
|
582
|
+
requiring it is one line in the environment; the cost of assuming it is a script handed
|
|
583
|
+
clients its author never asked for."""
|
|
553
584
|
prefix = f"{label}: x-labcode.script.flavor 'sila2' but"
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
585
|
+
identifier = route_transporter(transport)
|
|
586
|
+
if identifier is not None:
|
|
587
|
+
if identifier not in transporters.declared:
|
|
588
|
+
errors.append(f"{prefix} transporter {identifier!r} is not declared in transporters[]")
|
|
589
|
+
elif identifier not in transporters.with_connection:
|
|
590
|
+
errors.append(f"{prefix} transporter {identifier!r} declares no x-labcode.connection")
|
|
591
|
+
return
|
|
592
|
+
if not script_endpoints(script):
|
|
593
|
+
errors.append(
|
|
594
|
+
f"{prefix} the route names no transporter and the script does not ask for its "
|
|
595
|
+
f"endpoint clients; add `endpoints: true` so it is handed the devices at either "
|
|
596
|
+
f"end of the route, which are what performs a move nothing carries"
|
|
597
|
+
)
|
|
598
|
+
return
|
|
599
|
+
named = _route_ends(transport)
|
|
600
|
+
if not any(device in devices.with_connection for device in named):
|
|
601
|
+
errors.append(
|
|
602
|
+
f"{prefix} the route names no transporter and neither end "
|
|
603
|
+
f"({', '.join(named)}) declares an x-labcode.connection; there is no machine "
|
|
604
|
+
f"left for the script to drive"
|
|
605
|
+
)
|
|
@@ -315,6 +315,38 @@ def declared_probe(extension: Any) -> dict[str, Any]:
|
|
|
315
315
|
return declared
|
|
316
316
|
|
|
317
317
|
|
|
318
|
+
def route_transporter(transport: Any) -> str | None:
|
|
319
|
+
"""The transporter carrying a transport route, or None when the route names none.
|
|
320
|
+
|
|
321
|
+
A route may declare ``transporter: null`` (ofplang-schedule SPECIFICATIONS §4.6/§5.4):
|
|
322
|
+
the move needs no transporter at all -- a device shifting material between its own
|
|
323
|
+
spots, a chute -- and is performed by the devices at its ends instead.
|
|
324
|
+
|
|
325
|
+
Anything that is not a non-empty string reads as "none" here. A malformed value is the
|
|
326
|
+
schedule environment validator's to reject, and this must not hold a second opinion
|
|
327
|
+
about it; what matters at this layer is only whether there is a transporter to drive.
|
|
328
|
+
"""
|
|
329
|
+
if not isinstance(transport, dict):
|
|
330
|
+
return None
|
|
331
|
+
identifier = transport.get("transporter")
|
|
332
|
+
return identifier if isinstance(identifier, str) and identifier else None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def transport_label(transport: Any) -> str:
|
|
336
|
+
"""How a transport route is named in a message: its carrier, then its ends.
|
|
337
|
+
|
|
338
|
+
A route with no transporter says so in words. Rendering the absent id would print
|
|
339
|
+
``transport None a -> b``, which reads as a defect in the message rather than as a fact
|
|
340
|
+
about the route -- and this label reaches the user in both a front-door diagnostic and a
|
|
341
|
+
failing operation, so it is written once and shared.
|
|
342
|
+
"""
|
|
343
|
+
if not isinstance(transport, dict):
|
|
344
|
+
transport = {}
|
|
345
|
+
identifier = route_transporter(transport)
|
|
346
|
+
who = repr(identifier) if identifier is not None else "(no transporter)"
|
|
347
|
+
return f"transport {who} {transport.get('from')} -> {transport.get('to')}"
|
|
348
|
+
|
|
349
|
+
|
|
318
350
|
def spot_device(spot: Any) -> str | None:
|
|
319
351
|
"""The device a qualified spot (``<device>.<spot>``, schedule SPECIFICATIONS §8.2)
|
|
320
352
|
belongs to, or None when there is no name to take.
|
|
@@ -65,6 +65,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
65
65
|
"--margin", type=int, default=None, metavar="M",
|
|
66
66
|
help="running-task margin for replans (default: the poll interval)",
|
|
67
67
|
)
|
|
68
|
+
p.add_argument(
|
|
69
|
+
"--max-transport-legs", type=int, default=1, metavar="N",
|
|
70
|
+
help="how many transport activities one Object-bearing arc may be carried in "
|
|
71
|
+
"(default 1: the single hop), joined by relays -- raise it for an "
|
|
72
|
+
"instrument the transporter reaches at one position only, or a plate "
|
|
73
|
+
"that has to cross a hand-off station",
|
|
74
|
+
)
|
|
68
75
|
p.add_argument(
|
|
69
76
|
"--poll-interval", type=int, default=1, metavar="D",
|
|
70
77
|
help="poll every D time units (default 1)",
|
|
@@ -329,6 +336,11 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
329
336
|
running_task_margin=args.margin,
|
|
330
337
|
random_seed=args.seed,
|
|
331
338
|
poll_interval=args.poll_interval,
|
|
339
|
+
# How many moves one arc may be carried in (schedule SPEC §6.4.1). Passed
|
|
340
|
+
# through rather than decided here: it is the scheduler's offer, and a real
|
|
341
|
+
# laboratory is exactly where an instrument reachable from one position
|
|
342
|
+
# only makes the single hop insufficient.
|
|
343
|
+
max_transport_legs=args.max_transport_legs,
|
|
332
344
|
# 0 is "no limit" here as it is upstream; the runner spells that None.
|
|
333
345
|
max_ticks=args.max_ticks or None,
|
|
334
346
|
seconds_per_tick=args.seconds_per_tick,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: labcode -- a dialect wrapper over the Object-Flow Programming Language toolchain
|
|
5
5
|
Author-email: Kazunari Kaizu <kwaizu@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,8 +21,8 @@ Requires-Python: >=3.10
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: ofplang-validate<0.3,>=0.2
|
|
24
|
-
Requires-Dist: ofplang-schedule<0.
|
|
25
|
-
Requires-Dist: ofplang-run<0.
|
|
24
|
+
Requires-Dist: ofplang-schedule<0.9,>=0.8
|
|
25
|
+
Requires-Dist: ofplang-run<0.8,>=0.7
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
28
28
|
Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
|
|
@@ -143,7 +143,8 @@ described below.
|
|
|
143
143
|
lc run <workflow> --env <env>
|
|
144
144
|
[--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
|
|
145
145
|
[--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
|
|
146
|
-
[--ignore-resources] [--trace] [--mission-id ID]
|
|
146
|
+
[--max-transport-legs N] [--ignore-resources] [--trace] [--mission-id ID]
|
|
147
|
+
[--object-ids seeded|real]
|
|
147
148
|
```
|
|
148
149
|
|
|
149
150
|
- `<workflow>` — the portable v0 workflow: *what* happens.
|
|
@@ -176,6 +177,15 @@ lc run <workflow> --env <env>
|
|
|
176
177
|
else 7200 real seconds. The two forms exclude each other.
|
|
177
178
|
- `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
|
|
178
179
|
machine as reachable (§1.6). The documents are still validated.
|
|
180
|
+
- `--max-transport-legs N` — how many transport activities one Object-bearing arc may
|
|
181
|
+
be carried in (schedule spec §6.4.1), joined by **relay** activities. It is 1 by
|
|
182
|
+
default: the single hop. Raise it where the transporter cannot reach an instrument
|
|
183
|
+
from where the plate is — a bench it serves from one position only, or a hand-off
|
|
184
|
+
station a plate has to cross — and the move is planned as the shortest chain between
|
|
185
|
+
the two spots instead of being reported unreachable. Only the fewest possible moves
|
|
186
|
+
are offered, so a plate one move away is never sent round by way of somewhere else.
|
|
187
|
+
This describes the laboratory's reach rather than tuning the loop, which is why it is
|
|
188
|
+
here and not among the replan knobs below.
|
|
179
189
|
- `--ignore-resources` — switch the consumable model off. The environment's resource
|
|
180
190
|
declarations are still checked for shape but none is applied, so a bench whose devices
|
|
181
191
|
declare stocks nobody is tracking runs without the boundary saying what they held.
|
|
@@ -41,9 +41,18 @@ classifiers = [
|
|
|
41
41
|
# `dispatch_replenishment` seam and the `kind: replenishment` the child harness has to
|
|
42
42
|
# recognise as side-effect only.
|
|
43
43
|
#
|
|
44
|
-
# `ofplang-schedule>=0.
|
|
45
|
-
#
|
|
46
|
-
#
|
|
44
|
+
# `ofplang-schedule>=0.8` is required, not merely tested against: `lc run` passes
|
|
45
|
+
# `--max-transport-legs` through to it (schedule SPEC §6.4.1), and a job may leave a
|
|
46
|
+
# running plan (§6.11), which needs the `withdraw` argument and the levels carried
|
|
47
|
+
# forward with it. The `>=0.5` floor it replaces was required because an environment may
|
|
48
|
+
# declare a route that needs no transporter (`transporter: null`, schedule §4.6/§5.4) and
|
|
49
|
+
# this dialect accepts a `sila2` script on one, but an older scheduler refuses the
|
|
50
|
+
# environment with `wrong_type` before there is a plan to run -- and `ofplang-run>=0.6` is
|
|
51
|
+
# where such a move is dispatched rather than rejected.
|
|
52
|
+
#
|
|
53
|
+
# The `>=0.2.3` floor it replaces said what this package was tested against rather than what
|
|
54
|
+
# its own code called -- nothing then called a scheduler API newer than the in-memory
|
|
55
|
+
# workflow acceptance of 0.1.3. CI resolves the siblings from PyPI, so every run of this suite and
|
|
47
56
|
# every real-lab example already happens on it, and run's own floor requires it anyway
|
|
48
57
|
# (an older scheduler reports a running refill free before it is).
|
|
49
58
|
dependencies = [
|
|
@@ -55,8 +64,8 @@ dependencies = [
|
|
|
55
64
|
# bounds cannot protect what is already on PyPI without one, which is why they go
|
|
56
65
|
# in now rather than at the first breakage.
|
|
57
66
|
"ofplang-validate>=0.2,<0.3",
|
|
58
|
-
"ofplang-schedule>=0.
|
|
59
|
-
"ofplang-run>=0.
|
|
67
|
+
"ofplang-schedule>=0.8,<0.9",
|
|
68
|
+
"ofplang-run>=0.7,<0.8",
|
|
60
69
|
]
|
|
61
70
|
|
|
62
71
|
[project.urls]
|
|
@@ -288,6 +288,40 @@ def test_a_route_within_one_device_connects_to_it_once():
|
|
|
288
288
|
assert code.count("('cycler',") == 1
|
|
289
289
|
|
|
290
290
|
|
|
291
|
+
def _internal_transport_env(**script) -> dict:
|
|
292
|
+
"""A route with no transporter: the cycler shifts the plate between its own spots."""
|
|
293
|
+
env = _sila2_transport_env("cycler.door", "cycler.block", **script)
|
|
294
|
+
for entry in env["devices"]:
|
|
295
|
+
if entry["id"] == "cycler":
|
|
296
|
+
entry["spots"] = ["door", "block"]
|
|
297
|
+
env["transports"][0]["transporter"] = None
|
|
298
|
+
return env
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_a_transporter_less_route_drives_the_devices_at_its_ends():
|
|
302
|
+
# Nothing carries this move, so `sila2_client` -- "the first of them" -- is the source
|
|
303
|
+
# device, which is the machine that performs it. Both ends are the same device here, so
|
|
304
|
+
# it is connected to once.
|
|
305
|
+
code = make_transport_resolver(_internal_transport_env(endpoints=True))(
|
|
306
|
+
None, "cycler.door", "cycler.block"
|
|
307
|
+
)
|
|
308
|
+
assert code is not None
|
|
309
|
+
assert "('cycler', '127.0.0.1', 50055, True)" in code
|
|
310
|
+
assert "('arm'," not in code # the arm is not on this route and is left alone
|
|
311
|
+
assert code.count("('cycler',") == 1
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_a_transporter_less_route_is_named_in_words_when_it_fails():
|
|
315
|
+
# The label reaches the user in a failing operation as well as a diagnostic;
|
|
316
|
+
# `transporter!r` would render it `transport None a -> b`.
|
|
317
|
+
env = _internal_transport_env(endpoints=True)
|
|
318
|
+
for entry in env["devices"]: # nothing left with an address
|
|
319
|
+
entry.pop("x-labcode", None)
|
|
320
|
+
code = make_transport_resolver(env)(None, "cycler.door", "cycler.block")
|
|
321
|
+
assert code is not None
|
|
322
|
+
assert "transport (no transporter) cycler.door -> cycler.block" in code
|
|
323
|
+
|
|
324
|
+
|
|
291
325
|
TRANSPORT_ENV = {
|
|
292
326
|
"time": {"unit": "second"},
|
|
293
327
|
"devices": [{"id": "s0", "spots": ["core"]}, {"id": "s1", "spots": ["core"]}],
|
|
@@ -700,3 +700,85 @@ def test_an_x_labcode_somewhere_nothing_reads_still_says_where_it_belongs():
|
|
|
700
700
|
result = validate_dialect({"processes": {}}, env)
|
|
701
701
|
assert not result.ok
|
|
702
702
|
assert any("replenishments[]" in e for e in result.errors)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# -- a route with no transporter (ofplang-schedule §4.6/§5.4) -------------------------
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _internal_env(script: dict | None = None, *, connected: bool = True) -> dict:
|
|
709
|
+
"""A cycler that shifts the plate between its own two spots: `transporter: null`."""
|
|
710
|
+
route = {"transporter": None, "from": "cycler.door", "to": "cycler.block"}
|
|
711
|
+
if script is not None:
|
|
712
|
+
route["x-labcode"] = {"script": script}
|
|
713
|
+
env = _transport_env(route)
|
|
714
|
+
cycler = {"x-labcode": {"connection": CONNECTION}} if connected else {}
|
|
715
|
+
env["devices"] = [{"id": "cycler", "spots": ["door", "block"], **cycler}]
|
|
716
|
+
return env
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def test_a_transporter_less_route_needs_no_script():
|
|
720
|
+
result = validate_dialect({}, _internal_env())
|
|
721
|
+
assert result.ok, result.errors
|
|
722
|
+
assert any("no-op move" in w for w in result.warnings)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def test_a_transporter_less_route_takes_a_raw_script():
|
|
726
|
+
result = validate_dialect(
|
|
727
|
+
{}, _internal_env({"language": "python", "code": "load_block()"})
|
|
728
|
+
)
|
|
729
|
+
assert result.ok, result.errors
|
|
730
|
+
assert not result.warnings
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_a_transporter_less_sila2_script_must_ask_for_its_endpoints():
|
|
734
|
+
"""There is no transporter to connect to, and `endpoints` is what asks for the ends.
|
|
735
|
+
|
|
736
|
+
Read as implicitly asked, the author would be handed clients they never requested for
|
|
737
|
+
exactly the routes where which machine is driven matters most.
|
|
738
|
+
"""
|
|
739
|
+
for endpoints in (None, False):
|
|
740
|
+
script = {"language": "python", "code": "load_block()", "flavor": "sila2"}
|
|
741
|
+
if endpoints is not None:
|
|
742
|
+
script["endpoints"] = endpoints
|
|
743
|
+
result = validate_dialect({}, _internal_env(script))
|
|
744
|
+
assert not result.ok
|
|
745
|
+
assert any("add `endpoints: true`" in e for e in result.errors), result.errors
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def test_a_transporter_less_sila2_script_passes_when_it_asks():
|
|
749
|
+
result = validate_dialect(
|
|
750
|
+
{},
|
|
751
|
+
_internal_env(
|
|
752
|
+
{"language": "python", "code": "load_block()", "flavor": "sila2",
|
|
753
|
+
"endpoints": True}
|
|
754
|
+
),
|
|
755
|
+
)
|
|
756
|
+
assert result.ok, result.errors
|
|
757
|
+
assert not result.warnings
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def test_a_transporter_less_route_with_no_reachable_end_is_an_error():
|
|
761
|
+
"""A carried route only *warns* here -- it still works through its transporter.
|
|
762
|
+
|
|
763
|
+
This one has no transporter to fall back on, so there is no machine left to drive, and
|
|
764
|
+
exactly one message says so (not the warning as well).
|
|
765
|
+
"""
|
|
766
|
+
result = validate_dialect(
|
|
767
|
+
{},
|
|
768
|
+
_internal_env(
|
|
769
|
+
{"language": "python", "code": "load_block()", "flavor": "sila2",
|
|
770
|
+
"endpoints": True},
|
|
771
|
+
connected=False,
|
|
772
|
+
),
|
|
773
|
+
)
|
|
774
|
+
assert not result.ok
|
|
775
|
+
assert len(result.errors) == 1
|
|
776
|
+
assert "no machine left for the script to drive" in result.errors[0]
|
|
777
|
+
assert not result.warnings
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def test_a_transporter_less_route_is_named_in_words():
|
|
781
|
+
"""`transporter!r` would print `transport None a -> b`, which reads as a broken message."""
|
|
782
|
+
result = validate_dialect({}, _internal_env())
|
|
783
|
+
assert any("transport (no transporter) cycler.door -> cycler.block" in w
|
|
784
|
+
for w in result.warnings), result.warnings
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|