shadowbox 0.3.0__py3-none-any.whl
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.
- shadowbox/__init__.py +3 -0
- shadowbox/api.py +136 -0
- shadowbox/cards.py +59 -0
- shadowbox/cli.py +222 -0
- shadowbox/compare.py +103 -0
- shadowbox/data/__init__.py +1 -0
- shadowbox/data/cards/cache-poison.yaml +7 -0
- shadowbox/data/cards/db-down.yaml +7 -0
- shadowbox/data/cards/latency-500ms.yaml +8 -0
- shadowbox/data/cards/queue-overflow.yaml +6 -0
- shadowbox/data/cards/slow-dependency.yaml +8 -0
- shadowbox/data/cards/traffic-10x.yaml +6 -0
- shadowbox/data/cards/zone-loss.yaml +8 -0
- shadowbox/data/example/docker-compose.yaml +13 -0
- shadowbox/data/example/model.yaml +33 -0
- shadowbox/data/example/scenarios/db-failure.yaml +12 -0
- shadowbox/dsl.py +94 -0
- shadowbox/engine.py +220 -0
- shadowbox/errors.py +38 -0
- shadowbox/importers/__init__.py +3 -0
- shadowbox/importers/compose.py +109 -0
- shadowbox/metrics.py +49 -0
- shadowbox/model.py +74 -0
- shadowbox/report.py +48 -0
- shadowbox/store.py +85 -0
- shadowbox-0.3.0.dist-info/METADATA +130 -0
- shadowbox-0.3.0.dist-info/RECORD +29 -0
- shadowbox-0.3.0.dist-info/WHEEL +4 -0
- shadowbox-0.3.0.dist-info/entry_points.txt +2 -0
shadowbox/store.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""SQLite persistence for models, simulations, and reports (M4a local server).
|
|
2
|
+
|
|
3
|
+
Cloud deploy (M4b) swaps this module for a D1-backed store with the same
|
|
4
|
+
function signatures; the API layer does not change.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sqlite3
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
_SCHEMA = """
|
|
14
|
+
CREATE TABLE IF NOT EXISTS models (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
body TEXT NOT NULL
|
|
17
|
+
);
|
|
18
|
+
CREATE TABLE IF NOT EXISTS simulations (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
model_id TEXT NOT NULL REFERENCES models(id),
|
|
21
|
+
scenario TEXT NOT NULL,
|
|
22
|
+
seed INTEGER NOT NULL,
|
|
23
|
+
status TEXT NOT NULL,
|
|
24
|
+
report TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _connect(path: Path) -> sqlite3.Connection:
|
|
30
|
+
conn = sqlite3.connect(path)
|
|
31
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
32
|
+
conn.executescript(_SCHEMA)
|
|
33
|
+
return conn
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Store:
|
|
37
|
+
"""File-backed store; one connection per call keeps the API thread-safe."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, path: Path) -> None:
|
|
40
|
+
self._path = path # file created lazily on first use, never on import
|
|
41
|
+
|
|
42
|
+
def _new_id(self) -> str:
|
|
43
|
+
return uuid.uuid4().hex[:16]
|
|
44
|
+
|
|
45
|
+
def save_model(self, body: dict[str, Any]) -> str:
|
|
46
|
+
model_id = self._new_id()
|
|
47
|
+
with _connect(self._path) as conn:
|
|
48
|
+
conn.execute(
|
|
49
|
+
"INSERT INTO models (id, body) VALUES (?, ?)", (model_id, json.dumps(body))
|
|
50
|
+
)
|
|
51
|
+
return model_id
|
|
52
|
+
|
|
53
|
+
def get_model(self, model_id: str) -> dict[str, Any] | None:
|
|
54
|
+
with _connect(self._path) as conn:
|
|
55
|
+
row = conn.execute("SELECT body FROM models WHERE id = ?", (model_id,)).fetchone()
|
|
56
|
+
return json.loads(row[0]) if row else None
|
|
57
|
+
|
|
58
|
+
def save_simulation(
|
|
59
|
+
self, model_id: str, scenario: dict[str, Any], seed: int, report: dict[str, Any]
|
|
60
|
+
) -> str:
|
|
61
|
+
sim_id = self._new_id()
|
|
62
|
+
with _connect(self._path) as conn:
|
|
63
|
+
conn.execute(
|
|
64
|
+
"INSERT INTO simulations (id, model_id, scenario, seed, status, report)"
|
|
65
|
+
" VALUES (?, ?, ?, ?, 'completed', ?)",
|
|
66
|
+
(sim_id, model_id, json.dumps(scenario), seed, json.dumps(report)),
|
|
67
|
+
)
|
|
68
|
+
return sim_id
|
|
69
|
+
|
|
70
|
+
def get_simulation(self, sim_id: str) -> dict[str, Any] | None:
|
|
71
|
+
with _connect(self._path) as conn:
|
|
72
|
+
row = conn.execute(
|
|
73
|
+
"SELECT model_id, scenario, seed, status, report FROM simulations WHERE id = ?",
|
|
74
|
+
(sim_id,),
|
|
75
|
+
).fetchone()
|
|
76
|
+
if row is None:
|
|
77
|
+
return None
|
|
78
|
+
return {
|
|
79
|
+
"id": sim_id,
|
|
80
|
+
"model_id": row[0],
|
|
81
|
+
"scenario": json.loads(row[1]),
|
|
82
|
+
"seed": row[2],
|
|
83
|
+
"status": row[3],
|
|
84
|
+
"report": json.loads(row[4]),
|
|
85
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: shadowbox
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Executable architectural model for safe what-if experimentation (M0: contracts + validate)
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: fastapi>=0.115
|
|
8
|
+
Requires-Dist: jsonschema>=4.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: typer>=0.9
|
|
13
|
+
Requires-Dist: uvicorn>=0.30
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# ShadowBox
|
|
17
|
+
|
|
18
|
+
Executable architectural model for safe what-if experimentation. Model the system. Experiment safely.
|
|
19
|
+
|
|
20
|
+
> Scope: headless CLI (validate, simulate, compare, report, import, init, serve) plus local API, static demo, and React Studio. Simulation output is always labeled with assumptions, confidence, and seed — never presented as production measurement.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Python `>=3.13` (pinned via `.python-version`)
|
|
25
|
+
- `uv` for env and runs (no Docker needed)
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
With a clone (development):
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
uv python pin 3.13
|
|
33
|
+
uv venv
|
|
34
|
+
uv sync --group dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Without a clone (use only):
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
uvx --from "shadowbox @ git+https://github.com/Pa004/shadowBox.git@v0.2.0" shadowbox init --out demo
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Run in development
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
uv run shadowbox init --out demo
|
|
47
|
+
uv run shadowbox import --from demo/docker-compose.yaml --out demo/model2.yaml
|
|
48
|
+
uv run shadowbox validate demo/model.yaml --scenario demo/scenarios/db-failure.yaml
|
|
49
|
+
uv run shadowbox simulate demo/model.yaml --scenario demo/scenarios/db-failure.yaml --seed 42 --out report.json
|
|
50
|
+
uv run shadowbox report report.json --format text
|
|
51
|
+
uv run shadowbox compare --a base.json --b report.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Exit codes: `0` valid/pass, `2` scenario regression (compare), `3` invalid input or existing files without `--force` (prints `E_*` code).
|
|
55
|
+
|
|
56
|
+
Import notes: every performance field is an estimated default (see warnings). Calibrate before trusting output.
|
|
57
|
+
|
|
58
|
+
Chaos cards ship in the package (`init` writes them to `cards/`): `db-down`, `cache-poison`, `latency-500ms`, `traffic-10x`, `zone-loss`, `slow-dependency`, `queue-overflow`.
|
|
59
|
+
|
|
60
|
+
## API server (local)
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
uv run uvicorn shadowbox.api:app --port 8000
|
|
64
|
+
# or: uv run shadowbox serve --port 8000
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Endpoints: `POST /api/v1/models`, `GET /api/v1/models/{id}`, `POST /api/v1/simulations?model_id=...`, `GET /api/v1/simulations/{id}[/events|/metrics|/report]`. Events are paginated (`limit` 1..1000, `cursor` offset over the stored 500-request sample). State lives in `shadowbox.db` (git-ignored, created on first use). The API has no authentication: bind to localhost (`serve` defaults to `127.0.0.1`) and never expose it directly to the internet.
|
|
68
|
+
|
|
69
|
+
## Deploy (Cloudflare free tier, no card)
|
|
70
|
+
|
|
71
|
+
Scaffold ready in `wrangler.jsonc` + `schema.sql` + `apps/api/worker.py` + `apps/web/` (static demo, no build step). Remaining steps need your Cloudflare account:
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
!npm install -g wrangler
|
|
75
|
+
!wrangler login
|
|
76
|
+
!wrangler d1 create shadowbox # paste database_id into wrangler.jsonc
|
|
77
|
+
!wrangler d1 execute shadowbox --file schema.sql
|
|
78
|
+
!uvx --from workers-py pywrangler dev # local Worker emulation, no account needed
|
|
79
|
+
!wrangler deploy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Production note: the Worker serves the same FastAPI app; swapping the SQLite file store for the D1 binding is a follow-up task verified against a real account (M4b-full). The static demo deploys to Pages as-is and talks to any API base URL.
|
|
83
|
+
|
|
84
|
+
Open `apps/web/index.html` after `Run` to scrub virtual time: the SVG graph colors failed components red and shows active faults per second (first 500 sampled requests).
|
|
85
|
+
|
|
86
|
+
## Studio (React + Cytoscape)
|
|
87
|
+
|
|
88
|
+
Full UI in `apps/studio/` (Vite, strict TS). Needs Node deps (run yourself):
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
cd apps/studio
|
|
92
|
+
npm install
|
|
93
|
+
npm run build # tsc plus vite
|
|
94
|
+
npm run dev # /api proxies to 127.0.0.1:8000
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run any scenario vs baseline, inspect p99/error verdict, scrub the failure cascade on the Cytoscape graph.
|
|
98
|
+
|
|
99
|
+
## Environment variables
|
|
100
|
+
|
|
101
|
+
None required. Server mode reads no env vars yet; Cloudflare D1 bindings arrive with the production Worker swap.
|
|
102
|
+
|
|
103
|
+
## Project structure
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
src/shadowbox/ # model, dsl, cli, errors, engine, metrics, report, cards, api, store
|
|
107
|
+
src/shadowbox/data/ # canonical example + chaos cards (shipped in the wheel)
|
|
108
|
+
schemas/ # model-v1.json, scenario-v1.json
|
|
109
|
+
tests/ # unit, deterministic (golden seed 42), property, integration
|
|
110
|
+
tests/fixtures/ # broken models (E_CYCLE, E_REF)
|
|
111
|
+
apps/web/ # static demo with replay (no build)
|
|
112
|
+
apps/studio/ # React + Cytoscape UI (Vite, strict TS)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Run tests
|
|
116
|
+
|
|
117
|
+
```powershell
|
|
118
|
+
uv run ruff check .
|
|
119
|
+
uv run mypy src
|
|
120
|
+
uv run pytest
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Benchmarks (reference: i7-1255U, 16GB, Python 3.13; SLO: 50k events < 2s):
|
|
124
|
+
|
|
125
|
+
- checkout db-failure (6k reqs, 85k events): ~0.05s
|
|
126
|
+
- 60k reqs, 840k events: ~0.6s
|
|
127
|
+
|
|
128
|
+
## Deploy notes
|
|
129
|
+
|
|
130
|
+
Local-first: CLI and `serve` need nothing but Python. Demo deploy: Cloudflare Pages (web) + Python Worker (FastAPI via `workers.asgi`) + D1 — see `Deploy (Cloudflare...)` above. No paid service, no credit card at any tier. See `ShadowBox.md` (local spec, git-ignored) for the full contract.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
shadowbox/__init__.py,sha256=o_QtY1_AvIUnqpsVyGITrFt4bdOlGJEo86lcTCcgOuQ,159
|
|
2
|
+
shadowbox/api.py,sha256=g32KlSsMq6pU5uyviHt4gJwAkdbJEtaNbPoKYgm4Mjc,4858
|
|
3
|
+
shadowbox/cards.py,sha256=Ds_T2Og1okx6InFbGumkpzpapVb5uZkrK8KA3fQSycA,2327
|
|
4
|
+
shadowbox/cli.py,sha256=aRw6S0Ypx4peWbDw4Q6FSkPz779Qyic--tZIL6gI5JI,8414
|
|
5
|
+
shadowbox/compare.py,sha256=8dnzO9cqJIIwi3f0-OIxi3Ztq1ZElltz86zi135OmSU,3563
|
|
6
|
+
shadowbox/dsl.py,sha256=nTZSu1StPpudHBdjxDKnylfHPAo8Ym1OxQYvzCt9844,3114
|
|
7
|
+
shadowbox/engine.py,sha256=AJfSJeG9XlzKw26T33B_ohf6ByelXK660U9jRWar1lI,7905
|
|
8
|
+
shadowbox/errors.py,sha256=ZqfRuI2O9RsCJRZQVf9-Q-9XXu4GfY0TaTSk-JqNTfE,707
|
|
9
|
+
shadowbox/metrics.py,sha256=Bb1aAL_kNyjusWfh5mPg_rc6wW89e-SqvUI66D4ti24,1828
|
|
10
|
+
shadowbox/model.py,sha256=eoRNQ_k5hw2MkAxThoBjagQDEwYH217Bew3JCh3Y29o,1926
|
|
11
|
+
shadowbox/report.py,sha256=NU47bqVbDl_FoHcQjVH6VF_X3Ka7YIugPrDtUj8TRJM,1538
|
|
12
|
+
shadowbox/store.py,sha256=zNU6_b9gIPqRrZYKLpBbJLdaNEVIHlEH3wwxDkC-gfY,2684
|
|
13
|
+
shadowbox/data/__init__.py,sha256=jUhnEvdba9MKVpBiy43hbNqZH-H0Vyspnl_23_XjSok,76
|
|
14
|
+
shadowbox/data/cards/cache-poison.yaml,sha256=L7mS51H1Z_V3Jc2J77tJ1LUyyVQYk-LDnaVMOddwXzo,290
|
|
15
|
+
shadowbox/data/cards/db-down.yaml,sha256=ndaEcepXSk4pqnrZSRO5XCd2lAKJv_xVSjON0LTq-HE,267
|
|
16
|
+
shadowbox/data/cards/latency-500ms.yaml,sha256=KRtMmJLin6ztVrLgATIYALtupxcKGu6fMUsO3CWWCr4,356
|
|
17
|
+
shadowbox/data/cards/queue-overflow.yaml,sha256=743eZqtrMjM1n-c9Sd-JyYlZvXh6OxbriCEAybND6wk,212
|
|
18
|
+
shadowbox/data/cards/slow-dependency.yaml,sha256=E2YZDirycQWZabLGXqRv7H1ohWbj_uVes8VX1-D1vDg,323
|
|
19
|
+
shadowbox/data/cards/traffic-10x.yaml,sha256=K2r4w-_u0IIPWZmGVen99TfURUlVwfMuQJr35lxXqY0,214
|
|
20
|
+
shadowbox/data/cards/zone-loss.yaml,sha256=3Sq_f9i_WtPVvfko3Pg7TEkFpmm_HD_spixPyDn6pSo,355
|
|
21
|
+
shadowbox/data/example/docker-compose.yaml,sha256=QtQ2NLVb41So98upHPd8xD7YUYs8TVY7lK-fdtOpBZA,264
|
|
22
|
+
shadowbox/data/example/model.yaml,sha256=Z7rGZMtnc16kXAFBujKVWUbQb4gxhehBLevlbY-sGZg,648
|
|
23
|
+
shadowbox/data/example/scenarios/db-failure.yaml,sha256=h9nXmjMAvG29OPp2W9SkTcx_RQA01IrAAeiE846LX28,270
|
|
24
|
+
shadowbox/importers/__init__.py,sha256=hwFgnRof5uJCK9ea2j-gZqKrv9cHAjliyRQ9qJfwlt0,97
|
|
25
|
+
shadowbox/importers/compose.py,sha256=pjNLtc_oJmjU7nrfiKZHYBsSBzkxGLYnXOU__xCw95k,3834
|
|
26
|
+
shadowbox-0.3.0.dist-info/METADATA,sha256=gLj9g7tIW3m4-6NWjd067uqBG5qdAc7dfeEet6u5h_U,5097
|
|
27
|
+
shadowbox-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
28
|
+
shadowbox-0.3.0.dist-info/entry_points.txt,sha256=VYWQuwhvc9Aek2hm5AdJt8sPDC4OVtBk7ZYHFbL2068,48
|
|
29
|
+
shadowbox-0.3.0.dist-info/RECORD,,
|