loadpilot 0.1.3__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.
@@ -0,0 +1 @@
1
+ TARGET_URL=http://localhost:8000
@@ -0,0 +1,39 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .venv/
9
+ venv/
10
+ .env
11
+
12
+ # pytest / coverage
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+
17
+ # Rust
18
+ engine/target/
19
+ **/*.rs.bk
20
+
21
+ # Cargo.lock — library crates omit it; binary crates keep it.
22
+ # LoadPilot ships binaries, so Cargo.lock is committed.
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+ .DS_Store
30
+
31
+ # Bundled binaries (added by hatch_build.py — never commit)
32
+ cli/loadpilot/coordinator
33
+ cli/loadpilot/coordinator.exe
34
+ cli/loadpilot/agent
35
+ cli/loadpilot/agent.exe
36
+
37
+ # LoadPilot generated outputs
38
+ report*.html
39
+ *.html
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: loadpilot
3
+ Version: 0.1.3
4
+ Summary: Write load tests in Python. Run them at Rust speed.
5
+ License: MIT
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: httpx
8
+ Requires-Dist: pydantic
9
+ Requires-Dist: questionary
10
+ Requires-Dist: rich
11
+ Requires-Dist: typer
12
+ Provides-Extra: dev
13
+ Requires-Dist: anyio[trio]; extra == 'dev'
14
+ Requires-Dist: coverage>=7; extra == 'dev'
15
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Requires-Dist: ruff>=0.9; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # LoadPilot
21
+
22
+ [![CI](https://github.com/VladislavAkulich/loadpilot/actions/workflows/ci.yml/badge.svg)](https://github.com/VladislavAkulich/loadpilot/actions)
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
24
+
25
+ > Write load tests in Python. Run them at Rust speed.
26
+
27
+ ```
28
+ LoadPilot — CheckoutFlow [01:42] steady: 100/100 RPS
29
+
30
+ Requests/sec: 100.0 Total: 9,812
31
+ Errors: 0.0% Failed: 0
32
+
33
+ Latency:
34
+ p50: 12ms
35
+ p95: 28ms
36
+ p99: 41ms
37
+ max: 203ms
38
+
39
+ [████████████████████] 100%
40
+ ```
41
+
42
+ LoadPilot is a load testing tool for teams that want to write scenarios in Python
43
+ without the throughput ceiling of a pure-Python engine.
44
+
45
+ Scenarios are plain Python classes. HTTP execution runs in async Rust.
46
+
47
+ ---
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install loadpilot
53
+ ```
54
+
55
+ No Rust required — the coordinator binary is bundled in the wheel.
56
+
57
+ **Supported platforms:** Linux x86_64 · macOS Intel · macOS Apple Silicon · Windows x86_64
58
+
59
+ ---
60
+
61
+ ## Quick Start
62
+
63
+ ```bash
64
+ # scaffold a project
65
+ loadpilot init my-load-tests
66
+ cd my-load-tests
67
+
68
+ # run a scenario
69
+ loadpilot run scenarios/example.py --target https://your-api.example.com
70
+ ```
71
+
72
+ Or run without arguments to pick a scenario interactively:
73
+
74
+ ```bash
75
+ loadpilot run --target https://your-api.example.com
76
+ ```
77
+
78
+ → [Getting Started in 5 minutes](docs/getting-started.md)
79
+
80
+ ---
81
+
82
+ ## Write a Scenario
83
+
84
+ ```python
85
+ from loadpilot import VUser, scenario, task, LoadClient
86
+
87
+ @scenario(rps=100, duration="2m", ramp_up="30s")
88
+ class CheckoutFlow(VUser):
89
+
90
+ def on_start(self, client: LoadClient):
91
+ resp = client.post("/auth/login", json={"user": "test", "pass": "secret"})
92
+ self.token = resp.json()["access_token"]
93
+
94
+ @task(weight=5)
95
+ def browse(self, client: LoadClient):
96
+ client.get("/api/products", headers={"Authorization": f"Bearer {self.token}"})
97
+
98
+ def check_browse(self, status_code: int, body) -> None:
99
+ assert status_code == 200
100
+ assert isinstance(body, list)
101
+
102
+ @task(weight=1)
103
+ def purchase(self, client: LoadClient):
104
+ client.post("/api/orders", json={"product_id": 42, "qty": 1},
105
+ headers={"Authorization": f"Bearer {self.token}"})
106
+ ```
107
+
108
+ - `on_start` — runs once per virtual user before tasks start (login, setup)
109
+ - `@task` — weighted HTTP task; multiple tasks run in proportion
110
+ - `check_{task}` — assertion after each request; fail = error counted
111
+
112
+ ---
113
+
114
+ ## Features
115
+
116
+ | | |
117
+ |---|---|
118
+ | **Python DSL** | `@scenario`, `@task`, `on_start`, `check_*` — no new syntax to learn |
119
+ | **Load profiles** | ramp (default), constant, step, spike |
120
+ | **SLA thresholds** | fail CI with exit code 1 on p99/p95/error-rate breach |
121
+ | **Distributed mode** | coordinator + N agents over NATS; local or remote, free |
122
+ | **HTML report** | self-contained, open in any browser, no server required |
123
+ | **Prometheus metrics** | live Grafana dashboards while the test runs |
124
+ | **Interactive TUI** | `loadpilot run` with no args opens a scenario browser |
125
+ | **`pip install`** | coordinator binary bundled in the wheel, no Rust needed |
126
+
127
+ ---
128
+
129
+ ## Performance
130
+
131
+ LoadPilot is benchmarked against k6 and Locust against a Rust/axum echo server in Docker.
132
+ LoadPilot runs in **PyO3 mode** — with `on_start` (login) and `check_*` (assertion per task) — a realistic workload with Python callbacks active.
133
+
134
+ ### Precision — 500 RPS target
135
+
136
+ | Tool | RPS actual | p50 | p99 | Errors | CPU avg |
137
+ |------|-----------|-----|-----|--------|---------|
138
+ | **LoadPilot (PyO3)** | **478** | 4ms | 15ms | 0% | **14%** |
139
+ | k6 | 491 | 8ms | 118ms | 0% | 129% |
140
+ | Locust | 498 | 150ms | 1500ms | 0% | 88% |
141
+
142
+ LoadPilot uses **9× less CPU** than k6 at the same load. Locust reaches the RPS target but its Python/GIL scheduler adds significant latency (p99 ≥ 1500ms at only 500 RPS).
143
+
144
+ ### Max throughput — 30s, no cap
145
+
146
+ | Tool | RPS | p50 | p99 | Errors | CPU avg |
147
+ |------|-----|-----|-----|--------|---------|
148
+ | **LoadPilot (PyO3)** | **2205** | 11ms | 38ms | 0% | **165%** |
149
+ | k6 | 1799 | 14ms | 175ms | 0% | 212% |
150
+ | Locust | 677 | 100ms | 170ms | 0% | 117% |
151
+
152
+ **1.2× k6** and **3.3× Locust** at max throughput. **1.6× better CPU efficiency per request** vs k6.
153
+
154
+ → [Full benchmark details and methodology](docs/benchmark.md)
155
+
156
+ ---
157
+
158
+ ## SLA Thresholds
159
+
160
+ ```python
161
+ @scenario(
162
+ rps=100, duration="2m",
163
+ thresholds={"p99_ms": 500, "p95_ms": 300, "error_rate": 1.0},
164
+ )
165
+ class CheckoutFlow(VUser): ...
166
+ ```
167
+
168
+ ```
169
+ Thresholds
170
+ ✓ p99 latency 41ms < 500ms
171
+ ✓ p95 latency 28ms < 300ms
172
+ ✓ error rate 0% < 1%
173
+
174
+ All thresholds passed.
175
+ ```
176
+
177
+ Exit code `1` on breach — drop into any CI pipeline. Override without editing the file:
178
+
179
+ ```bash
180
+ loadpilot run scenarios/checkout.py --target https://staging.example.com \
181
+ --threshold p99_ms=800 --threshold error_rate=2
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Distributed Mode
187
+
188
+ ```bash
189
+ # 4 local processes
190
+ loadpilot run scenarios/checkout.py --target https://api.example.com --agents 4
191
+
192
+ # external agents on remote machines
193
+ loadpilot run scenarios/checkout.py \
194
+ --target https://api.example.com \
195
+ --external-agents 4
196
+ ```
197
+
198
+ The CLI output is identical to single-machine mode — the coordinator aggregates
199
+ everything transparently. `on_start` (login, setup) runs on the coordinator and
200
+ ships per-user auth headers to agents, so distributed tests can authenticate
201
+ without Python running on each agent.
202
+
203
+ → [Distributed mode guide](docs/distributed.md)
204
+
205
+ ---
206
+
207
+ ## Live Monitoring
208
+
209
+ ```bash
210
+ # scaffolded by `loadpilot init`
211
+ docker compose -f monitoring/docker-compose.yml up -d
212
+ # Grafana → http://localhost:3000 (LoadPilot dashboard auto-imported)
213
+ # Prometheus → http://localhost:9091
214
+ ```
215
+
216
+ Use Grafana to correlate load test metrics with your service's own metrics
217
+ (CPU, DB latency, error rates) on the same timeline.
218
+
219
+ ---
220
+
221
+ ## Documentation
222
+
223
+ - [Getting Started](docs/getting-started.md)
224
+ - [DSL Reference](docs/dsl-reference.md) — `@scenario`, `@task`, `on_start`, `check_*`, `client.batch()`
225
+ - [CLI Reference](docs/cli-reference.md) — all flags for `loadpilot run` and `loadpilot init`
226
+ - [Distributed Mode](docs/distributed.md) — local agents, remote agents, Railway, NATS
227
+ - [Benchmark](docs/benchmark.md) — methodology, full results, how to reproduce
228
+ - [Architecture](docs/architecture.md) — how the Python DSL and Rust engine interact
229
+ - [Development](docs/development.md) — building from source, running tests
230
+
231
+ ---
232
+
233
+ ## License
234
+
235
+ MIT
@@ -0,0 +1,216 @@
1
+ # LoadPilot
2
+
3
+ [![CI](https://github.com/VladislavAkulich/loadpilot/actions/workflows/ci.yml/badge.svg)](https://github.com/VladislavAkulich/loadpilot/actions)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+
6
+ > Write load tests in Python. Run them at Rust speed.
7
+
8
+ ```
9
+ LoadPilot — CheckoutFlow [01:42] steady: 100/100 RPS
10
+
11
+ Requests/sec: 100.0 Total: 9,812
12
+ Errors: 0.0% Failed: 0
13
+
14
+ Latency:
15
+ p50: 12ms
16
+ p95: 28ms
17
+ p99: 41ms
18
+ max: 203ms
19
+
20
+ [████████████████████] 100%
21
+ ```
22
+
23
+ LoadPilot is a load testing tool for teams that want to write scenarios in Python
24
+ without the throughput ceiling of a pure-Python engine.
25
+
26
+ Scenarios are plain Python classes. HTTP execution runs in async Rust.
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install loadpilot
34
+ ```
35
+
36
+ No Rust required — the coordinator binary is bundled in the wheel.
37
+
38
+ **Supported platforms:** Linux x86_64 · macOS Intel · macOS Apple Silicon · Windows x86_64
39
+
40
+ ---
41
+
42
+ ## Quick Start
43
+
44
+ ```bash
45
+ # scaffold a project
46
+ loadpilot init my-load-tests
47
+ cd my-load-tests
48
+
49
+ # run a scenario
50
+ loadpilot run scenarios/example.py --target https://your-api.example.com
51
+ ```
52
+
53
+ Or run without arguments to pick a scenario interactively:
54
+
55
+ ```bash
56
+ loadpilot run --target https://your-api.example.com
57
+ ```
58
+
59
+ → [Getting Started in 5 minutes](docs/getting-started.md)
60
+
61
+ ---
62
+
63
+ ## Write a Scenario
64
+
65
+ ```python
66
+ from loadpilot import VUser, scenario, task, LoadClient
67
+
68
+ @scenario(rps=100, duration="2m", ramp_up="30s")
69
+ class CheckoutFlow(VUser):
70
+
71
+ def on_start(self, client: LoadClient):
72
+ resp = client.post("/auth/login", json={"user": "test", "pass": "secret"})
73
+ self.token = resp.json()["access_token"]
74
+
75
+ @task(weight=5)
76
+ def browse(self, client: LoadClient):
77
+ client.get("/api/products", headers={"Authorization": f"Bearer {self.token}"})
78
+
79
+ def check_browse(self, status_code: int, body) -> None:
80
+ assert status_code == 200
81
+ assert isinstance(body, list)
82
+
83
+ @task(weight=1)
84
+ def purchase(self, client: LoadClient):
85
+ client.post("/api/orders", json={"product_id": 42, "qty": 1},
86
+ headers={"Authorization": f"Bearer {self.token}"})
87
+ ```
88
+
89
+ - `on_start` — runs once per virtual user before tasks start (login, setup)
90
+ - `@task` — weighted HTTP task; multiple tasks run in proportion
91
+ - `check_{task}` — assertion after each request; fail = error counted
92
+
93
+ ---
94
+
95
+ ## Features
96
+
97
+ | | |
98
+ |---|---|
99
+ | **Python DSL** | `@scenario`, `@task`, `on_start`, `check_*` — no new syntax to learn |
100
+ | **Load profiles** | ramp (default), constant, step, spike |
101
+ | **SLA thresholds** | fail CI with exit code 1 on p99/p95/error-rate breach |
102
+ | **Distributed mode** | coordinator + N agents over NATS; local or remote, free |
103
+ | **HTML report** | self-contained, open in any browser, no server required |
104
+ | **Prometheus metrics** | live Grafana dashboards while the test runs |
105
+ | **Interactive TUI** | `loadpilot run` with no args opens a scenario browser |
106
+ | **`pip install`** | coordinator binary bundled in the wheel, no Rust needed |
107
+
108
+ ---
109
+
110
+ ## Performance
111
+
112
+ LoadPilot is benchmarked against k6 and Locust against a Rust/axum echo server in Docker.
113
+ LoadPilot runs in **PyO3 mode** — with `on_start` (login) and `check_*` (assertion per task) — a realistic workload with Python callbacks active.
114
+
115
+ ### Precision — 500 RPS target
116
+
117
+ | Tool | RPS actual | p50 | p99 | Errors | CPU avg |
118
+ |------|-----------|-----|-----|--------|---------|
119
+ | **LoadPilot (PyO3)** | **478** | 4ms | 15ms | 0% | **14%** |
120
+ | k6 | 491 | 8ms | 118ms | 0% | 129% |
121
+ | Locust | 498 | 150ms | 1500ms | 0% | 88% |
122
+
123
+ LoadPilot uses **9× less CPU** than k6 at the same load. Locust reaches the RPS target but its Python/GIL scheduler adds significant latency (p99 ≥ 1500ms at only 500 RPS).
124
+
125
+ ### Max throughput — 30s, no cap
126
+
127
+ | Tool | RPS | p50 | p99 | Errors | CPU avg |
128
+ |------|-----|-----|-----|--------|---------|
129
+ | **LoadPilot (PyO3)** | **2205** | 11ms | 38ms | 0% | **165%** |
130
+ | k6 | 1799 | 14ms | 175ms | 0% | 212% |
131
+ | Locust | 677 | 100ms | 170ms | 0% | 117% |
132
+
133
+ **1.2× k6** and **3.3× Locust** at max throughput. **1.6× better CPU efficiency per request** vs k6.
134
+
135
+ → [Full benchmark details and methodology](docs/benchmark.md)
136
+
137
+ ---
138
+
139
+ ## SLA Thresholds
140
+
141
+ ```python
142
+ @scenario(
143
+ rps=100, duration="2m",
144
+ thresholds={"p99_ms": 500, "p95_ms": 300, "error_rate": 1.0},
145
+ )
146
+ class CheckoutFlow(VUser): ...
147
+ ```
148
+
149
+ ```
150
+ Thresholds
151
+ ✓ p99 latency 41ms < 500ms
152
+ ✓ p95 latency 28ms < 300ms
153
+ ✓ error rate 0% < 1%
154
+
155
+ All thresholds passed.
156
+ ```
157
+
158
+ Exit code `1` on breach — drop into any CI pipeline. Override without editing the file:
159
+
160
+ ```bash
161
+ loadpilot run scenarios/checkout.py --target https://staging.example.com \
162
+ --threshold p99_ms=800 --threshold error_rate=2
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Distributed Mode
168
+
169
+ ```bash
170
+ # 4 local processes
171
+ loadpilot run scenarios/checkout.py --target https://api.example.com --agents 4
172
+
173
+ # external agents on remote machines
174
+ loadpilot run scenarios/checkout.py \
175
+ --target https://api.example.com \
176
+ --external-agents 4
177
+ ```
178
+
179
+ The CLI output is identical to single-machine mode — the coordinator aggregates
180
+ everything transparently. `on_start` (login, setup) runs on the coordinator and
181
+ ships per-user auth headers to agents, so distributed tests can authenticate
182
+ without Python running on each agent.
183
+
184
+ → [Distributed mode guide](docs/distributed.md)
185
+
186
+ ---
187
+
188
+ ## Live Monitoring
189
+
190
+ ```bash
191
+ # scaffolded by `loadpilot init`
192
+ docker compose -f monitoring/docker-compose.yml up -d
193
+ # Grafana → http://localhost:3000 (LoadPilot dashboard auto-imported)
194
+ # Prometheus → http://localhost:9091
195
+ ```
196
+
197
+ Use Grafana to correlate load test metrics with your service's own metrics
198
+ (CPU, DB latency, error rates) on the same timeline.
199
+
200
+ ---
201
+
202
+ ## Documentation
203
+
204
+ - [Getting Started](docs/getting-started.md)
205
+ - [DSL Reference](docs/dsl-reference.md) — `@scenario`, `@task`, `on_start`, `check_*`, `client.batch()`
206
+ - [CLI Reference](docs/cli-reference.md) — all flags for `loadpilot run` and `loadpilot init`
207
+ - [Distributed Mode](docs/distributed.md) — local agents, remote agents, Railway, NATS
208
+ - [Benchmark](docs/benchmark.md) — methodology, full results, how to reproduce
209
+ - [Architecture](docs/architecture.md) — how the Python DSL and Rust engine interact
210
+ - [Development](docs/development.md) — building from source, running tests
211
+
212
+ ---
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1,73 @@
1
+ """Hatchling build hook — compiles the Rust coordinator and bundles it into the wheel.
2
+
3
+ During `pip install` from source (or `hatch build` in CI) this hook:
4
+ 1. Runs `cargo build --release` for the coordinator crate.
5
+ 2. Copies the resulting binary into `loadpilot/` so it is packaged as data.
6
+ 3. Tells hatchling that the wheel is platform-specific (not pure Python).
7
+
8
+ When installing a pre-built wheel from PyPI the hook is NOT invoked — the binary
9
+ is already present inside the wheel (baked in by CI).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
21
+
22
+ # Path from cli/ (where pyproject.toml lives) up to the engine workspace root.
23
+ _ENGINE_DIR = Path(__file__).parent.parent / "engine"
24
+ _PKG_DIR = Path(__file__).parent / "loadpilot"
25
+
26
+
27
+ def _binary_name() -> str:
28
+ return "coordinator.exe" if sys.platform == "win32" else "coordinator"
29
+
30
+
31
+ class CustomBuildHook(BuildHookInterface):
32
+ """Hatchling hook: build + bundle Rust coordinator binary."""
33
+
34
+ PLUGIN_NAME = "custom"
35
+
36
+ def initialize(self, version: str, build_data: dict) -> None:
37
+ if self.target_name not in ("wheel", "editable"):
38
+ return
39
+
40
+ binary_name = _binary_name()
41
+ dst = _PKG_DIR / binary_name
42
+
43
+ # In CI the binary may already be built and copied by the workflow.
44
+ # Skip compilation if it's already there and SKIP_CARGO env var is set.
45
+ if dst.exists() and os.environ.get("LOADPILOT_SKIP_CARGO"):
46
+ self.app.display_info(f"[loadpilot] Skipping cargo build — {dst} already exists.")
47
+ else:
48
+ self.app.display_info("[loadpilot] Building Rust coordinator (cargo build --release)…")
49
+ env = os.environ.copy()
50
+ # PyO3 0.23 officially supports up to Python 3.13.
51
+ # This flag enables forward-compatibility with newer Python versions
52
+ # via the stable ABI — safe for binary-only use.
53
+ env.setdefault("PYO3_USE_ABI3_FORWARD_COMPATIBILITY", "1")
54
+ subprocess.run(
55
+ ["cargo", "build", "--release", "--package", "coordinator", "--package", "agent"],
56
+ cwd=str(_ENGINE_DIR),
57
+ env=env,
58
+ check=True,
59
+ )
60
+ for name in [binary_name, "agent" + ("" if sys.platform != "win32" else ".exe")]:
61
+ src = _ENGINE_DIR / "target" / "release" / name
62
+ dst_bin = _PKG_DIR / name
63
+ shutil.copy2(src, dst_bin)
64
+ dst_bin.chmod(dst_bin.stat().st_mode | 0o111)
65
+ self.app.display_info(f"[loadpilot] Bundled {name} → {dst_bin}")
66
+
67
+ # Tell hatchling to include the binary in the wheel as package data.
68
+ build_data["shared_data"] = {}
69
+ build_data["artifacts"] = [str(dst)]
70
+
71
+ # Mark the wheel as platform-specific so pip picks the right one.
72
+ build_data["pure_python"] = False
73
+ build_data["infer_tag"] = True
@@ -0,0 +1,4 @@
1
+ from loadpilot.client import LoadClient
2
+ from loadpilot.dsl import ScenarioDef, TaskDef, VUser, _scenarios, scenario, task
3
+
4
+ __all__ = ["scenario", "task", "VUser", "LoadClient", "_scenarios", "ScenarioDef", "TaskDef"]