pyattacker 0.1.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.
Files changed (74) hide show
  1. pyattacker-0.1.0/.github/workflows/ci.yml +53 -0
  2. pyattacker-0.1.0/.gitignore +17 -0
  3. pyattacker-0.1.0/CHANGELOG.md +91 -0
  4. pyattacker-0.1.0/LICENSE +21 -0
  5. pyattacker-0.1.0/PKG-INFO +347 -0
  6. pyattacker-0.1.0/README.md +323 -0
  7. pyattacker-0.1.0/docs/cli.md +203 -0
  8. pyattacker-0.1.0/docs/design.md +639 -0
  9. pyattacker-0.1.0/docs/reference.md +1336 -0
  10. pyattacker-0.1.0/docs/tutorial.md +1641 -0
  11. pyattacker-0.1.0/examples/data.jsonl +5 -0
  12. pyattacker-0.1.0/examples/llm_eval/README.md +122 -0
  13. pyattacker-0.1.0/examples/llm_eval/__init__.py +4 -0
  14. pyattacker-0.1.0/examples/llm_eval/backend.py +264 -0
  15. pyattacker-0.1.0/examples/llm_eval/demo.py +193 -0
  16. pyattacker-0.1.0/examples/llm_eval/pipelines.py +347 -0
  17. pyattacker-0.1.0/examples/plugin_package/README.md +23 -0
  18. pyattacker-0.1.0/examples/plugin_package/pa_demo_plugin/__init__.py +10 -0
  19. pyattacker-0.1.0/examples/plugin_package/pa_demo_plugin/algorithms.py +34 -0
  20. pyattacker-0.1.0/examples/plugin_package/pa_demo_plugin/codecs.py +34 -0
  21. pyattacker-0.1.0/examples/plugin_package/pa_demo_plugin/tasks.py +35 -0
  22. pyattacker-0.1.0/examples/plugin_package/pyproject.toml +31 -0
  23. pyattacker-0.1.0/examples/plugin_tasks.yaml +33 -0
  24. pyattacker-0.1.0/examples/qa_eval.yaml +58 -0
  25. pyattacker-0.1.0/examples/quickstart.py +140 -0
  26. pyattacker-0.1.0/examples/sharded.py +135 -0
  27. pyattacker-0.1.0/pyproject.toml +82 -0
  28. pyattacker-0.1.0/src/pyattacker/__init__.py +198 -0
  29. pyattacker-0.1.0/src/pyattacker/__main__.py +12 -0
  30. pyattacker-0.1.0/src/pyattacker/algorithm.py +355 -0
  31. pyattacker-0.1.0/src/pyattacker/artifact.py +255 -0
  32. pyattacker-0.1.0/src/pyattacker/backends.py +228 -0
  33. pyattacker-0.1.0/src/pyattacker/cli.py +682 -0
  34. pyattacker-0.1.0/src/pyattacker/declarative.py +327 -0
  35. pyattacker-0.1.0/src/pyattacker/errors.py +223 -0
  36. pyattacker-0.1.0/src/pyattacker/export.py +291 -0
  37. pyattacker-0.1.0/src/pyattacker/merge.py +177 -0
  38. pyattacker-0.1.0/src/pyattacker/monitor.py +108 -0
  39. pyattacker-0.1.0/src/pyattacker/pipeline.py +241 -0
  40. pyattacker-0.1.0/src/pyattacker/plugins.py +244 -0
  41. pyattacker-0.1.0/src/pyattacker/resource.py +1018 -0
  42. pyattacker-0.1.0/src/pyattacker/runner.py +1406 -0
  43. pyattacker-0.1.0/src/pyattacker/scheduler.py +135 -0
  44. pyattacker-0.1.0/src/pyattacker/server.py +297 -0
  45. pyattacker-0.1.0/src/pyattacker/shard.py +107 -0
  46. pyattacker-0.1.0/src/pyattacker/store/__init__.py +32 -0
  47. pyattacker-0.1.0/src/pyattacker/store/base.py +351 -0
  48. pyattacker-0.1.0/src/pyattacker/store/memory.py +368 -0
  49. pyattacker-0.1.0/src/pyattacker/store/sqlite.py +734 -0
  50. pyattacker-0.1.0/src/pyattacker/store/writebehind.py +262 -0
  51. pyattacker-0.1.0/src/pyattacker/task.py +508 -0
  52. pyattacker-0.1.0/src/pyattacker/tasks/__init__.py +380 -0
  53. pyattacker-0.1.0/tests/helpers.py +50 -0
  54. pyattacker-0.1.0/tests/test_algorithms.py +591 -0
  55. pyattacker-0.1.0/tests/test_artifact.py +130 -0
  56. pyattacker-0.1.0/tests/test_backends.py +466 -0
  57. pyattacker-0.1.0/tests/test_cli.py +398 -0
  58. pyattacker-0.1.0/tests/test_declarative.py +500 -0
  59. pyattacker-0.1.0/tests/test_errors.py +193 -0
  60. pyattacker-0.1.0/tests/test_export.py +741 -0
  61. pyattacker-0.1.0/tests/test_lease_safety.py +313 -0
  62. pyattacker-0.1.0/tests/test_m2.py +232 -0
  63. pyattacker-0.1.0/tests/test_monitor.py +192 -0
  64. pyattacker-0.1.0/tests/test_packaging.py +92 -0
  65. pyattacker-0.1.0/tests/test_pipeline.py +197 -0
  66. pyattacker-0.1.0/tests/test_plugins.py +572 -0
  67. pyattacker-0.1.0/tests/test_runner.py +632 -0
  68. pyattacker-0.1.0/tests/test_scheduler.py +268 -0
  69. pyattacker-0.1.0/tests/test_server.py +451 -0
  70. pyattacker-0.1.0/tests/test_shard.py +302 -0
  71. pyattacker-0.1.0/tests/test_store.py +679 -0
  72. pyattacker-0.1.0/tests/test_tasks.py +130 -0
  73. pyattacker-0.1.0/tests/test_tutorial.py +95 -0
  74. pyattacker-0.1.0/uv.lock +167 -0
@@ -0,0 +1,53 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: python ${{ matrix.python-version }}
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ # The two versions declared in pyproject.toml classifiers.
21
+ python-version: ["3.11", "3.12"]
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v5
27
+ with:
28
+ enable-cache: true
29
+ python-version: ${{ matrix.python-version }}
30
+
31
+ - name: Install dependencies
32
+ run: uv sync --locked
33
+
34
+ - name: Lint
35
+ run: uv run ruff check
36
+
37
+ - name: Test
38
+ # The whole suite is offline; network access is never needed.
39
+ run: uv run pytest
40
+
41
+ - name: CLI smoke test
42
+ run: uv run pyattacker demo --pipelines 25 --store runs/demo.db
43
+
44
+ - name: Example smoke tests
45
+ # The examples are documentation, and documentation rots: the llm_eval demo also asserts
46
+ # its own claim about checkpoint granularity, so this catches recovery regressions.
47
+ run: |
48
+ uv run python examples/quickstart.py
49
+ uv run python examples/sharded.py
50
+ uv run python -m examples.llm_eval.demo
51
+
52
+ - name: Build sdist and wheel
53
+ run: uv build
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ .uv-cache/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ runs/
12
+ *.db
13
+ *.db-wal
14
+ *.db-shm
15
+ .pyattacker/
16
+
17
+ .scratch/
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] — 2026-09-17
8
+
9
+ First release. An artifact-centric async orchestration kernel: it runs many independent pipelines to
10
+ completion, resumably and observably, without touching the network itself.
11
+
12
+ ### Core
13
+
14
+ * **Five-concept kernel** — artifact, task, pipeline, resource, algorithm. A task is a unary
15
+ `(artifact) -> artifact` function, sync or async; a pipeline is a linear chain of them and the unit of
16
+ completion; pipelines share nothing but resource pools.
17
+ * **Task-level checkpoints.** Every artifact is persisted the moment it is produced, so `resume` restarts at
18
+ the first task that produced nothing and re-sends nothing that already succeeded. The seed artifact is
19
+ persisted too, so recovery does not depend on the original dataset file.
20
+ * **Content-addressed identity.** `pipeline_key` is a digest of the task-chain fingerprint, the seed, and the
21
+ repeat index, which makes reruns idempotent, results reproducible, and `map(seeds, repeats=k)` (pass@k) free.
22
+ Task source digests are included, so changing task code is treated as a new pipeline rather than reusing a
23
+ stale checkpoint.
24
+ * **Lease safety contract.** Resources are only usable through a lease, and a task never holds one after it
25
+ ends: `async with ctx.acquire(...)` returns on exit, on exception, on cancellation and on timeout, and the
26
+ escape hatch is force-reclaimed with a `lease.leaked` event. Reclaim is a pure synchronous function, so
27
+ `CancelledError` cannot interrupt it. `strict_leases=True` turns a leak into a task failure.
28
+ * **Resource pools** with per-resource capacity, a `READY/DEGRADED/DEAD/REVOKED` state machine driven by
29
+ explicit `lease.report(...)`, quota accounting, publish/subscribe, and a signal bus.
30
+ * **Seven acquisition algorithms** — `wait`, `backoff`, `least_busy`, `failover`, `sticky`, `quota_aware`,
31
+ `immediate` — kept orthogonal to retry policy: the algorithm decides how to *get* a resource, retry decides
32
+ what to do *after* a failure.
33
+ * **Failure classification and retry.** `error_class_of` is a pure function mapping status codes and exception
34
+ types to `timeout`/`rate_limit`/`upstream`/`fatal`/`connection`/`invalid`/`unknown`; `Retrying` adds
35
+ exponential backoff with full jitter and a total-time budget. No retry by default. **Every retry decision is
36
+ persisted** — `{retry, reason, delay_s, error_class, attempt, max_attempts, retry_after}`.
37
+
38
+ ### Scheduling and persistence
39
+
40
+ * **Backoff parks the pipeline instead of sleeping in a worker**, so `concurrency` means attempts in flight.
41
+ A parked pipeline is never mistaken for a dead one: the run waits for it, and a stop records it as resumable.
42
+ * **Targeted pool wakeups** — releasing a resource wakes only the waiters whose selector can use it.
43
+ * **Write-behind batching** for append-only facts (attempts, events), while state writes — pipelines, tasks,
44
+ artifacts — always go straight through. `SIGKILL` can cost the last batch of history, never a checkpoint.
45
+ `--no-write-behind` opts out.
46
+ * **SQLite store** (WAL, `synchronous=NORMAL`) across seven tables plus an in-memory store with identical
47
+ semantics. WAL's one-writer-many-readers model is what lets `watch`/`report`/`serve` run beside a live run.
48
+ * **Wait-time metrics** — `waits_total`, `wait_ms_avg`, `p50`, `p95`, `max`, and an `acquire.slow_wait` event
49
+ past `slow_wait_ms`. Suspected deadlocks emit `acquire.suspected_deadlock`.
50
+
51
+ ### Scale, I/O and ecosystem
52
+
53
+ * **Deterministic sharding** — `--shard I/N` for one process doing its share, `--shards N` to spawn children
54
+ locally. Assignment is a pure function of the pipeline key, so the split is stable and resume-safe.
55
+ * **Merged reports** across shard stores: de-duplicated by `pipeline_id`, statistics recomputed, folded row
56
+ count reported.
57
+ * **Export** in five row shapes (`pipelines`, `tasks`, `attempts`, `events`, `artifacts`) and three formats
58
+ (`jsonl`, `json`, `csv`, with an `extra` column for late keys).
59
+ * **Entry-point plugins** for tasks, algorithms, codecs and stores. Built-ins resolve first, and a plugin that
60
+ raises on import is recorded rather than fatal — `pyattacker plugins` shows both.
61
+ * **Artifact backends** — `inline` (default), content-addressed `file://` spilling above a threshold with
62
+ transparent hydration on read, and `null`.
63
+ * **`fanout(...)`** for genuine branching inside one task, with group-level retry granularity as the stated price.
64
+ * **Read-only HTTP endpoint** — `pyattacker serve`, a dependency-free dashboard plus JSON. Loopback-only and
65
+ unauthenticated by design; it is a debug view, not a service.
66
+ * **CLI** — `run`, `resume`, `report`, `watch`, `export`, `serve`, `plugins`, `validate`, `demo`, with exit
67
+ codes `0`/`1`/`2`/`130`.
68
+ * **Declarative layer** — YAML/TOML/JSON describing composition and resources, with `${ENV}` expansion and
69
+ `--strict-env`.
70
+
71
+ ### Documentation and testing
72
+
73
+ * A fourteen-step [tutorial](docs/tutorial.md) whose every code block is extracted and executed by the test
74
+ suite, so documentation that rots fails CI.
75
+ * A [design document](docs/design.md) stating the six invariants, the lease contract, the data model, and
76
+ twelve known tradeoffs up front.
77
+ * A [CLI reference](docs/cli.md) and four worked examples, including one that *measures* the checkpoint
78
+ granularity tradeoff rather than asserting it.
79
+ * The suite is offline and deterministic — all time goes through an injectable `Clock` — and runs in seconds.
80
+ CI covers Python 3.11 and 3.12, lint, the examples, and a build.
81
+
82
+ ### Known limitations
83
+
84
+ Deliberate, and documented in [`docs/design.md`](docs/design.md) §8: stop conditions are best-effort;
85
+ write-behind can lose the tail of history (never a checkpoint); `journal=summary` and the `null` backend trade
86
+ away recovery granularity; resource health relies on explicit reporting; `quota_aware` is a preference, not a
87
+ hard limit; asyncio tasks only (wrap blocking code with `asyncio.to_thread`); one writer per database; shard
88
+ balance is statistical; a merged report is a union, not a sum; a pipeline is a linear chain; the HTTP endpoint
89
+ is unauthenticated.
90
+
91
+ [0.1.0]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 pyattacker contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,347 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyattacker
3
+ Version: 0.1.0
4
+ Summary: Artifact-centric, resumable async task orchestration: pipeline / task / resource pool.
5
+ Project-URL: Homepage, https://github.com/Hazer-BJTU/pyattacker
6
+ Project-URL: Repository, https://github.com/Hazer-BJTU/pyattacker
7
+ Project-URL: Issues, https://github.com/Hazer-BJTU/pyattacker/issues
8
+ Project-URL: Changelog, https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md
10
+ Project-URL: Design, https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md
11
+ Author: pyattacker contributors
12
+ License: MIT
13
+ License-File: LICENSE
14
+ Keywords: artifact,async,evaluation,orchestration,pipeline,resource-pool
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Framework :: AsyncIO
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: pyyaml>=6.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pyattacker
26
+
27
+ [![CI](https://github.com/Hazer-BJTU/pyattacker/actions/workflows/ci.yml/badge.svg)](https://github.com/Hazer-BJTU/pyattacker/actions/workflows/ci.yml)
28
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)](https://www.python.org/)
29
+ [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/Hazer-BJTU/pyattacker/blob/main/LICENSE)
30
+
31
+ > Run tens of thousands of independent tasks to completion — resumably, observably, and without
32
+ > reimplementing endpoint pools, retries and "which rows already ran" for the fifth time.
33
+
34
+ ## The situation this is for
35
+
36
+ Picture a long workflow test, a model benchmark, or any experiment made of many independent items.
37
+ Three hours in, one network request fails. The process dies — and you have no idea *which stage*
38
+ each item actually reached. Rerunning from scratch means paying for every request that already
39
+ succeeded, so you start writing a `results.jsonl` and a "skip what's already in there" check.
40
+
41
+ Then the provider starts rate-limiting you. A single API key serialises your parallel requests into
42
+ a queue, so you add a second key, a third, and now you need to decide which request goes where, what
43
+ happens when one endpoint starts returning 429s, and whether a failure means *retry* or *give up*.
44
+ Somewhere in there, `asyncio.Semaphore` stops being enough and you are writing a scheduler.
45
+
46
+ pyattacker is that scheduler, extracted and made boring:
47
+
48
+ * **a failure costs you one task, not the run** — every task's output is persisted the moment it is
49
+ produced, so `resume` restarts at the first task that produced nothing, and re-sends nothing that
50
+ already succeeded;
51
+ * **endpoints are a pool, not a global variable** — capacity, health, and quota per endpoint, leased
52
+ through `async with`, with seven policies for choosing which one to use and how to wait;
53
+ * **the record is queryable, not a log file** — every attempt, every retry decision (`{retry, reason,
54
+ delay_s, error_class}`), every intermediate artifact, in SQLite you can `SELECT` from while the run
55
+ is still going.
56
+
57
+ It **does not touch the network**: you write the openai/anthropic calls, it handles everything around
58
+ them. The core dependency list is the standard library plus PyYAML.
59
+
60
+ **New here?** The [tutorial](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) goes from a five-line program to a sharded, resumable
61
+ model evaluation. Every snippet in it is executed by the test suite.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ uv add pyattacker # or: pip install pyattacker
67
+ ```
68
+
69
+ From a clone:
70
+
71
+ ```bash
72
+ git clone https://github.com/Hazer-BJTU/pyattacker && cd pyattacker
73
+ uv sync
74
+ uv run pyattacker demo # zero-config smoke test: 50 simulated pipelines, retries, a report
75
+ ```
76
+
77
+ Requires Python 3.11+.
78
+
79
+ ## Core Model
80
+
81
+ Five concepts, and that is the whole vocabulary:
82
+
83
+ | Concept | Meaning | In one line |
84
+ |---|---|---|
85
+ | **artifact** | the persisted state of a task | content-addressed, **persisted as soon as it is produced** → checkpoint granularity = task |
86
+ | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async |
87
+ | **pipeline** | the unit of completion | `fetch \| ask \| judge \| metrics` chained linearly, semantically independent of each other |
88
+ | **resource** | a leasable external capability | one endpoint / one key; once pooled, it can be published and subscribed to concurrency-safely |
89
+ | **algorithm** | the policy for acquiring resources | `wait`, `backoff`, `least_busy`, `failover`, `sticky`, `quota_aware`, `immediate` — orthogonal to "retry on failure" |
90
+
91
+ ## 30-Second Quickstart (SDK)
92
+
93
+ ```python
94
+ from pyattacker import Pool, Resource, Retrying, Runner, pipeline, task
95
+
96
+ @task("ask", resource="apis", algorithm="backoff",
97
+ retry=Retrying(max_attempts=4, base=0.5, cap=30.0), timeout_s=60)
98
+ async def ask(row: dict, ctx) -> dict:
99
+ async with ctx.acquire(model="gpt-4o") as lease: # returned on exit; returned on exception too
100
+ text = await lease.client.chat(row["q"]) # lease.client is built by resource.factory
101
+ lease.report(ok=True, usage={"tokens": 128}) # report health/quota back to the pool
102
+ return {"q": row["q"], "a": text}
103
+
104
+ pool = Pool("apis",
105
+ [Resource.create("llm", capacity=4, options={"model": "gpt-4o"},
106
+ factory=lambda res: MyClient(res.options)) for _ in range(8)],
107
+ algorithm="backoff")
108
+
109
+ template = pipeline("qa", ask)
110
+
111
+ with Runner(store="runs/qa.db", pools=[pool], concurrency=64) as runner:
112
+ report = runner.run(template.map(dataset_rows)) # a generator, streaming, memory O(concurrency)
113
+ print(report.summary())
114
+ report.export_jsonl("runs/qa.jsonl")
115
+ ```
116
+
117
+ Want pass@k? `template.map(rows, repeats=3)` — the same seed expands into three independent pipelines.
118
+
119
+ ## Lease Safety: The Framework's Hardest Guarantee
120
+
121
+ Resource leasing is the most critical point of interaction between your code and the framework, so:
122
+
123
+ ```python
124
+ async with ctx.acquire(resource) as lease: # ← the only recommended form
125
+ for chunk in chunks: # repeatedly acquiring/releasing inside a loop is fine too
126
+ ...
127
+ ```
128
+
129
+ * An exception, a cancellation, a `timeout_s` timeout — **always returned**;
130
+ * Forgot to release through the escape hatch `await ctx.acquire_lease()`? It is **force-reclaimed** when the
131
+ task ends, and a `lease.leaked` event is recorded;
132
+ * Reclaim is a **pure synchronous function**, and `CancelledError` cannot interrupt it — this is why "a task
133
+ never holds a resource after it ends" holds;
134
+ * When you need something stricter, `strict_leases=True`: a leak immediately fails that task (`LeaseLeakError`).
135
+
136
+ ## Scheduling That Keeps Its Promises
137
+
138
+ * **A retry backoff does not hold a worker.** When the policy wants another attempt, the pipeline is parked in
139
+ a delay queue and the worker immediately picks up other work. `concurrency` therefore means *attempts in
140
+ flight*, not *pipelines sitting out a 30-second backoff*.
141
+ * **A parked pipeline is never mistaken for a dead one.** The end of a run waits for parked pipelines too; a
142
+ stop records them as `interrupted` with their checkpoints intact, so `resume` picks them up.
143
+ * **Pool wakeups are targeted.** Releasing one resource wakes only the waiters whose selector can use it,
144
+ instead of every blocked pipeline.
145
+ * **History is batched, checkpoints are not.** Attempts and events are written in batches (size, interval,
146
+ heartbeat, and end-of-run), while artifacts and checkpoints are always committed immediately. `SIGKILL` can
147
+ cost you the last batch of history, never a checkpoint. `--no-write-behind` opts out.
148
+ * **Waiting is measured.** Pool stats report `waits_total`, `wait_ms_avg`, `p50`, `p95` and `max`, and a wait
149
+ beyond `slow_wait_ms` emits an `acquire.slow_wait` event you can alert on.
150
+
151
+ ## Semantic Recovery
152
+
153
+ Every successful task persists its artifact and advances the checkpoint. On resume:
154
+
155
+ ```python
156
+ runner.run(template.map(rows), resume=True) # or pyattacker resume -c config.yaml
157
+ ```
158
+
159
+ * Already successful pipelines → skipped outright;
160
+ * Failed pipelines → continue from **the first task that produced no artifact**: **if task C died, only task C
161
+ reruns, and task B's request is not re-sent**;
162
+ * The seed artifact is persisted too → recovery **does not depend on the original dataset file**;
163
+ * Changed a task's source code (`spec_digest` includes source digests) → treated as a new pipeline, so old
164
+ results are not incorrectly reused.
165
+
166
+ ## Running It Across Processes (Sharding)
167
+
168
+ SQLite takes one writer and the kernel is a single event loop, so scale means **processes with their own
169
+ stores**, joined afterwards. A pipeline's shard comes from its content-addressed key, so the same dataset always
170
+ splits the same way and `--resume` puts every pipeline back where it was:
171
+
172
+ ```bash
173
+ # convenience: N children, then a merged report
174
+ uv run pyattacker run -c examples/qa_eval.yaml --shards 4 --jobs 4 --store runs/qa.db
175
+ # -> runs/qa.shard0of4.db … runs/qa.shard3of4.db
176
+
177
+ # or drive each shard yourself (cluster, scheduler, four terminals)
178
+ uv run pyattacker run -c examples/qa_eval.yaml --shard 0/4 --store runs/qa.shard0.db
179
+ uv run pyattacker resume -c examples/qa_eval.yaml --shard 0/4 --store runs/qa.shard0.db
180
+
181
+ # one coherent answer out of N files (de-duplicated, statistics recomputed)
182
+ uv run pyattacker report runs/qa.shard*of4.db
183
+ uv run pyattacker export runs/qa.shard*of4.db runs/all.jsonl
184
+ uv run pyattacker export runs/qa.shard*of4.db runs/tasks.csv --rows tasks --format csv
185
+ ```
186
+
187
+ `--rows` picks the shape: `pipelines` (nested, default), `tasks`, `attempts` (including each retry `decision`),
188
+ `events`, `artifacts`. `--format` picks `jsonl`, `json` or `csv`.
189
+
190
+ ## Declarative (Simple Tasks)
191
+
192
+ The YAML describes **composition and resources**; the logic stays in Python (`use: my_pkg.tasks:ask`).
193
+
194
+ ```yaml
195
+ run: { store: runs/demo.db, concurrency: 8, label: demo }
196
+ pools:
197
+ apis:
198
+ kind: llm
199
+ algorithm: backoff
200
+ resources: [ { id: api-1, capacity: 4, options: { model: gpt-4o } } ]
201
+ pipeline:
202
+ name: qa
203
+ tasks:
204
+ - { use: pyattacker.tasks:echo }
205
+ - { use: pyattacker.tasks:simulate_llm, resource: apis, algorithm: backoff,
206
+ retry: { max_attempts: 3, on: [RetryableError, TimeoutError] } }
207
+ source: { kind: range, n: 100 }
208
+ ```
209
+
210
+ ```bash
211
+ uv run pyattacker validate -c examples/qa_eval.yaml
212
+ uv run pyattacker run -c examples/qa_eval.yaml --progress
213
+ uv run pyattacker watch runs/demo.db # open another process to monitor it live
214
+ uv run pyattacker report runs/demo.db --errors 20
215
+ uv run pyattacker export runs/demo.db out.jsonl
216
+ uv run pyattacker serve runs/demo.db # HTTP dashboard + JSON endpoints
217
+ uv run pyattacker plugins # installed plugins
218
+ ```
219
+
220
+ Exit codes: `0` all succeeded / `1` some failed / `2` config error / `130` interrupted.
221
+ Every flag of every subcommand: [`docs/cli.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/cli.md).
222
+
223
+ ## Records and Monitoring
224
+
225
+ The complete story of one pipeline = five tables queried by `pipeline_id`: state and checkpoint, the final
226
+ state of each task, the full attempt history (including **every retry decision**
227
+ `{retry, reason, delay_s, error_class}`), intermediate and final artifacts, and the structured event stream.
228
+ `pyattacker report/watch` consumes these facts directly.
229
+
230
+ ## Extending It
231
+
232
+ **Plugins** are ordinary `importlib.metadata` entry points — install a package and its names become
233
+ usable from any config:
234
+
235
+ ```toml
236
+ [project.entry-points."pyattacker.tasks"]
237
+ my_judge = "my_pkg.tasks:my_judge" # a TaskSpec, or a factory returning one
238
+ [project.entry-points."pyattacker.algorithms"]
239
+ my_algo = "my_pkg.algo:MyAlgorithm"
240
+ [project.entry-points."pyattacker.stores"]
241
+ s3 = "my_pkg.s3:open_store" # keyed by URI scheme: store = "s3://bucket/runs.db"
242
+ ```
243
+
244
+ ```bash
245
+ uv run pyattacker plugins # what is installed, and what failed to load
246
+ ```
247
+
248
+ Built-ins resolve first (a plugin cannot shadow `echo`), and a plugin that raises on import is
249
+ recorded rather than fatal. A complete worked example: [`examples/plugin_package/`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/plugin_package/README.md).
250
+
251
+ **Large payloads** can live outside the database:
252
+
253
+ ```bash
254
+ uv run pyattacker run -c examples/qa_eval.yaml --artifact-backend file:///data/blobs
255
+ # or, in the config: artifact_backend = { kind = "file", root = "/data/blobs", min_bytes = 262144 }
256
+ ```
257
+
258
+ Files are content-addressed, written atomically, and hydrated back on read — so a resumed run
259
+ reuses spilled checkpoints transparently. `null` keeps digests and drops bytes; `inline` (default)
260
+ keeps everything in the store.
261
+
262
+ **A zero-dependency monitoring endpoint**:
263
+
264
+ ```bash
265
+ uv run pyattacker serve runs/qa.db # http://127.0.0.1:8787
266
+ # / dashboard /stats /events /pipelines /resources /errors (JSON)
267
+ ```
268
+
269
+ It opens a fresh read-only connection per request, so it runs happily beside a live run. It has **no
270
+ authentication** and binds to loopback: it exposes your payloads, so treat it as a debug view and do not
271
+ put it on a public interface without your own proxy in front.
272
+
273
+ **Branching inside a step** — `fanout(a, b)` runs several tasks on the same input concurrently and
274
+ returns `{task_name: value}`. Retry granularity becomes the group, which is the honest price of not
275
+ turning pipelines into a DAG. The group is the only spec the Runner sees, so `resource`, `algorithm` and
276
+ `timeout_s` are inherited from the children when all of them agree (a `timeout_s` then bounds the whole
277
+ group).
278
+
279
+ ## Documentation
280
+
281
+ | Document | What is in it |
282
+ |---|---|
283
+ | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | fourteen runnable steps, from "one task" to a sharded evaluation; each one executed by the test suite |
284
+ | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples |
285
+ | [`docs/cli.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/cli.md) | every subcommand, every flag, exit codes, config reference |
286
+ | [`docs/design.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md) | conceptual model, the six invariants, the lease contract, data model, tradeoffs |
287
+ | [`CHANGELOG.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md) | what changed, release by release |
288
+
289
+ ## Examples
290
+
291
+ | Example | What it shows |
292
+ |---|---|
293
+ | [`examples/quickstart.py`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/quickstart.py) | the SDK in 60 lines: a custom client factory, retries, resume |
294
+ | [`examples/llm_eval/`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/llm_eval/README.md) | a complete evaluation — prepare → 2-turn model call → 3 judges → reduce, in **two pipeline shapes**, with the checkpoint-granularity tradeoff *measured* (grouped re-sent 2 judge requests that had already succeeded; split re-sent 0) |
295
+ | [`examples/sharded.py`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/sharded.py) | one dataset across N stores, then a merged report |
296
+ | [`examples/plugin_package/`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/plugin_package/README.md) | a real installable plugin: tasks, an algorithm, a codec |
297
+ | [`examples/qa_eval.yaml`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/qa_eval.yaml) | the declarative path, end to end |
298
+
299
+ ```bash
300
+ uv run python examples/quickstart.py
301
+ uv run python -m examples.llm_eval.demo
302
+ uv run python examples/sharded.py
303
+ uv run pyattacker run -c examples/qa_eval.yaml --limit 40
304
+ ```
305
+
306
+ ## Out of Scope
307
+
308
+ These are design decisions, not missing features:
309
+
310
+ * **Network requests** — you write the openai/anthropic protocols yourself. The kernel never opens a socket.
311
+ * **Semantic reduction** — accuracy, pass@k, F1 and any cross-pipeline aggregation. Export the artifacts and
312
+ compute it outside, or write a sink pipeline out of the primitives.
313
+ * **DAG orchestration** — a pipeline is a linear chain; branch inside a task with `fanout`.
314
+ * **A serving gateway** — the only HTTP surface is the read-only debug endpoint above.
315
+ * **Distributed scheduling** — scale out with `--shard`; multi-process is the ceiling.
316
+
317
+ Sections 1 and 11 of [`docs/design.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md) state the boundary precisely, and section 8 lists
318
+ every known tradeoff with its reason.
319
+
320
+ ## Status
321
+
322
+ **0.1.0 — the first release.** Everything planned for it is implemented (M0–M4: the kernel, persistence and
323
+ task-level recovery, retries and error classification, the resource pool with 7 acquisition algorithms,
324
+ delayed continuations and write-behind batching, sharding and merged reports, five export shapes in three
325
+ formats, entry-point plugins, external artifact backends, the fan-out helper, and the HTTP monitoring
326
+ endpoint). The API is young: it follows semantic versioning from here, but expect refinement before 1.0.
327
+
328
+ Left for later: a distributed scheduler, Parquet export, blob garbage collection, and first-class
329
+ `Parallel`/`Gather` nodes.
330
+
331
+ ## Development
332
+
333
+ ```bash
334
+ uv sync # create the venv + install dependencies (the only core dependency is pyyaml)
335
+ uv run pytest # the whole suite: zero network, a few seconds
336
+ uv run ruff check # lint (configuration lives in pyproject.toml, with reasons for each exception)
337
+ uv run pyattacker demo # end-to-end smoke test
338
+ uv build # sdist + wheel
339
+ ```
340
+
341
+ Tests are offline and deterministic (time goes through an injectable `Clock`). The tutorial's code blocks are
342
+ extracted and executed by `tests/test_tutorial.py`, so documentation that rots fails CI.
343
+
344
+ ## License
345
+
346
+ MIT — see [LICENSE](https://github.com/Hazer-BJTU/pyattacker/blob/main/LICENSE).
347
+