bladekit 4.5.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.
- bladekit-4.5.0/.gitignore +71 -0
- bladekit-4.5.0/PKG-INFO +373 -0
- bladekit-4.5.0/README.md +353 -0
- bladekit-4.5.0/pyproject.toml +75 -0
- bladekit-4.5.0/src/bladekit/__init__.py +219 -0
- bladekit-4.5.0/src/bladekit/_app.py +3241 -0
- bladekit-4.5.0/src/bladekit/_banner.py +640 -0
- bladekit-4.5.0/src/bladekit/_batch.py +150 -0
- bladekit-4.5.0/src/bladekit/_bucket.py +211 -0
- bladekit-4.5.0/src/bladekit/_build.py +337 -0
- bladekit-4.5.0/src/bladekit/_bundle.py +459 -0
- bladekit-4.5.0/src/bladekit/_client.py +1152 -0
- bladekit-4.5.0/src/bladekit/_cls.py +574 -0
- bladekit-4.5.0/src/bladekit/_constants.py +121 -0
- bladekit-4.5.0/src/bladekit/_cron.py +88 -0
- bladekit-4.5.0/src/bladekit/_dict.py +174 -0
- bladekit-4.5.0/src/bladekit/_env.py +98 -0
- bladekit-4.5.0/src/bladekit/_image.py +577 -0
- bladekit-4.5.0/src/bladekit/_loader.py +79 -0
- bladekit-4.5.0/src/bladekit/_map.py +555 -0
- bladekit-4.5.0/src/bladekit/_preempt.py +94 -0
- bladekit-4.5.0/src/bladekit/_queue.py +137 -0
- bladekit-4.5.0/src/bladekit/_render.py +683 -0
- bladekit-4.5.0/src/bladekit/_runner_api.py +148 -0
- bladekit-4.5.0/src/bladekit/_sandbox.py +1051 -0
- bladekit-4.5.0/src/bladekit/_secret.py +52 -0
- bladekit-4.5.0/src/bladekit/_serialize.py +88 -0
- bladekit-4.5.0/src/bladekit/_serve.py +188 -0
- bladekit-4.5.0/src/bladekit/_volume.py +232 -0
- bladekit-4.5.0/src/bladekit/_webhook.py +90 -0
- bladekit-4.5.0/src/bladekit/cli.py +2282 -0
- bladekit-4.5.0/src/bladekit/config.py +181 -0
- bladekit-4.5.0/src/bladekit/exceptions.py +544 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Go
|
|
2
|
+
*.test
|
|
3
|
+
/control-plane/bin/
|
|
4
|
+
/forge-node/bin/
|
|
5
|
+
# Local `go build` output landing next to the sources instead of bin/. CI builds
|
|
6
|
+
# these from ./cmd/... and publishes them as generic packages — a host binary
|
|
7
|
+
# committed here is always an accident.
|
|
8
|
+
/forge-node/forge-node
|
|
9
|
+
/forge-node/forge-relay
|
|
10
|
+
/forge-node/forge-sandboxd
|
|
11
|
+
/control-plane/control-plane
|
|
12
|
+
|
|
13
|
+
# Python
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
.venv/
|
|
17
|
+
dist/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
|
|
21
|
+
# Node
|
|
22
|
+
node_modules/
|
|
23
|
+
|
|
24
|
+
# Env / local
|
|
25
|
+
.env
|
|
26
|
+
*.local
|
|
27
|
+
.DS_Store
|
|
28
|
+
|
|
29
|
+
# e2e generated artifacts (logs, screenshots, tokens)
|
|
30
|
+
/e2e/artifacts/
|
|
31
|
+
/e2e/tutorials/artifacts/
|
|
32
|
+
/e2e/artifacts-*
|
|
33
|
+
|
|
34
|
+
# Inference outputs from running the tutorials by hand — the tutorial code
|
|
35
|
+
# writes these to the working directory, so they land at the repo root. The
|
|
36
|
+
# copies that actually matter live in motion/assets/. .dockerignore already
|
|
37
|
+
# excluded sd-out.png and structure.pdb; keep the two lists in step.
|
|
38
|
+
/flux-out.png
|
|
39
|
+
/sdxl-out/
|
|
40
|
+
/sd-out.png
|
|
41
|
+
/animation.mp4
|
|
42
|
+
/ltx-out.mp4
|
|
43
|
+
/structure.pdb
|
|
44
|
+
.claude/worktrees/
|
|
45
|
+
|
|
46
|
+
# provider secrets (OpenStack OS_* creds) — never commit
|
|
47
|
+
.env.provider
|
|
48
|
+
infra/dev/.env.provider
|
|
49
|
+
|
|
50
|
+
# OVH S3 credentials (never commit)
|
|
51
|
+
user-*.txt
|
|
52
|
+
|
|
53
|
+
# Registry v2 token-signing keypair (registry-v2.md §5.1) — never commit
|
|
54
|
+
infra/dev/registry/
|
|
55
|
+
|
|
56
|
+
infra/dev/headscale/lib/
|
|
57
|
+
infra/dev/headscale/authkey.txt
|
|
58
|
+
infra/dev/headscale/preauth.key
|
|
59
|
+
|
|
60
|
+
# forge-node fetches contracts/ here (make contracts) — never a vendored copy.
|
|
61
|
+
/forge-node/.contracts
|
|
62
|
+
/forge-node/.contracts.tmp
|
|
63
|
+
|
|
64
|
+
# iam's published persona -> capability contract, fetched by
|
|
65
|
+
# contracts/fetch-iam-personas.sh and asserted by
|
|
66
|
+
# control-plane/internal/api/adminauthz_test.go. Same rule as the line above:
|
|
67
|
+
# never a vendored copy. A copy in this repo would keep passing after iam's map
|
|
68
|
+
# moved, which is the one thing the cross-check exists to catch.
|
|
69
|
+
/contracts/.iam-personas.json
|
|
70
|
+
/contracts/.iam-personas.json.tmp.*
|
|
71
|
+
|
bladekit-4.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bladekit
|
|
3
|
+
Version: 4.5.0
|
|
4
|
+
Summary: Blade AI Fabric SDK + CLI — run Python functions on Blade's GPU fleet.
|
|
5
|
+
Project-URL: Homepage, https://blade.sh
|
|
6
|
+
Project-URL: Documentation, https://console.blade.sh
|
|
7
|
+
Author: Blade AI Fabric team
|
|
8
|
+
License: Proprietary
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: cloudpickle<4,>=3.0
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Requires-Dist: rich>=13.7
|
|
13
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
14
|
+
Requires-Dist: typer>=0.12
|
|
15
|
+
Requires-Dist: watchfiles>=0.21
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# bladekit — Blade AI Fabric SDK + CLI
|
|
22
|
+
|
|
23
|
+
Run Python functions on the GPU fleet, from your laptop.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Distribution `bladekit`, import `bladekit`, command `blade` (`bladekit` is installed too, as an alias of the same command). Python ≥ 3.10.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install bladekit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Also served, with the `forge-shadow` compatibility package, by the branded index
|
|
34
|
+
`https://console.blade.sh/sdk/` (`docs/INSTALL.md` in the source repository).
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import bladekit
|
|
38
|
+
|
|
39
|
+
app = bladekit.app("demo")
|
|
40
|
+
|
|
41
|
+
@app.function(gpu="A100", image="fabric/torch-2.x")
|
|
42
|
+
def embed(texts):
|
|
43
|
+
from sentence_transformers import SentenceTransformer
|
|
44
|
+
return SentenceTransformer("all-MiniLM-L6-v2").encode(texts).tolist()
|
|
45
|
+
|
|
46
|
+
@app.local_entrypoint()
|
|
47
|
+
def main():
|
|
48
|
+
print(embed.remote(["hello", "world"]))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
$ blade setup # browser flow: mints + saves a token (defaults to https://cp.run.blade.sh)
|
|
53
|
+
$ blade setup --api-url https://cp.run.stg.blade.sh # ... or against another plane
|
|
54
|
+
$ echo "$FABRIC_TOKEN" | blade token set # headless alternative; or: --from-env FABRIC_TOKEN
|
|
55
|
+
$ blade run script.py # streams logs live, returns the result
|
|
56
|
+
$ blade logs <run_id> -f # follow logs with resume
|
|
57
|
+
$ blade status <run_id> # state + queue position
|
|
58
|
+
$ blade runs # list runs
|
|
59
|
+
$ blade cancel <run_id>
|
|
60
|
+
$ blade nodes # fleet: GPUs free/busy
|
|
61
|
+
$ blade --version # installed bladekit version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Object API
|
|
65
|
+
|
|
66
|
+
- `bladekit.app(name, image=...)` (or the class, `bladekit.App`) — namespace of remote functions.
|
|
67
|
+
- `@app.function(gpu=, cpu=, memory=, timeout=, image=, idle_timeout=)` — register
|
|
68
|
+
a function. `image` is a catalogue alias (`fabric/base`) or a raw registry ref
|
|
69
|
+
with an explicit tag/digest. Note: when the platform enforces a registry
|
|
70
|
+
allowlist (`FORGE_IMAGE_REGISTRY_ALLOWLIST`), a raw ref (or an Image-DSL
|
|
71
|
+
`Image.from_registry(...)` base) outside the allowed registries is rejected
|
|
72
|
+
with a 400 `IMAGE_REGISTRY_FORBIDDEN` — catalogue aliases are always allowed.
|
|
73
|
+
- `@app.local_entrypoint()` — the function `blade run` invokes locally.
|
|
74
|
+
- `fn.remote(*args, **kwargs)` — blocking; returns the result or re-raises the
|
|
75
|
+
remote exception (with the remote traceback).
|
|
76
|
+
- `fn.spawn(*args, **kwargs)` — non-blocking; returns a handle with
|
|
77
|
+
`.get(timeout=)` / `.cancel()`.
|
|
78
|
+
- `fn.local(...)` — run locally without the fleet.
|
|
79
|
+
|
|
80
|
+
Successive `.remote()` calls of the same function in one process reuse the same
|
|
81
|
+
run (hot container, FIFO calls). At the end of `blade run` the SDK POSTs
|
|
82
|
+
`/complete` to drain the container immediately.
|
|
83
|
+
|
|
84
|
+
## Container lifecycle hooks
|
|
85
|
+
|
|
86
|
+
An `@app.cls()` class is the stateful counterpart of `@app.function`: it is
|
|
87
|
+
instantiated once per container and its methods (marked with `@app.method()`)
|
|
88
|
+
reuse that instance across calls. Three decorators run code at defined moments in
|
|
89
|
+
the container's life:
|
|
90
|
+
|
|
91
|
+
- `@bladekit.build()` — runs **once at image-build time** to bake expensive,
|
|
92
|
+
deterministic setup into the image layer (download weights, warm a compile
|
|
93
|
+
cache), so every container starts with it already on disk. Until the
|
|
94
|
+
control-plane image-build path executes it, the runner falls back to running it
|
|
95
|
+
**once per container, before the enter hooks** — semantically weaker
|
|
96
|
+
(per-container, not baked in) but correct.
|
|
97
|
+
- `@bladekit.enter()` — runs **once per container** before the first call (the usual
|
|
98
|
+
place to load a model onto the GPU). Re-runs on a fresh instance after a
|
|
99
|
+
`blade serve` hot reload.
|
|
100
|
+
- `@bladekit.exit()` — runs **once per container at graceful teardown**, after the
|
|
101
|
+
last call drains and before the process exits (flush a cache, `volume.commit()`,
|
|
102
|
+
close a handle, emit a final metric). Exit hooks are **best-effort**: an
|
|
103
|
+
exception in one is logged and the remaining hooks still run, so a failing
|
|
104
|
+
teardown never wedges shutdown.
|
|
105
|
+
|
|
106
|
+
Within each kind, hooks run in **definition order**; across kinds the order is
|
|
107
|
+
always build → enter → (calls) → exit.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
@app.cls(gpu="RTX-A4500", image="fabric/torch")
|
|
111
|
+
class Embedder:
|
|
112
|
+
MODEL = "BAAI/bge-small-en-v1.5"
|
|
113
|
+
|
|
114
|
+
@bladekit.build()
|
|
115
|
+
def fetch_weights(self): # once at build time (fallback: per container)
|
|
116
|
+
from huggingface_hub import snapshot_download
|
|
117
|
+
snapshot_download(self.MODEL)
|
|
118
|
+
|
|
119
|
+
@bladekit.enter()
|
|
120
|
+
def load(self): # once per container
|
|
121
|
+
from sentence_transformers import SentenceTransformer
|
|
122
|
+
self.model = SentenceTransformer(self.MODEL, device="cuda")
|
|
123
|
+
|
|
124
|
+
@app.method()
|
|
125
|
+
def embed(self, texts):
|
|
126
|
+
return self.model.encode(texts).tolist()
|
|
127
|
+
|
|
128
|
+
@bladekit.exit()
|
|
129
|
+
def flush(self): # once at teardown, best-effort
|
|
130
|
+
print("shutting down")
|
|
131
|
+
|
|
132
|
+
Embedder().embed.remote(["hello", "world"])
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Parametrized classes
|
|
136
|
+
|
|
137
|
+
`bladekit.parameter()` turns one `@app.cls` definition into **many independent
|
|
138
|
+
instances**, one per distinct set of parameter values — the equivalent of
|
|
139
|
+
`modal.parameter`. Declare parameters as typed class attributes; a parametrized
|
|
140
|
+
class defines no `__init__` (the values are set for you). Each distinct value set
|
|
141
|
+
gets its own run and its own container pool, and the bound values travel with
|
|
142
|
+
every call so AI Fabric routes it to the matching container.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
@app.cls(gpu="RTX-A4500", image="fabric/torch")
|
|
146
|
+
class LLM:
|
|
147
|
+
model_name: str = bladekit.parameter(default="Qwen/Qwen2.5-1.5B-Instruct")
|
|
148
|
+
temperature: float = bladekit.parameter(default=0.7)
|
|
149
|
+
|
|
150
|
+
@bladekit.enter()
|
|
151
|
+
def load(self):
|
|
152
|
+
# each instance reads its own bound parameters
|
|
153
|
+
self.pipe = load_pipeline(self.model_name)
|
|
154
|
+
|
|
155
|
+
@app.method()
|
|
156
|
+
def generate(self, prompt):
|
|
157
|
+
return self.pipe(prompt, temperature=self.temperature)
|
|
158
|
+
|
|
159
|
+
# one definition, two parametrized instances → two container pools
|
|
160
|
+
LLM(model_name="Qwen/Qwen2.5-1.5B-Instruct").generate.remote("hi")
|
|
161
|
+
LLM(model_name="meta-llama/Llama-3.1-8B-Instruct").generate.remote("hi")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- The value type comes from the annotation (or an explicit `type=`) and must be
|
|
165
|
+
one of `str`, `int`, `float`, `bool`, `bytes`. A parameter without a `default`
|
|
166
|
+
is **required** at call time.
|
|
167
|
+
- Instantiate with **keyword arguments only**; unknown or wrongly-typed values
|
|
168
|
+
are rejected with `bladekit.InvalidArgumentError`.
|
|
169
|
+
|
|
170
|
+
## Fan-out: map, spawn_map
|
|
171
|
+
|
|
172
|
+
- `fn.map(iterable, ...)` — blocking fan-out of `f(x)` over the GPU fleet; yields
|
|
173
|
+
results in input order (`order="index"`, default) or arrival order.
|
|
174
|
+
- `fn.starmap(iterable, ...)` — like `map`, but each element is a tuple splatted
|
|
175
|
+
as positional args: `(a, b)` → `f(a, b)`.
|
|
176
|
+
- `fn.spawn_map(iterable, ...)` — **`spawn` + `map`**: fire the whole fan-out and
|
|
177
|
+
return a `MapHandle` *immediately*, without blocking on results. The batch is
|
|
178
|
+
created, all inputs are pushed, and it is finalized server-side; you then poll
|
|
179
|
+
it at your leisure. `fn.spawn_starmap(iterable, ...)` is the tuple-splatting
|
|
180
|
+
variant.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
handle = classify.spawn_map(urls) # returns at once — nothing awaited
|
|
184
|
+
... # do other work; the fleet churns
|
|
185
|
+
for label in handle.results(): # collect later (order/return_exceptions here)
|
|
186
|
+
...
|
|
187
|
+
print(handle.status()) # inspect progress
|
|
188
|
+
handle.cancel() # stop the batch
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The batch is durable server-side, so a handle survives a client disconnect —
|
|
192
|
+
re-attach from another process with `blade map resume <map_id>`. `map()` blocks
|
|
193
|
+
and streams; `spawn_map()` hands you the handle and gets out of the way.
|
|
194
|
+
|
|
195
|
+
## Dynamic batching: `@bladekit.batched`
|
|
196
|
+
|
|
197
|
+
`@bladekit.batched` auto-collects individual calls into a **batch** and invokes the
|
|
198
|
+
decorated function *once* per batch, then fans the per-call results back to each
|
|
199
|
+
caller. The function is written to take *lists* and return a list; callers still
|
|
200
|
+
invoke one item at a time. Stack it under `@app.function` (Modal order):
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
@app.function(gpu="RTX-A4500", image="fabric/torch")
|
|
204
|
+
@bladekit.batched(max_batch_size=32, wait_ms=50)
|
|
205
|
+
def embed(prompts: list[str]) -> list[list[float]]:
|
|
206
|
+
# `prompts` is the whole batch; return one vector per prompt.
|
|
207
|
+
return model.encode(prompts).tolist()
|
|
208
|
+
|
|
209
|
+
vec = embed.remote("hello") # one call in, one vector out — batched under the hood
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
A batch is dispatched as soon as **either** trigger fires: it reaches
|
|
213
|
+
`max_batch_size` inputs, **or** `wait_ms` milliseconds elapse after the batch's
|
|
214
|
+
first input. This is distinct from `@app.concurrent`, which forwards N concurrent
|
|
215
|
+
requests to a long-lived server, each handled independently — `@bladekit.batched`
|
|
216
|
+
*fuses* calls into one invocation so you amortize a fixed per-call cost (a model
|
|
217
|
+
forward pass, a tokenizer round-trip) across the whole batch.
|
|
218
|
+
|
|
219
|
+
Contract: positional args are transposed by position and keyword args by name, so
|
|
220
|
+
every call in a batch must share the same call shape; the function must return a
|
|
221
|
+
list of exactly `len(batch)` results (else the whole batch fails with
|
|
222
|
+
`BATCH_RESULT_LENGTH_MISMATCH`). If the function raises, every call in the batch
|
|
223
|
+
receives that exception.
|
|
224
|
+
|
|
225
|
+
## Cloud bucket mounts
|
|
226
|
+
|
|
227
|
+
`bladekit.CloudBucketMount` mounts an **external** object-store bucket you own
|
|
228
|
+
(S3 / GCS / R2 / MinIO / Scaleway — anything speaking the S3 API) as a
|
|
229
|
+
filesystem path inside a function or deployed replica. Unlike `bladekit.Volume`
|
|
230
|
+
(an AI Fabric-managed network volume with generations and `commit()`), a bucket
|
|
231
|
+
mount is a live view of *your* bucket — AI Fabric only mounts it, it never stores
|
|
232
|
+
the objects. The runner FUSE-mounts each bucket at container start and unmounts
|
|
233
|
+
it on teardown.
|
|
234
|
+
|
|
235
|
+
Pass it in the same `volumes=` map as a `Volume`, keyed by mount path:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
import bladekit, pathlib
|
|
239
|
+
|
|
240
|
+
app = bladekit.App("bucket-demo")
|
|
241
|
+
|
|
242
|
+
creds = bladekit.Secret.from_name("aws-credentials") # holds the bucket keys
|
|
243
|
+
|
|
244
|
+
@app.function(
|
|
245
|
+
image="fabric/base",
|
|
246
|
+
volumes={
|
|
247
|
+
"/mnt/data": bladekit.CloudBucketMount(
|
|
248
|
+
"my-data-bucket",
|
|
249
|
+
secret=creds,
|
|
250
|
+
read_only=True, # mount without write access
|
|
251
|
+
key_prefix="raw/2024/", # mount only this sub-path
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
def count_parquet():
|
|
256
|
+
return sum(1 for _ in pathlib.Path("/mnt/data").glob("*.parquet"))
|
|
257
|
+
|
|
258
|
+
@app.local_entrypoint()
|
|
259
|
+
def main():
|
|
260
|
+
print(count_parquet.remote())
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`bladekit.CloudBucketMount(bucket_name, *, secret, read_only=False, key_prefix=None,
|
|
264
|
+
bucket_endpoint_url=None, requester_pays=False)`:
|
|
265
|
+
|
|
266
|
+
- `secret` — **required**. Credentials come **only** from a `bladekit.Secret`,
|
|
267
|
+
never inline. The wire spec carries the secret *name*, never a value; the
|
|
268
|
+
control plane resolves it and injects its keys (`AWS_ACCESS_KEY_ID` /
|
|
269
|
+
`AWS_SECRET_ACCESS_KEY`, plus optional `AWS_SESSION_TOKEN`) as container env
|
|
270
|
+
vars, and the runner reads those at mount time. A credential never appears in
|
|
271
|
+
the run body, argv, or a log line.
|
|
272
|
+
- `read_only=True` — mounted without write access.
|
|
273
|
+
- `key_prefix="raw/2024/"` — mount only objects under this bucket-relative key
|
|
274
|
+
prefix (must end with `/`).
|
|
275
|
+
- `bucket_endpoint_url=` — S3 endpoint override for non-AWS stores (R2, MinIO,
|
|
276
|
+
GCS-S3, Scaleway); omit for AWS S3.
|
|
277
|
+
- `requester_pays=True` — for buckets with S3 Requester Pays.
|
|
278
|
+
|
|
279
|
+
Store the credentials first with `blade secret create aws-credentials
|
|
280
|
+
AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=…`. At most 8 bucket mounts per
|
|
281
|
+
function; mount paths share the volume namespace (absolute, unique, outside
|
|
282
|
+
reserved paths).
|
|
283
|
+
|
|
284
|
+
## Errors
|
|
285
|
+
|
|
286
|
+
- `bladekit.APIError` (and subclasses) carry the API envelope `code` + human
|
|
287
|
+
message.
|
|
288
|
+
- `bladekit.RunError` (and subclasses `NodeLostError`, `ContainerFailedError`,
|
|
289
|
+
`VersionMismatchError`, …) describe *platform* failures with actionable hints.
|
|
290
|
+
- A remote *user* exception is re-raised as the original exception when possible,
|
|
291
|
+
otherwise wrapped in `bladekit.RemoteFunctionError` — never confused with a
|
|
292
|
+
platform error.
|
|
293
|
+
|
|
294
|
+
## Config
|
|
295
|
+
|
|
296
|
+
`blade setup` (browser token flow) and `blade token set` both write
|
|
297
|
+
`~/.blade/config.toml`. `FABRIC_API_URL` / `FABRIC_TOKEN` environment variables
|
|
298
|
+
take precedence; `FABRIC_CONFIG` points the CLI at another file. `blade token show`
|
|
299
|
+
says where each value came from.
|
|
300
|
+
|
|
301
|
+
`blade token set` **never** accepts the token as a positional argument — that would
|
|
302
|
+
leak it into `ps aux`, shell history and process monitors. Pass it on stdin
|
|
303
|
+
(`echo "$TOK" | blade token set`) or name the variable (`--from-env FABRIC_TOKEN`).
|
|
304
|
+
|
|
305
|
+
Inside a container, read the platform's variables under their `FABRIC_*` names
|
|
306
|
+
(`FABRIC_SERVER_PORT`, `FABRIC_VOLUMES`, `FABRIC_PREEMPT_GRACE_SECONDS`,
|
|
307
|
+
`FABRIC_MAX_SESSION_SECONDS`, `FABRIC_TURN_URL` / `FABRIC_TURNS_URL` /
|
|
308
|
+
`FABRIC_TURN_SECRET`). `FABRIC_*` and `FORGE_*` are reserved: `Image.env()` rejects
|
|
309
|
+
both prefixes.
|
|
310
|
+
|
|
311
|
+
## Compatibility and migration (from `forge` / `forge-shadow`)
|
|
312
|
+
|
|
313
|
+
The SDK used to be `forge-shadow` (import `forge`, command `forge`). Nothing an
|
|
314
|
+
existing user does breaks on upgrade; the old names are deprecated and keep
|
|
315
|
+
working for a transition period (see "Removal" below).
|
|
316
|
+
|
|
317
|
+
| Before | Now | The old one… |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| `pip install forge-shadow` | `pip install bladekit` | installs bladekit + the shims below |
|
|
320
|
+
| `import forge` | `import bladekit` | works, `DeprecationWarning` once |
|
|
321
|
+
| `forge.<x>`, `forge.<module>` | `bladekit.<x>`, `bladekit.<module>` | same objects, same modules |
|
|
322
|
+
| `forge` command | `blade` command | same subcommands and options, notice on stderr |
|
|
323
|
+
| `ForgeError` | `FabricError` | the same class, `DeprecationWarning` on use |
|
|
324
|
+
| `FORGE_TOKEN`, `FORGE_API_URL`, `FORGE_CONFIG`, `FORGE_VERBOSE`, … | `FABRIC_*` | read when the `FABRIC_*` one is unset |
|
|
325
|
+
| `~/.forge/config.toml` | `~/.blade/config.toml` | copied to the new path on first use, left in place |
|
|
326
|
+
| image `forge/torch` | image `fabric/torch` | still accepted, same image |
|
|
327
|
+
|
|
328
|
+
Details:
|
|
329
|
+
|
|
330
|
+
- **`forge-shadow`** is now a thin compatibility distribution (source in
|
|
331
|
+
`sdk/compat/forge-shadow/`), published at exactly the same
|
|
332
|
+
version as `bladekit`, which it requires (`bladekit==<version>`). It
|
|
333
|
+
ships the `forge` import shim and the `forge` command. `bladekit` alone does
|
|
334
|
+
not ship a `forge` package: two distributions owning one `forge/` directory is
|
|
335
|
+
what pip deletes during an upgrade, and `forge` is another project's name on
|
|
336
|
+
public PyPI. `pip install -U forge-shadow` therefore upgrades an existing install
|
|
337
|
+
in place; new installs should use `bladekit`.
|
|
338
|
+
- **The `forge` import shim** resolves every `forge.<module>` to the already
|
|
339
|
+
imported `bladekit.<module>` object, so `isinstance`/`except` checks and
|
|
340
|
+
pickles that name `forge.*` modules (written by an older client) keep working.
|
|
341
|
+
New pickles name `bladekit.*`, which needs bladekit on the other side —
|
|
342
|
+
every image the control plane builds has it, because the image's
|
|
343
|
+
`forge-shadow==<version>` pulls it in.
|
|
344
|
+
- **Environment**: `FABRIC_<NAME>` wins; `FORGE_<NAME>` is the fallback. When both
|
|
345
|
+
are set to different values the SDK uses `FABRIC_<NAME>` and warns once.
|
|
346
|
+
The control plane still injects `FORGE_*` into containers, so apps deployed with
|
|
347
|
+
the old SDK keep running unchanged, and importing `bladekit` in a container
|
|
348
|
+
also exposes the user-facing ones under their `FABRIC_*` names.
|
|
349
|
+
- **Catalog aliases**: the SDK sends `fabric/<name>` to the control plane as
|
|
350
|
+
`forge/<name>` (the only spelling the server knows for now); `blade image list`
|
|
351
|
+
shows `fabric/<name>`.
|
|
352
|
+
- **Unchanged on purpose** (server side, renamed separately): HTTP headers
|
|
353
|
+
(`X-Forge-*`, `Forge-Signature`), hosts, the `forge-runner` / `forge-sandboxd`
|
|
354
|
+
processes and their `[forge-runner]` log prefix, the `fg_` token prefix, the
|
|
355
|
+
`__forge_*__` attributes the runner reads, and `.forgeignore`.
|
|
356
|
+
|
|
357
|
+
**Removal.** The deprecated names (`forge` import and command, `forge-shadow`,
|
|
358
|
+
`ForgeError`, `FORGE_*` fallback, `~/.forge` migration, `forge/` alias rewrite) are
|
|
359
|
+
kept for a transition period whose end is not decided yet; they are removed in a
|
|
360
|
+
future major release, announced in the changelog.
|
|
361
|
+
|
|
362
|
+
## .forgeignore
|
|
363
|
+
|
|
364
|
+
Bundling tars the script's directory. Add a `.forgeignore` to exclude paths; it
|
|
365
|
+
uses exactly the `.gitignore` syntax and matching rules (`*` never crosses `/`,
|
|
366
|
+
a leading or middle `/` anchors at the bundle root, a trailing `/` matches
|
|
367
|
+
directories only, `**` spans directories, `!` re-includes). `.git`, `.venv`,
|
|
368
|
+
`__pycache__` are always excluded.
|
|
369
|
+
|
|
370
|
+
`Image.from_dockerfile(context_dir=...)` filters its build context with a
|
|
371
|
+
`.dockerignore` at the context root instead, matched exactly like `docker build`
|
|
372
|
+
(patterns are relative to the context root, so `*.py` only matches top-level
|
|
373
|
+
files; use `**/*.py` for any depth). `!` is not supported there yet.
|