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.
- bladekit/__init__.py +219 -0
- bladekit/_app.py +3241 -0
- bladekit/_banner.py +640 -0
- bladekit/_batch.py +150 -0
- bladekit/_bucket.py +211 -0
- bladekit/_build.py +337 -0
- bladekit/_bundle.py +459 -0
- bladekit/_client.py +1152 -0
- bladekit/_cls.py +574 -0
- bladekit/_constants.py +121 -0
- bladekit/_cron.py +88 -0
- bladekit/_dict.py +174 -0
- bladekit/_env.py +98 -0
- bladekit/_image.py +577 -0
- bladekit/_loader.py +79 -0
- bladekit/_map.py +555 -0
- bladekit/_preempt.py +94 -0
- bladekit/_queue.py +137 -0
- bladekit/_render.py +683 -0
- bladekit/_runner_api.py +148 -0
- bladekit/_sandbox.py +1051 -0
- bladekit/_secret.py +52 -0
- bladekit/_serialize.py +88 -0
- bladekit/_serve.py +188 -0
- bladekit/_volume.py +232 -0
- bladekit/_webhook.py +90 -0
- bladekit/cli.py +2282 -0
- bladekit/config.py +181 -0
- bladekit/exceptions.py +544 -0
- bladekit-4.5.0.dist-info/METADATA +373 -0
- bladekit-4.5.0.dist-info/RECORD +33 -0
- bladekit-4.5.0.dist-info/WHEEL +4 -0
- bladekit-4.5.0.dist-info/entry_points.txt +3 -0
bladekit/config.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Client configuration resolution.
|
|
2
|
+
|
|
3
|
+
Precedence (task requirement 5): ``FABRIC_API_URL`` / ``FABRIC_TOKEN`` environment
|
|
4
|
+
variables (or their legacy ``FORGE_*`` spelling) win over ``~/.blade/config.toml``.
|
|
5
|
+
|
|
6
|
+
The TOML file is written by ``blade token set`` and read by ``blade token show``
|
|
7
|
+
and every client. Kept dependency-light: stdlib ``tomllib`` on 3.11+, ``tomli``
|
|
8
|
+
fallback on 3.10, and a tiny hand-rolled writer (we never need to serialize
|
|
9
|
+
arbitrary TOML — only ``api_url`` and ``token``).
|
|
10
|
+
|
|
11
|
+
Migration (PAAS-771): the file used to live at ``~/.forge/config.toml``. When the
|
|
12
|
+
new file is absent and the legacy one exists, the legacy file is copied to the new
|
|
13
|
+
location on first read and left in place, so an older ``forge`` CLI installed side
|
|
14
|
+
by side keeps working.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from . import _env
|
|
27
|
+
from ._constants import DEFAULT_API_URL
|
|
28
|
+
|
|
29
|
+
if sys.version_info >= (3, 11):
|
|
30
|
+
import tomllib as _toml
|
|
31
|
+
else: # pragma: no cover - exercised only on 3.10
|
|
32
|
+
import tomli as _toml # type: ignore[no-redef]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_config_path() -> Path:
|
|
36
|
+
"""``~/.blade/config.toml`` — where the CLI keeps its token."""
|
|
37
|
+
return Path.home() / ".blade" / "config.toml"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def legacy_config_path() -> Path:
|
|
41
|
+
"""``~/.forge/config.toml`` — where the ``forge`` CLI kept it (pre-PAAS-771)."""
|
|
42
|
+
return Path.home() / ".forge" / "config.toml"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def config_path() -> Path:
|
|
46
|
+
"""Location of the config file (override with ``FABRIC_CONFIG`` for tests)."""
|
|
47
|
+
override = _env.get("CONFIG")
|
|
48
|
+
if override:
|
|
49
|
+
return Path(override)
|
|
50
|
+
return default_config_path()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def migrate_legacy_config() -> Optional[Path]:
|
|
54
|
+
"""Copy ``~/.forge/config.toml`` to ``~/.blade/config.toml`` if only the former exists.
|
|
55
|
+
|
|
56
|
+
Returns the path that now holds the configuration (the new one after a
|
|
57
|
+
successful copy, the legacy one if the copy failed — e.g. a read-only home —
|
|
58
|
+
so the caller can still read it), or None when there is nothing to migrate.
|
|
59
|
+
The legacy file is never modified or removed. Skipped when ``FABRIC_CONFIG``
|
|
60
|
+
/ ``FORGE_CONFIG`` points somewhere explicit.
|
|
61
|
+
"""
|
|
62
|
+
if _env.get("CONFIG"):
|
|
63
|
+
return None
|
|
64
|
+
new, old = default_config_path(), legacy_config_path()
|
|
65
|
+
if new.exists() or not old.is_file():
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
# Same private permissions write_config() uses: the file holds a token.
|
|
69
|
+
new.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
70
|
+
shutil.copyfile(old, new)
|
|
71
|
+
new.chmod(0o600)
|
|
72
|
+
except OSError:
|
|
73
|
+
return old
|
|
74
|
+
return new
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class Config:
|
|
78
|
+
api_url: str
|
|
79
|
+
token: Optional[str]
|
|
80
|
+
# Where each value came from, for ``blade token show``.
|
|
81
|
+
api_url_source: str = "default"
|
|
82
|
+
token_source: str = "none"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read_file() -> dict:
|
|
86
|
+
path = config_path()
|
|
87
|
+
if not path.exists():
|
|
88
|
+
migrated = migrate_legacy_config()
|
|
89
|
+
if migrated is not None:
|
|
90
|
+
path = migrated
|
|
91
|
+
if not path.exists():
|
|
92
|
+
return {}
|
|
93
|
+
try:
|
|
94
|
+
with path.open("rb") as fh:
|
|
95
|
+
return _toml.load(fh)
|
|
96
|
+
except (OSError, _toml.TOMLDecodeError):
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_config() -> Config:
|
|
101
|
+
"""Resolve effective config from env then file then defaults."""
|
|
102
|
+
file_data = _read_file()
|
|
103
|
+
|
|
104
|
+
env_url, env_url_var = _env.lookup("API_URL")
|
|
105
|
+
if env_url:
|
|
106
|
+
api_url, url_src = env_url, f"env ({env_url_var})"
|
|
107
|
+
elif file_data.get("api_url"):
|
|
108
|
+
api_url, url_src = str(file_data["api_url"]), f"file ({config_path()})"
|
|
109
|
+
else:
|
|
110
|
+
api_url, url_src = DEFAULT_API_URL, "default"
|
|
111
|
+
|
|
112
|
+
env_token, env_token_var = _env.lookup("TOKEN")
|
|
113
|
+
if env_token:
|
|
114
|
+
token, tok_src = env_token, f"env ({env_token_var})"
|
|
115
|
+
elif file_data.get("token"):
|
|
116
|
+
token, tok_src = str(file_data["token"]), f"file ({config_path()})"
|
|
117
|
+
else:
|
|
118
|
+
# In-container fallback: a function calling the AI Fabric API (Sandbox.create
|
|
119
|
+
# without secrets, Queue/Dict items) authenticates with the run's
|
|
120
|
+
# ephemeral runner token, obtained from the runner's in-process context —
|
|
121
|
+
# NOT from os.environ, which the runner scrubs before importing user code
|
|
122
|
+
# (PAAS-250). The server accepts that token as the run's owner only on
|
|
123
|
+
# the runner-token allowlist (control plane runnerTokenRoutes: no
|
|
124
|
+
# secrets in sandboxes, no volumes, no org-wide listings) and only while
|
|
125
|
+
# the run is alive. The env form is honoured for pre-1.4 runners.
|
|
126
|
+
from ._runner_api import runner_credentials
|
|
127
|
+
|
|
128
|
+
creds = runner_credentials()
|
|
129
|
+
if creds is not None:
|
|
130
|
+
token, tok_src = creds.token, creds.source
|
|
131
|
+
else:
|
|
132
|
+
token, tok_src = None, "none"
|
|
133
|
+
|
|
134
|
+
return Config(
|
|
135
|
+
api_url=api_url.rstrip("/"),
|
|
136
|
+
token=token,
|
|
137
|
+
api_url_source=url_src,
|
|
138
|
+
token_source=tok_src,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _toml_escape(value: str) -> str:
|
|
143
|
+
return value.replace("\\", "\\\\").replace('"', '\\"')
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def write_config(*, token: Optional[str] = None, api_url: Optional[str] = None) -> Path:
|
|
147
|
+
"""Persist token and/or api_url to the config file (merging with existing)."""
|
|
148
|
+
data = _read_file()
|
|
149
|
+
if token is not None:
|
|
150
|
+
data["token"] = token
|
|
151
|
+
if api_url is not None:
|
|
152
|
+
data["api_url"] = api_url.rstrip("/")
|
|
153
|
+
|
|
154
|
+
path = config_path()
|
|
155
|
+
# The file holds a secret token: create both the dir and the file with private
|
|
156
|
+
# perms *from the start* (no world/group-readable window between create and
|
|
157
|
+
# chmod). mkdir(mode=0o700) + os.open(O_CREAT, 0o600) — the mode only applies on
|
|
158
|
+
# creation, so chmod afterwards keeps perms tight if the file already existed.
|
|
159
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
160
|
+
lines = ["# Blade AI Fabric CLI configuration — written by `blade token set`.", ""]
|
|
161
|
+
if "api_url" in data:
|
|
162
|
+
lines.append(f'api_url = "{_toml_escape(str(data["api_url"]))}"')
|
|
163
|
+
if "token" in data:
|
|
164
|
+
lines.append(f'token = "{_toml_escape(str(data["token"]))}"')
|
|
165
|
+
lines.append("")
|
|
166
|
+
payload = "\n".join(lines).encode("utf-8")
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
170
|
+
except OSError: # pragma: no cover - non-POSIX fallback
|
|
171
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
172
|
+
else:
|
|
173
|
+
with os.fdopen(fd, "wb") as fh:
|
|
174
|
+
fh.write(payload)
|
|
175
|
+
# Re-assert 0600 in case the file pre-existed with looser perms (O_CREAT mode
|
|
176
|
+
# is ignored for an existing file).
|
|
177
|
+
try:
|
|
178
|
+
path.chmod(0o600)
|
|
179
|
+
except OSError: # pragma: no cover - non-POSIX
|
|
180
|
+
pass
|
|
181
|
+
return path
|