wexample-wex-addon-process 2.0.0__tar.gz → 3.0.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.
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/PKG-INFO +126 -43
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/README.md +123 -40
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/pyproject.toml +3 -3
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/commands/run/execute.py +5 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/const/filestate.py +21 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/const/process.py +69 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/exception/run_cancelled_exception.py +9 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/helper/git_commit.py +83 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/process_type/family/file_state_process_type_family.py +6 -3
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/process_type/filestate/file_state_option_process_type.py +202 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/process_type/filestate/file_state_report_process_type.py +2 -4
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/process_type/process_context.py +114 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/process_type/selection.py +148 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/record/record_file.py +7 -16
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/runner/process_runner.py +671 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/selection/__init__.py +0 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/selection/abstract_code_from_directory_selection.py +84 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/selection/abstract_process_selection.py +50 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/selection/selection_registry.py +32 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/service/__init__.py +0 -0
- wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/service/process_run_service.py +145 -0
- wexample_wex_addon_process-3.0.0/tests/.gitkeep +0 -0
- wexample_wex_addon_process-3.0.0/tests/unit/__init__.py +0 -0
- wexample_wex_addon_process-3.0.0/tests/unit/test_artifacts.py +97 -0
- wexample_wex_addon_process-3.0.0/tests/unit/test_long_runs.py +219 -0
- wexample_wex_addon_process-3.0.0/tests/unit/test_run_overrides.py +208 -0
- wexample_wex_addon_process-3.0.0/tests/unit/test_selection.py +80 -0
- wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/const/process.py +0 -34
- wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/process_type/filestate/file_state_option_process_type.py +0 -70
- wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/process_type/process_context.py +0 -43
- wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/process_type/selection.py +0 -86
- wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/runner/process_runner.py +0 -257
- wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/service/process_run_service.py +0 -77
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/commands/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/commands/run/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/commands/worker/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/commands/worker/start.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/const/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/process_type → wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/exception}/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/process_type/family → wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/helper}/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/process_addon_manager.py +0 -0
- {wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/process_type/filestate → wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/process_type}/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/process_type/abstract_process_type.py +0 -0
- {wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/record → wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/process_type/family}/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/process_type/family/abstract_process_type_family.py +0 -0
- {wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/runner → wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/process_type/filestate}/__init__.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/process_type/process_type_registry.py +0 -0
- {wexample_wex_addon_process-2.0.0 → wexample_wex_addon_process-3.0.0}/src/wexample_wex_addon_process/py.typed +0 -0
- {wexample_wex_addon_process-2.0.0/src/wexample_wex_addon_process/service → wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/record}/__init__.py +0 -0
- /wexample_wex_addon_process-2.0.0/tests/.gitkeep → /wexample_wex_addon_process-3.0.0/src/wexample_wex_addon_process/runner/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: wexample-wex-addon-process
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Executes process runs defined on disk—either directly or by consuming them from a queue as a worker—against filestate selections
|
|
5
5
|
Author-Email: weeger <contact@wexample.com>
|
|
6
6
|
License: MIT
|
|
@@ -9,8 +9,8 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: wexample-file>=9.2.0
|
|
12
|
-
Requires-Dist: wexample-queue>=
|
|
13
|
-
Requires-Dist: wexample-wex-addon-app>=
|
|
12
|
+
Requires-Dist: wexample-queue>=2.0.0
|
|
13
|
+
Requires-Dist: wexample-wex-addon-app>=32.0.0
|
|
14
14
|
Provides-Extra: dev
|
|
15
15
|
Requires-Dist: pytest; extra == "dev"
|
|
16
16
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
|
|
19
19
|
# wex-addon-process
|
|
20
20
|
|
|
21
|
-
Version:
|
|
21
|
+
Version: 3.0.0
|
|
22
22
|
|
|
23
23
|
`wexample-wex-addon-process` runs the treatments a board declares. A *process* says what to run and on which selection of files; a *run* is one execution of it, asked for by writing a record and picked up by a worker.
|
|
24
24
|
|
|
@@ -41,7 +41,7 @@ wex process::worker/start
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Blocks, consuming the `process_run` queue, and rings back on `process_run_event`
|
|
44
|
-
every time a run moves — on each state change, and at most
|
|
44
|
+
every time a run moves — on each state change, and at most four times a second while
|
|
45
45
|
it advances. This is the process a worker container runs, and there may be as
|
|
46
46
|
many as the parallelism asked for.
|
|
47
47
|
|
|
@@ -65,17 +65,27 @@ filestate:managed_blocks filestate:class filestate:on_bad_format
|
|
|
65
65
|
Nothing is written per treatment: an option added to filestate is a type
|
|
66
66
|
available here the same day, under the very name filestate gives it.
|
|
67
67
|
|
|
68
|
-
The process's `options`
|
|
68
|
+
The process's `options` object is the configuration handed to each file, and has
|
|
69
69
|
to carry the option the type is named after:
|
|
70
70
|
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
mode:
|
|
74
|
-
dry_run: true
|
|
71
|
+
```json
|
|
72
|
+
// the options of a process of type filestate:mode
|
|
73
|
+
{"mode": "600", "action": "dry_run"}
|
|
75
74
|
```
|
|
76
75
|
|
|
77
|
-
`
|
|
78
|
-
|
|
76
|
+
`action` is read by the family and handed to nobody. It picks which of
|
|
77
|
+
filestate's three verbs is asked, the same three the command line offers on an
|
|
78
|
+
app's declared state:
|
|
79
|
+
|
|
80
|
+
- `check`, the default, says what is wrong with each selected file and changes
|
|
81
|
+
nothing. Its `data` counts the files `checked` and `failing`, and lists under
|
|
82
|
+
`files` only those at fault, each with its `code`, `message`, `parameters` and
|
|
83
|
+
`remedy`, the name of the operation that would fix it or null when nothing can.
|
|
84
|
+
- `dry_run` lists the operations one pass would apply.
|
|
85
|
+
- `fix` applies them, then lists what is still wrong, the way `check` does.
|
|
86
|
+
|
|
87
|
+
A rule that only observes, such as `max_lines`, is a type like the others: it has
|
|
88
|
+
no remedy, so a `fix` changes nothing and reports it again.
|
|
79
89
|
|
|
80
90
|
## Writing a type
|
|
81
91
|
|
|
@@ -87,12 +97,17 @@ class ReviewProcessType(AbstractProcessType):
|
|
|
87
97
|
label = "Review"
|
|
88
98
|
|
|
89
99
|
def run(self, context: ProcessContext) -> dict[str, Any]:
|
|
90
|
-
for path in context.
|
|
91
|
-
|
|
100
|
+
for path in context.each():
|
|
101
|
+
...
|
|
92
102
|
|
|
93
103
|
return {"files": len(context.files)}
|
|
94
104
|
```
|
|
95
105
|
|
|
106
|
+
`context.each()` hands the files over one at a time and keeps the run up to
|
|
107
|
+
date: the file being worked on is the run's `current`, and it is counted once
|
|
108
|
+
the loop comes back for the next one. A file whose treatment raises is never
|
|
109
|
+
counted and stays `current`, so a failed run says where it failed.
|
|
110
|
+
|
|
96
111
|
What `run()` returns goes into the run's `data`. Raising is how a type says it
|
|
97
112
|
could not go on — the failure is written down for it, so nothing in a type has
|
|
98
113
|
to know what a failed run looks like.
|
|
@@ -108,6 +123,9 @@ exist.
|
|
|
108
123
|
- [The types on offer](#the-types-on-offer)
|
|
109
124
|
- [Writing a type](#writing-a-type)
|
|
110
125
|
- [Installation](#installation)
|
|
126
|
+
- [What a run may ask for itself](#what-a-run-may-ask-for-itself)
|
|
127
|
+
- [Runs that last a night](#runs-that-last-a-night)
|
|
128
|
+
- [An item that fails, a run that is cancelled](#an-item-that-fails-a-run-that-is-cancelled)
|
|
111
129
|
- [Tests](#tests)
|
|
112
130
|
- [Architecture](#architecture)
|
|
113
131
|
- [Integration in the Suite](#integration-in-the-suite)
|
|
@@ -128,28 +146,35 @@ pip install wexample-wex-addon-process
|
|
|
128
146
|
|
|
129
147
|
Requires Python >=3.10.
|
|
130
148
|
|
|
131
|
-
A process and a selection are records, so a treatment can be declared without a board
|
|
149
|
+
A process and a selection are records, so a treatment can be declared without a board.
|
|
150
|
+
Records are JSON, written and read by programs on both sides:
|
|
132
151
|
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
```json
|
|
153
|
+
// .wex/data/selection/<uuid>.json
|
|
154
|
+
{
|
|
155
|
+
"title": "Markdowns",
|
|
156
|
+
"patterns": "*.md\n!.wex/**"
|
|
157
|
+
}
|
|
137
158
|
```
|
|
138
159
|
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
```json
|
|
161
|
+
// .wex/data/process/<uuid>.json
|
|
162
|
+
{
|
|
163
|
+
"title": "What the Markdowns take",
|
|
164
|
+
"type": "filestate:report",
|
|
165
|
+
"selection_id": "<the selection's uuid>",
|
|
166
|
+
"options": {}
|
|
167
|
+
}
|
|
145
168
|
```
|
|
146
169
|
|
|
147
170
|
Asking for a run is writing a third record:
|
|
148
171
|
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
172
|
+
```json
|
|
173
|
+
// .wex/data/process_run/<uuid>.json
|
|
174
|
+
{
|
|
175
|
+
"process_id": "<the process's uuid>",
|
|
176
|
+
"state": "pending"
|
|
177
|
+
}
|
|
153
178
|
```
|
|
154
179
|
|
|
155
180
|
Then:
|
|
@@ -160,18 +185,76 @@ wex process::run/execute --run <the run's uuid>
|
|
|
160
185
|
|
|
161
186
|
The same file is read back afterwards, holding what happened:
|
|
162
187
|
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.md: 3
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"process_id": "...",
|
|
191
|
+
"state": "complete",
|
|
192
|
+
"items_total": 3,
|
|
193
|
+
"items_done": 3,
|
|
194
|
+
"data": {
|
|
195
|
+
"files": 3,
|
|
196
|
+
"bytes": 16,
|
|
197
|
+
"by_extension": {".md": 3}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## What a run may ask for itself
|
|
203
|
+
|
|
204
|
+
A run record may carry three keys of its own, written when it is created:
|
|
205
|
+
|
|
206
|
+
- `action` — `check`, `dry_run` or `fix`, in place of the process's own, for this run.
|
|
207
|
+
- `items` — paths relative to the app, taken in place of the selection. A path that is
|
|
208
|
+
gone is listed under `items_missing` rather than failing the run; a path outside the
|
|
209
|
+
app fails it.
|
|
210
|
+
- `commit: true` — once the run completes, commit what it wrote and nothing else. The
|
|
211
|
+
result replaces the key: `{"sha": ..., "files": [...]}`, `{"sha": null, "files": []}`
|
|
212
|
+
when nothing changed, or `{"error": ..., "files": [...]}` when git refused — the
|
|
213
|
+
changes stay either way.
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{"process_id": "...", "state": "pending", "action": "fix", "items": ["docs/a.md"], "commit": true}
|
|
173
217
|
```
|
|
174
218
|
|
|
219
|
+
What a run wrote is what its type declared through `context.touch(path)`: the operations
|
|
220
|
+
of a filestate fix, the items an agent changed and their engrams.
|
|
221
|
+
|
|
222
|
+
## Runs that last a night
|
|
223
|
+
|
|
224
|
+
A worker takes one message at a time from the queue and acknowledges it at once: the
|
|
225
|
+
run's record, not the broker, says what became of it. The run is worked on in a thread
|
|
226
|
+
of its own while the worker keeps the broker connection alive, so a run of several
|
|
227
|
+
hours needs no RabbitMQ setting — neither `consumer_timeout` nor the heartbeat can cut
|
|
228
|
+
it. A connection lost meanwhile is opened again once the run ends; the board is told of
|
|
229
|
+
the run's moves on a best-effort basis and reads the record again anyway.
|
|
230
|
+
|
|
231
|
+
A running run writes `date_alive` into its record every minute. One still `running`
|
|
232
|
+
whose last sign of life is too old was interrupted — worker killed, machine stopped —
|
|
233
|
+
and is said `failed`, never run again: by the next run of the same app, and by any
|
|
234
|
+
worker that served that app, within a minute of finding its queue empty.
|
|
235
|
+
|
|
236
|
+
| Variable | Default | What it sets |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `PROCESS_RUN_STALE_AFTER` | `600` | Seconds without a sign of life after which a running run is taken for dead |
|
|
239
|
+
| `RABBITMQ_HEARTBEAT` | the broker's | Seconds between two heartbeats on the worker's connection |
|
|
240
|
+
|
|
241
|
+
There is no limit on how long a run may take.
|
|
242
|
+
|
|
243
|
+
## An item that fails, a run that is cancelled
|
|
244
|
+
|
|
245
|
+
A type that cannot deal with one item says so with `context.fail(path, reason)` and
|
|
246
|
+
moves on: the run still completes, and lists what it could not do under its data.
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
"data": {"errors": [{"path": "docs/a.pdf", "reason": "docling timed out"}]}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
To stop a run, write `"cancel": true` into its record. The runner checks it before every
|
|
253
|
+
item (`context.each()`, `context.advance()`, or `context.check_cancel()` in a type's own
|
|
254
|
+
loop) and ends the run `cancelled`, with `items_done` where it got to and the errors and
|
|
255
|
+
artifacts collected so far. A run cancelled before it was taken ends `cancelled` without
|
|
256
|
+
starting.
|
|
257
|
+
|
|
175
258
|
## Tests
|
|
176
259
|
|
|
177
260
|
This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
|
|
@@ -229,7 +312,7 @@ The addon is one runner, a few types, and two ways to reach it.
|
|
|
229
312
|
|
|
230
313
|
src/wexample_wex_addon_process/runner/process_runner.py is where a run is taken from `pending` to `complete` or to `failed`. It reads the run, the process it names, the selection that process names, resolves the type, walks the files, and writes back at every state change.
|
|
231
314
|
|
|
232
|
-
Three decisions are worth reading. A run in any state but `pending` is left where it is, because a queue redelivers and a run is done once.
|
|
315
|
+
Three decisions are worth reading. A run in any state but `pending` is left where it is, because a queue redelivers and a run is done once. Progress — the files done and the file being worked on, `current` — is kept in memory and written down at most four times a second, each write followed by a ring: the other end reads the record when rung, and ten thousand files must not be ten thousand rewrites saying the same thing. A state change is never held back, and a failed run keeps `current` on the file it failed on. And the type never sees the run: it is handed a src/wexample_wex_addon_process/process_type/process_context.py holding the files, the options and a way to say how far it has got, so what a run is *in* is decided in one place.
|
|
233
316
|
|
|
234
317
|
### Reading a selection
|
|
235
318
|
|
|
@@ -247,7 +330,7 @@ A type's name carries its family: `filestate:mode`, `agent:review`. The prefix i
|
|
|
247
330
|
|
|
248
331
|
src/wexample_wex_addon_process/process_type/family/file_state_process_type_family.py declares none of its types by hand. It walks filestate's `DefaultOptionsProvider` and keeps the options that override `create_required_operation` — the precise line between a treatment and a piece of structure, asked of the option itself rather than listed here. `mode` is a treatment, `children` is not. Thirteen types come out of it, and an option added to filestate is a fourteenth the same day.
|
|
249
332
|
|
|
250
|
-
Each of those is a src/wexample_wex_addon_process/process_type/filestate/file_state_option_process_type.py, which holds no logic either: the process's `options` become the configuration asked of every selected file, and filestate turns the difference with the disk into operations that can be described, applied and undone. `
|
|
333
|
+
Each of those is a src/wexample_wex_addon_process/process_type/filestate/file_state_option_process_type.py, which holds no logic either: the process's `options` become the configuration asked of every selected file, and filestate turns the difference with the disk into operations that can be described, applied and undone. `action` in the options picks the verb: `check` returns verdicts per file, `dry_run` the operations a pass would apply, `fix` applies them.
|
|
251
334
|
|
|
252
335
|
src/wexample_wex_addon_process/process_type/filestate/file_state_report_process_type.py is in that family and yet emits no operation, on filestate's own rule: a scope names something observable on disk, and a report changes nothing observable. An operation with no scope would be an operation nothing could ever run.
|
|
253
336
|
|
|
@@ -274,8 +357,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
|
|
|
274
357
|
## Dependencies
|
|
275
358
|
|
|
276
359
|
- wexample-file: >=9.2.0
|
|
277
|
-
- wexample-queue: >=
|
|
278
|
-
- wexample-wex-addon-app: >=
|
|
360
|
+
- wexample-queue: >=2.0.0
|
|
361
|
+
- wexample-wex-addon-app: >=32.0.0
|
|
279
362
|
|
|
280
363
|
## Versioning & Compatibility Policy
|
|
281
364
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wex-addon-process
|
|
2
2
|
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
|
|
5
5
|
`wexample-wex-addon-process` runs the treatments a board declares. A *process* says what to run and on which selection of files; a *run* is one execution of it, asked for by writing a record and picked up by a worker.
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ wex process::worker/start
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Blocks, consuming the `process_run` queue, and rings back on `process_run_event`
|
|
26
|
-
every time a run moves — on each state change, and at most
|
|
26
|
+
every time a run moves — on each state change, and at most four times a second while
|
|
27
27
|
it advances. This is the process a worker container runs, and there may be as
|
|
28
28
|
many as the parallelism asked for.
|
|
29
29
|
|
|
@@ -47,17 +47,27 @@ filestate:managed_blocks filestate:class filestate:on_bad_format
|
|
|
47
47
|
Nothing is written per treatment: an option added to filestate is a type
|
|
48
48
|
available here the same day, under the very name filestate gives it.
|
|
49
49
|
|
|
50
|
-
The process's `options`
|
|
50
|
+
The process's `options` object is the configuration handed to each file, and has
|
|
51
51
|
to carry the option the type is named after:
|
|
52
52
|
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
mode:
|
|
56
|
-
dry_run: true
|
|
53
|
+
```json
|
|
54
|
+
// the options of a process of type filestate:mode
|
|
55
|
+
{"mode": "600", "action": "dry_run"}
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
`
|
|
60
|
-
|
|
58
|
+
`action` is read by the family and handed to nobody. It picks which of
|
|
59
|
+
filestate's three verbs is asked, the same three the command line offers on an
|
|
60
|
+
app's declared state:
|
|
61
|
+
|
|
62
|
+
- `check`, the default, says what is wrong with each selected file and changes
|
|
63
|
+
nothing. Its `data` counts the files `checked` and `failing`, and lists under
|
|
64
|
+
`files` only those at fault, each with its `code`, `message`, `parameters` and
|
|
65
|
+
`remedy`, the name of the operation that would fix it or null when nothing can.
|
|
66
|
+
- `dry_run` lists the operations one pass would apply.
|
|
67
|
+
- `fix` applies them, then lists what is still wrong, the way `check` does.
|
|
68
|
+
|
|
69
|
+
A rule that only observes, such as `max_lines`, is a type like the others: it has
|
|
70
|
+
no remedy, so a `fix` changes nothing and reports it again.
|
|
61
71
|
|
|
62
72
|
## Writing a type
|
|
63
73
|
|
|
@@ -69,12 +79,17 @@ class ReviewProcessType(AbstractProcessType):
|
|
|
69
79
|
label = "Review"
|
|
70
80
|
|
|
71
81
|
def run(self, context: ProcessContext) -> dict[str, Any]:
|
|
72
|
-
for path in context.
|
|
73
|
-
|
|
82
|
+
for path in context.each():
|
|
83
|
+
...
|
|
74
84
|
|
|
75
85
|
return {"files": len(context.files)}
|
|
76
86
|
```
|
|
77
87
|
|
|
88
|
+
`context.each()` hands the files over one at a time and keeps the run up to
|
|
89
|
+
date: the file being worked on is the run's `current`, and it is counted once
|
|
90
|
+
the loop comes back for the next one. A file whose treatment raises is never
|
|
91
|
+
counted and stays `current`, so a failed run says where it failed.
|
|
92
|
+
|
|
78
93
|
What `run()` returns goes into the run's `data`. Raising is how a type says it
|
|
79
94
|
could not go on — the failure is written down for it, so nothing in a type has
|
|
80
95
|
to know what a failed run looks like.
|
|
@@ -90,6 +105,9 @@ exist.
|
|
|
90
105
|
- [The types on offer](#the-types-on-offer)
|
|
91
106
|
- [Writing a type](#writing-a-type)
|
|
92
107
|
- [Installation](#installation)
|
|
108
|
+
- [What a run may ask for itself](#what-a-run-may-ask-for-itself)
|
|
109
|
+
- [Runs that last a night](#runs-that-last-a-night)
|
|
110
|
+
- [An item that fails, a run that is cancelled](#an-item-that-fails-a-run-that-is-cancelled)
|
|
93
111
|
- [Tests](#tests)
|
|
94
112
|
- [Architecture](#architecture)
|
|
95
113
|
- [Integration in the Suite](#integration-in-the-suite)
|
|
@@ -110,28 +128,35 @@ pip install wexample-wex-addon-process
|
|
|
110
128
|
|
|
111
129
|
Requires Python >=3.10.
|
|
112
130
|
|
|
113
|
-
A process and a selection are records, so a treatment can be declared without a board
|
|
131
|
+
A process and a selection are records, so a treatment can be declared without a board.
|
|
132
|
+
Records are JSON, written and read by programs on both sides:
|
|
114
133
|
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
```json
|
|
135
|
+
// .wex/data/selection/<uuid>.json
|
|
136
|
+
{
|
|
137
|
+
"title": "Markdowns",
|
|
138
|
+
"patterns": "*.md\n!.wex/**"
|
|
139
|
+
}
|
|
119
140
|
```
|
|
120
141
|
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
```json
|
|
143
|
+
// .wex/data/process/<uuid>.json
|
|
144
|
+
{
|
|
145
|
+
"title": "What the Markdowns take",
|
|
146
|
+
"type": "filestate:report",
|
|
147
|
+
"selection_id": "<the selection's uuid>",
|
|
148
|
+
"options": {}
|
|
149
|
+
}
|
|
127
150
|
```
|
|
128
151
|
|
|
129
152
|
Asking for a run is writing a third record:
|
|
130
153
|
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
```json
|
|
155
|
+
// .wex/data/process_run/<uuid>.json
|
|
156
|
+
{
|
|
157
|
+
"process_id": "<the process's uuid>",
|
|
158
|
+
"state": "pending"
|
|
159
|
+
}
|
|
135
160
|
```
|
|
136
161
|
|
|
137
162
|
Then:
|
|
@@ -142,18 +167,76 @@ wex process::run/execute --run <the run's uuid>
|
|
|
142
167
|
|
|
143
168
|
The same file is read back afterwards, holding what happened:
|
|
144
169
|
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.md: 3
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"process_id": "...",
|
|
173
|
+
"state": "complete",
|
|
174
|
+
"items_total": 3,
|
|
175
|
+
"items_done": 3,
|
|
176
|
+
"data": {
|
|
177
|
+
"files": 3,
|
|
178
|
+
"bytes": 16,
|
|
179
|
+
"by_extension": {".md": 3}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## What a run may ask for itself
|
|
185
|
+
|
|
186
|
+
A run record may carry three keys of its own, written when it is created:
|
|
187
|
+
|
|
188
|
+
- `action` — `check`, `dry_run` or `fix`, in place of the process's own, for this run.
|
|
189
|
+
- `items` — paths relative to the app, taken in place of the selection. A path that is
|
|
190
|
+
gone is listed under `items_missing` rather than failing the run; a path outside the
|
|
191
|
+
app fails it.
|
|
192
|
+
- `commit: true` — once the run completes, commit what it wrote and nothing else. The
|
|
193
|
+
result replaces the key: `{"sha": ..., "files": [...]}`, `{"sha": null, "files": []}`
|
|
194
|
+
when nothing changed, or `{"error": ..., "files": [...]}` when git refused — the
|
|
195
|
+
changes stay either way.
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{"process_id": "...", "state": "pending", "action": "fix", "items": ["docs/a.md"], "commit": true}
|
|
155
199
|
```
|
|
156
200
|
|
|
201
|
+
What a run wrote is what its type declared through `context.touch(path)`: the operations
|
|
202
|
+
of a filestate fix, the items an agent changed and their engrams.
|
|
203
|
+
|
|
204
|
+
## Runs that last a night
|
|
205
|
+
|
|
206
|
+
A worker takes one message at a time from the queue and acknowledges it at once: the
|
|
207
|
+
run's record, not the broker, says what became of it. The run is worked on in a thread
|
|
208
|
+
of its own while the worker keeps the broker connection alive, so a run of several
|
|
209
|
+
hours needs no RabbitMQ setting — neither `consumer_timeout` nor the heartbeat can cut
|
|
210
|
+
it. A connection lost meanwhile is opened again once the run ends; the board is told of
|
|
211
|
+
the run's moves on a best-effort basis and reads the record again anyway.
|
|
212
|
+
|
|
213
|
+
A running run writes `date_alive` into its record every minute. One still `running`
|
|
214
|
+
whose last sign of life is too old was interrupted — worker killed, machine stopped —
|
|
215
|
+
and is said `failed`, never run again: by the next run of the same app, and by any
|
|
216
|
+
worker that served that app, within a minute of finding its queue empty.
|
|
217
|
+
|
|
218
|
+
| Variable | Default | What it sets |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `PROCESS_RUN_STALE_AFTER` | `600` | Seconds without a sign of life after which a running run is taken for dead |
|
|
221
|
+
| `RABBITMQ_HEARTBEAT` | the broker's | Seconds between two heartbeats on the worker's connection |
|
|
222
|
+
|
|
223
|
+
There is no limit on how long a run may take.
|
|
224
|
+
|
|
225
|
+
## An item that fails, a run that is cancelled
|
|
226
|
+
|
|
227
|
+
A type that cannot deal with one item says so with `context.fail(path, reason)` and
|
|
228
|
+
moves on: the run still completes, and lists what it could not do under its data.
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
"data": {"errors": [{"path": "docs/a.pdf", "reason": "docling timed out"}]}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
To stop a run, write `"cancel": true` into its record. The runner checks it before every
|
|
235
|
+
item (`context.each()`, `context.advance()`, or `context.check_cancel()` in a type's own
|
|
236
|
+
loop) and ends the run `cancelled`, with `items_done` where it got to and the errors and
|
|
237
|
+
artifacts collected so far. A run cancelled before it was taken ends `cancelled` without
|
|
238
|
+
starting.
|
|
239
|
+
|
|
157
240
|
## Tests
|
|
158
241
|
|
|
159
242
|
This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
|
|
@@ -211,7 +294,7 @@ The addon is one runner, a few types, and two ways to reach it.
|
|
|
211
294
|
|
|
212
295
|
src/wexample_wex_addon_process/runner/process_runner.py is where a run is taken from `pending` to `complete` or to `failed`. It reads the run, the process it names, the selection that process names, resolves the type, walks the files, and writes back at every state change.
|
|
213
296
|
|
|
214
|
-
Three decisions are worth reading. A run in any state but `pending` is left where it is, because a queue redelivers and a run is done once.
|
|
297
|
+
Three decisions are worth reading. A run in any state but `pending` is left where it is, because a queue redelivers and a run is done once. Progress — the files done and the file being worked on, `current` — is kept in memory and written down at most four times a second, each write followed by a ring: the other end reads the record when rung, and ten thousand files must not be ten thousand rewrites saying the same thing. A state change is never held back, and a failed run keeps `current` on the file it failed on. And the type never sees the run: it is handed a src/wexample_wex_addon_process/process_type/process_context.py holding the files, the options and a way to say how far it has got, so what a run is *in* is decided in one place.
|
|
215
298
|
|
|
216
299
|
### Reading a selection
|
|
217
300
|
|
|
@@ -229,7 +312,7 @@ A type's name carries its family: `filestate:mode`, `agent:review`. The prefix i
|
|
|
229
312
|
|
|
230
313
|
src/wexample_wex_addon_process/process_type/family/file_state_process_type_family.py declares none of its types by hand. It walks filestate's `DefaultOptionsProvider` and keeps the options that override `create_required_operation` — the precise line between a treatment and a piece of structure, asked of the option itself rather than listed here. `mode` is a treatment, `children` is not. Thirteen types come out of it, and an option added to filestate is a fourteenth the same day.
|
|
231
314
|
|
|
232
|
-
Each of those is a src/wexample_wex_addon_process/process_type/filestate/file_state_option_process_type.py, which holds no logic either: the process's `options` become the configuration asked of every selected file, and filestate turns the difference with the disk into operations that can be described, applied and undone. `
|
|
315
|
+
Each of those is a src/wexample_wex_addon_process/process_type/filestate/file_state_option_process_type.py, which holds no logic either: the process's `options` become the configuration asked of every selected file, and filestate turns the difference with the disk into operations that can be described, applied and undone. `action` in the options picks the verb: `check` returns verdicts per file, `dry_run` the operations a pass would apply, `fix` applies them.
|
|
233
316
|
|
|
234
317
|
src/wexample_wex_addon_process/process_type/filestate/file_state_report_process_type.py is in that family and yet emits no operation, on filestate's own rule: a scope names something observable on disk, and a report changes nothing observable. An operation with no scope would be an operation nothing could ever run.
|
|
235
318
|
|
|
@@ -256,8 +339,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
|
|
|
256
339
|
## Dependencies
|
|
257
340
|
|
|
258
341
|
- wexample-file: >=9.2.0
|
|
259
|
-
- wexample-queue: >=
|
|
260
|
-
- wexample-wex-addon-app: >=
|
|
342
|
+
- wexample-queue: >=2.0.0
|
|
343
|
+
- wexample-wex-addon-app: >=32.0.0
|
|
261
344
|
|
|
262
345
|
## Versioning & Compatibility Policy
|
|
263
346
|
|
|
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "wexample-wex-addon-process"
|
|
9
|
-
version = "
|
|
9
|
+
version = "3.0.0"
|
|
10
10
|
description = "Executes process runs defined on disk—either directly or by consuming them from a queue as a worker—against filestate selections"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "weeger", email = "contact@wexample.com" },
|
|
@@ -19,8 +19,8 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
21
|
"wexample-file>=9.2.0",
|
|
22
|
-
"wexample-queue>=
|
|
23
|
-
"wexample-wex-addon-app>=
|
|
22
|
+
"wexample-queue>=2.0.0",
|
|
23
|
+
"wexample-wex-addon-app>=32.0.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[project.readme]
|
|
@@ -42,10 +42,15 @@ def process__run__execute(
|
|
|
42
42
|
ProcessTypeRegistry,
|
|
43
43
|
)
|
|
44
44
|
from wexample_wex_addon_process.runner.process_runner import ProcessRunner
|
|
45
|
+
from wexample_wex_addon_process.selection.selection_registry import (
|
|
46
|
+
SelectionRegistry,
|
|
47
|
+
)
|
|
45
48
|
|
|
46
49
|
runner = ProcessRunner(
|
|
47
50
|
io=context.io,
|
|
48
51
|
registry=ProcessTypeRegistry.from_kernel(context.kernel),
|
|
52
|
+
selections=SelectionRegistry.from_kernel(context.kernel),
|
|
53
|
+
kernel=context.kernel,
|
|
49
54
|
)
|
|
50
55
|
|
|
51
56
|
record = runner.execute(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""The options a filestate process type reads for itself, before handing the rest
|
|
2
|
+
to each selected file.
|
|
3
|
+
|
|
4
|
+
The three actions are filestate's three verbs, and the same three the command
|
|
5
|
+
line offers on an app's declared state: `app::state/check`, `app::state/rectify
|
|
6
|
+
--dry-run`, `app::state/rectify`. A process is that, on a selection instead of a
|
|
7
|
+
whole declared tree.
|
|
8
|
+
|
|
9
|
+
The key must not be the name of a filestate option, since every other key of the
|
|
10
|
+
block is handed to each file as its configuration: `mode` already means chmod.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# filestate: python-constant-sort
|
|
16
|
+
ACTION_CHECK: str = "check"
|
|
17
|
+
ACTION_DRY_RUN: str = "dry_run"
|
|
18
|
+
ACTION_FIX: str = "fix"
|
|
19
|
+
ACTIONS: tuple[str, ...] = (ACTION_CHECK, ACTION_DRY_RUN, ACTION_FIX)
|
|
20
|
+
|
|
21
|
+
OPTION_KEY_ACTION: str = "action"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""What a process, a selection and a run are called on disk.
|
|
2
|
+
|
|
3
|
+
Spelled once here because wex is not the only reader: the board writes these very
|
|
4
|
+
keys into the same files, so a name changed on one side has to be changed on the
|
|
5
|
+
other or the two stop understanding each other.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# filestate: python-constant-sort
|
|
11
|
+
ARTIFACT_TYPE_FILE: str = "file"
|
|
12
|
+
|
|
13
|
+
# Under a run's data: the files it produced to be handed back, each
|
|
14
|
+
# {type, path, label}.
|
|
15
|
+
DATA_KEY_ARTIFACTS: str = "artifacts"
|
|
16
|
+
|
|
17
|
+
# Under a run's data: the items a type could not deal with, each {path, reason}.
|
|
18
|
+
# The run still completes — one bad file does not sink the batch.
|
|
19
|
+
DATA_KEY_ERRORS: str = "errors"
|
|
20
|
+
|
|
21
|
+
DEFAULT_COMMIT_AUTHOR: str = "wex <wex@localhost>"
|
|
22
|
+
"""Who commits a run's work when neither the run nor git names anyone."""
|
|
23
|
+
|
|
24
|
+
ENV_PROCESS_RUN_STALE_AFTER: str = "PROCESS_RUN_STALE_AFTER"
|
|
25
|
+
"""Seconds without a sign of life after which a running run is taken for dead."""
|
|
26
|
+
|
|
27
|
+
KIND_PROCESS: str = "process"
|
|
28
|
+
KIND_PROCESS_RUN: str = "process_run"
|
|
29
|
+
KIND_SELECTION: str = "selection"
|
|
30
|
+
|
|
31
|
+
PROCESS_KEY_OPTIONS: str = "options"
|
|
32
|
+
PROCESS_KEY_SELECTION_ID: str = "selection_id"
|
|
33
|
+
PROCESS_KEY_TITLE: str = "title"
|
|
34
|
+
PROCESS_KEY_TYPE: str = "type"
|
|
35
|
+
|
|
36
|
+
RUN_KEY_ACTION: str = "action"
|
|
37
|
+
RUN_KEY_AUTHOR: str = "author"
|
|
38
|
+
RUN_KEY_CANCEL: str = "cancel"
|
|
39
|
+
"""Written `true` by whoever wants the run stopped; read between two items."""
|
|
40
|
+
RUN_KEY_COMMIT: str = "commit"
|
|
41
|
+
RUN_KEY_CURRENT: str = "current"
|
|
42
|
+
RUN_KEY_DATA: str = "data"
|
|
43
|
+
RUN_KEY_DATE_ALIVE: str = "date_alive"
|
|
44
|
+
RUN_KEY_DATE_ENDED: str = "date_ended"
|
|
45
|
+
RUN_KEY_DATE_STARTED: str = "date_started"
|
|
46
|
+
RUN_KEY_ITEMS: str = "items"
|
|
47
|
+
RUN_KEY_ITEMS_DONE: str = "items_done"
|
|
48
|
+
RUN_KEY_ITEMS_MISSING: str = "items_missing"
|
|
49
|
+
RUN_KEY_ITEMS_TOTAL: str = "items_total"
|
|
50
|
+
RUN_KEY_PROCESS_ID: str = "process_id"
|
|
51
|
+
RUN_KEY_STATE: str = "state"
|
|
52
|
+
|
|
53
|
+
SELECTION_KEY_PATTERNS: str = "patterns"
|
|
54
|
+
SELECTION_KEY_PROVIDER: str = "provider"
|
|
55
|
+
SELECTION_KEY_TITLE: str = "title"
|
|
56
|
+
|
|
57
|
+
RUN_ALIVE_INTERVAL: float = 60.0
|
|
58
|
+
"""Seconds between two signs of life a running run writes into its record."""
|
|
59
|
+
|
|
60
|
+
RUN_STALE_AFTER_DEFAULT: float = 600.0
|
|
61
|
+
"""How long a running run may stay silent before it is taken for dead, unless
|
|
62
|
+
`PROCESS_RUN_STALE_AFTER` says otherwise. Ten signs of life missed: a busy machine
|
|
63
|
+
may skip one, never ten."""
|
|
64
|
+
|
|
65
|
+
STATE_CANCELLED: str = "cancelled"
|
|
66
|
+
STATE_COMPLETE: str = "complete"
|
|
67
|
+
STATE_FAILED: str = "failed"
|
|
68
|
+
STATE_PENDING: str = "pending"
|
|
69
|
+
STATE_RUNNING: str = "running"
|