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/_runner_api.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Minimal client for the *runner-internal* API, used from inside a container.
|
|
2
|
+
|
|
3
|
+
Only the SDK-in-runner needs this — currently just ``volume.commit()``
|
|
4
|
+
(runner-protocol.md §11). It authenticates with the run's ephemeral token and
|
|
5
|
+
talks to ``{FABRIC_API_URL}/internal/runner/v1/…`` (``FORGE_API_URL`` as injected
|
|
6
|
+
by the control plane today — see :mod:`bladekit._env`).
|
|
7
|
+
|
|
8
|
+
Where the token comes from (PAAS-250): the runner (PID 1) reads
|
|
9
|
+
``FORGE_RUNNER_TOKEN`` at boot and REMOVES it from ``os.environ`` before any user
|
|
10
|
+
code is imported, so the function's subprocesses and ``env`` dumps no longer
|
|
11
|
+
carry it. The SDK therefore asks the runner in-process —
|
|
12
|
+
``forge_runner.runtime.credentials()`` — and only falls back to the environment
|
|
13
|
+
for a runner too old to scrub it. See :func:`runner_credentials`.
|
|
14
|
+
|
|
15
|
+
The runner never talks to the agent (invariant): commits are orchestrated by
|
|
16
|
+
the control plane. ``httpx`` is imported lazily to keep ``import bladekit`` cheap.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from types import ModuleType
|
|
22
|
+
from typing import Any, NamedTuple, Optional
|
|
23
|
+
|
|
24
|
+
from . import _env
|
|
25
|
+
from . import exceptions as exc
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RunnerCredentials(NamedTuple):
|
|
29
|
+
"""The run's API base URL and ephemeral token, with where they came from."""
|
|
30
|
+
|
|
31
|
+
api_url: str
|
|
32
|
+
token: str
|
|
33
|
+
source: str # "runner" (in-process context) | "env (FABRIC_RUNNER_TOKEN)" | "env (FORGE_RUNNER_TOKEN)"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def runner_credentials() -> Optional[RunnerCredentials]:
|
|
37
|
+
"""Return the credentials of the run this process belongs to, or ``None``.
|
|
38
|
+
|
|
39
|
+
1. ``forge_runner.runtime.credentials()`` — the runner's in-process context,
|
|
40
|
+
set before user code is imported. The runner package is only importable
|
|
41
|
+
inside an AI Fabric container, so this is ``None`` on a laptop.
|
|
42
|
+
2. ``FABRIC_API_URL`` + ``FABRIC_RUNNER_TOKEN`` (or their ``FORGE_*`` spelling,
|
|
43
|
+
which is what the control plane injects today) in the environment — the legacy
|
|
44
|
+
channel, still honoured for a runner older than 1.4 that does not scrub
|
|
45
|
+
the variable. A current runner leaves nothing here to read.
|
|
46
|
+
"""
|
|
47
|
+
_runner_runtime: Optional[ModuleType]
|
|
48
|
+
try:
|
|
49
|
+
from forge_runner import runtime as _runner_runtime # type: ignore[import-not-found,no-redef]
|
|
50
|
+
except Exception: # noqa: BLE001 — not in a container, or a foreign package
|
|
51
|
+
_runner_runtime = None
|
|
52
|
+
if _runner_runtime is not None:
|
|
53
|
+
get = getattr(_runner_runtime, "credentials", None)
|
|
54
|
+
if callable(get):
|
|
55
|
+
got = get()
|
|
56
|
+
if got:
|
|
57
|
+
api_url, token = got
|
|
58
|
+
if api_url and token:
|
|
59
|
+
return RunnerCredentials(str(api_url), str(token), "runner")
|
|
60
|
+
base = _env.get("API_URL")
|
|
61
|
+
token, token_var = _env.lookup("RUNNER_TOKEN")
|
|
62
|
+
if base and token:
|
|
63
|
+
return RunnerCredentials(base, token, f"env ({token_var})")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# 404 VOLUME_COMMIT_NOT_FOUND / VOLUME_NOT_MOUNTED etc. map onto these.
|
|
67
|
+
_CODE_EXC = {
|
|
68
|
+
"VOLUME_NOT_MOUNTED": exc.VolumeError,
|
|
69
|
+
"VOLUME_READ_ONLY": exc.VolumeCommitError,
|
|
70
|
+
"VOLUME_COMMIT_IN_PROGRESS": exc.VolumeCommitError,
|
|
71
|
+
"VOLUME_COMMIT_NOT_FOUND": exc.VolumeCommitError,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RunnerApiClient:
|
|
76
|
+
"""Synchronous client for ``/internal/runner/v1/*`` (run-token auth)."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, base_url: str, runner_token: str) -> None:
|
|
79
|
+
import httpx
|
|
80
|
+
|
|
81
|
+
self.base_url = base_url.rstrip("/")
|
|
82
|
+
self._http = httpx.Client(
|
|
83
|
+
base_url=self.base_url,
|
|
84
|
+
headers={
|
|
85
|
+
"Authorization": f"Bearer {runner_token}",
|
|
86
|
+
"User-Agent": "forge-runner-sdk/0.1.0",
|
|
87
|
+
},
|
|
88
|
+
timeout=30.0,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_env(cls) -> "RunnerApiClient":
|
|
93
|
+
"""Build the client from the run's credentials (see :func:`runner_credentials`).
|
|
94
|
+
|
|
95
|
+
The name predates PAAS-250: the primary source is now the runner's
|
|
96
|
+
in-process context, not the environment.
|
|
97
|
+
"""
|
|
98
|
+
creds = runner_credentials()
|
|
99
|
+
if creds is None:
|
|
100
|
+
raise exc.VolumeError(
|
|
101
|
+
"volume.commit() must run inside an AI Fabric container "
|
|
102
|
+
"(no runner credentials: not started by forge-runner)."
|
|
103
|
+
)
|
|
104
|
+
return cls(creds.api_url, creds.token)
|
|
105
|
+
|
|
106
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
107
|
+
import httpx
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
resp = self._http.request(method, path, **kwargs)
|
|
111
|
+
except httpx.HTTPError as e:
|
|
112
|
+
raise exc.ConnectionFailed(
|
|
113
|
+
f"runner API call to {path} failed: {e}"
|
|
114
|
+
) from e
|
|
115
|
+
if resp.status_code >= 400:
|
|
116
|
+
code = "INTERNAL"
|
|
117
|
+
message = f"HTTP {resp.status_code}"
|
|
118
|
+
try:
|
|
119
|
+
body = resp.json()
|
|
120
|
+
err = body.get("error") if isinstance(body, dict) else None
|
|
121
|
+
if isinstance(err, dict):
|
|
122
|
+
code = str(err.get("code", code))
|
|
123
|
+
message = str(err.get("message", message))
|
|
124
|
+
except ValueError:
|
|
125
|
+
pass
|
|
126
|
+
exc_cls = _CODE_EXC.get(code)
|
|
127
|
+
if exc_cls is exc.VolumeError:
|
|
128
|
+
raise exc_cls(message)
|
|
129
|
+
if exc_cls is exc.VolumeCommitError:
|
|
130
|
+
raise exc_cls(code, message)
|
|
131
|
+
raise exc.APIError(code, message, status_code=resp.status_code)
|
|
132
|
+
if resp.content:
|
|
133
|
+
return resp.json()
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def commit_volume(self, volume_id: str) -> dict:
|
|
137
|
+
"""POST the commit (202 + VolumeCommit). Idempotent on network retry."""
|
|
138
|
+
return self._request(
|
|
139
|
+
"POST", f"/internal/runner/v1/volumes/{volume_id}/commit"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_volume_commit(self, commit_id: str) -> dict:
|
|
143
|
+
return self._request(
|
|
144
|
+
"GET", f"/internal/runner/v1/volume-commits/{commit_id}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def close(self) -> None:
|
|
148
|
+
self._http.close()
|