bladekit 4.5.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.
@@ -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.
@@ -0,0 +1,33 @@
1
+ bladekit/__init__.py,sha256=8zH27-ZVypvuQk_eqfXUEtQOA7y_65Q_JNVTKjThqOI,6202
2
+ bladekit/_app.py,sha256=CcbArZXVZVC1K0mkEfzEkrIHYBIyi7gGhVDbcVpsTtM,136854
3
+ bladekit/_banner.py,sha256=jB8hpZEM4y2o6Lih-hrYSLw1Zp_rQzcTVQw6Asm8--s,104941
4
+ bladekit/_batch.py,sha256=5FMquGqWl_PN3U_NYSCvTOBryC_0hEALqYFYnUYm2kk,6362
5
+ bladekit/_bucket.py,sha256=xaFUfLj4upLGewNHzLdMdY5eESJO1ufeoBAn1eFm8oM,8155
6
+ bladekit/_build.py,sha256=VKMOud69-o2qSE8TwhDFlRx1EEzm6nqtTwVY1PGt54g,13599
7
+ bladekit/_bundle.py,sha256=9Qso0mX8HXEBksw3DVLiUYhfG2OAYvZ_uQUsLDSldNQ,15898
8
+ bladekit/_client.py,sha256=l7TSsMA4qfrEC06Fnt7gUq-0CX_zO-hG3bC_tp5dN-Y,44590
9
+ bladekit/_cls.py,sha256=t4bL4CKEBo5BJaP_ojDEf0y4DRuFciK2_O9NuRM2H5o,24601
10
+ bladekit/_constants.py,sha256=7nGaRKfUD1TLoV4AUAfxJA8xXsCJpQO0lRgz92xBaEs,4686
11
+ bladekit/_cron.py,sha256=vxyXkjUoVvV44KNFGntrOiz0nHNLKNPolGDYsFrJnjY,3209
12
+ bladekit/_dict.py,sha256=hyNOfn93agetrQVvPz2qg5Q9XrBriCF4UYp0HKB6owk,6409
13
+ bladekit/_env.py,sha256=UAz50R18PHtpWSsf5ZDeDZaciSzxZYO1vqtTUO3_N8M,3870
14
+ bladekit/_image.py,sha256=FJAx-LbEXLUmHlyJ0aLUgQp-TB_6OwHkG9Ec7LnmsW0,25071
15
+ bladekit/_loader.py,sha256=lC-BXNH5w5Gp4lk7IgxORynNj1MZYoZxaN3UtLR-9Yg,2938
16
+ bladekit/_map.py,sha256=dnJxvQSHHbygzlS2Sq4C-CkSRSAZkj6ed0sfjzvkJGg,19995
17
+ bladekit/_preempt.py,sha256=bwqDfpgXYJz5LQdTvf3Nfn3BG5ewVzcxda5iFxWUBq4,4115
18
+ bladekit/_queue.py,sha256=Imd7tWIPEGCwFauzsD2gH9QNrNbvwOKkhrykkHnOAMI,5338
19
+ bladekit/_render.py,sha256=F8aWRe-7zD5TxBaThFLccV2gmhw4aht91P9G-v9_H7U,25068
20
+ bladekit/_runner_api.py,sha256=wOaWITd-JnNRGWS8tKMnol7Vt6oguE-qM1pkh-7a334,5864
21
+ bladekit/_sandbox.py,sha256=eeEH-QXLIMkOa0SYl0mly_OjkuwVFJ-JoKdp--8dUEI,42774
22
+ bladekit/_secret.py,sha256=xvoNJeiVqL82wGSOjd71fBYKMZhp1eWXxSub_fR8Sck,1748
23
+ bladekit/_serialize.py,sha256=mi1YLEoY11qc4iBd9PmvB9KRjABQQg98xf-1X1qKkmI,2818
24
+ bladekit/_serve.py,sha256=mqX2bJF4vzbeV716TyHgEl2XBNkOt4ci3RufkOe-2mk,7513
25
+ bladekit/_volume.py,sha256=v1zTproYZrUAIOGu4KynnE_YGF0uOqDoxnBl-ABTGA8,9272
26
+ bladekit/_webhook.py,sha256=cz-HjUePmPZQ5xd5a-kCRm26b1A_4ZiRXFM2eIoRDpM,3410
27
+ bladekit/cli.py,sha256=ZkbBjBEREgTe-u1QA5QRWhBWw3vR2_6SoWSzpWvFEbM,83202
28
+ bladekit/config.py,sha256=o7gdCS5RKvnf8Hx_nzwHKAGlrhYbGsqZObaySdLdH1Q,6653
29
+ bladekit/exceptions.py,sha256=D1gca1OJ8uqoWgu75H688ulUDMb3HACL8D3J7WE3rC8,19022
30
+ bladekit-4.5.0.dist-info/METADATA,sha256=BdP-oi-u29y56om8POLapRpLqKxCeziQ-eIWjKG3uQc,17287
31
+ bladekit-4.5.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
32
+ bladekit-4.5.0.dist-info/entry_points.txt,sha256=PEjjeU0e38da6CeODEwWZI3nnLxarvAoGH0OC82s-fA,71
33
+ bladekit-4.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ blade = bladekit.cli:app
3
+ bladekit = bladekit.cli:app