flywheel-bootstrap 0.1.9.202602032022__tar.gz → 0.1.9.202602032202__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.
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/PKG-INFO +3 -1
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/README.md +2 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/telemetry.py +53 -9
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/pyproject.toml +1 -1
- flywheel_bootstrap-0.1.9.202602032202/tests/test_telemetry.py +64 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/.gitignore +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/__init__.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/__main__.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/artifacts.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/config_loader.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/constants.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/git_ops.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/install.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/orchestrator.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/payload.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/prompts.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/py.typed +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/runner.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap.sh +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/examples/config.example.toml +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_artifacts.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_entrypoint.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_git_ops.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_orchestrator.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_prompts.py +0 -0
- {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flywheel-bootstrap
|
|
3
|
-
Version: 0.1.9.
|
|
3
|
+
Version: 0.1.9.202602032202
|
|
4
4
|
Summary: Bootstrap runner for Flywheel provisioned GPU instances
|
|
5
5
|
Project-URL: Homepage, http://paradigma.inc/
|
|
6
6
|
Author: Paradigma Labs
|
|
@@ -81,6 +81,8 @@ a starting point; update the paths and instructions for your machine.
|
|
|
81
81
|
- Run `codex exec --json --cd <workspace> --skip-git-repo-check flywheel_prompt.txt` with env `FLYWHEEL_RUN_ID/TOKEN/SERVER`.
|
|
82
82
|
- Start a heartbeat thread posting `/runs/{id}/heartbeat` every 30s.
|
|
83
83
|
- Stream Codex stdout lines as logs to `/runs/{id}/logs`; capture Codex `run_id` if emitted.
|
|
84
|
+
- Oversized log messages are dropped and replaced with a placeholder entry; if a proxy
|
|
85
|
+
returns `413`, the bootstrap treats it as non-fatal and emits the placeholder.
|
|
84
86
|
9. After Codex exits:
|
|
85
87
|
- Read `flywheel_artifacts.json`; if empty and Codex `run_id` is known, attempt one `codex resume <id>` then re-read.
|
|
86
88
|
- POST artifacts to `/runs/{id}/artifacts`; POST `/complete` on exit 0, else `/error` with the exit code.
|
|
@@ -65,6 +65,8 @@ a starting point; update the paths and instructions for your machine.
|
|
|
65
65
|
- Run `codex exec --json --cd <workspace> --skip-git-repo-check flywheel_prompt.txt` with env `FLYWHEEL_RUN_ID/TOKEN/SERVER`.
|
|
66
66
|
- Start a heartbeat thread posting `/runs/{id}/heartbeat` every 30s.
|
|
67
67
|
- Stream Codex stdout lines as logs to `/runs/{id}/logs`; capture Codex `run_id` if emitted.
|
|
68
|
+
- Oversized log messages are dropped and replaced with a placeholder entry; if a proxy
|
|
69
|
+
returns `413`, the bootstrap treats it as non-fatal and emits the placeholder.
|
|
68
70
|
9. After Codex exits:
|
|
69
71
|
- Read `flywheel_artifacts.json`; if empty and Codex `run_id` is known, attempt one `codex resume <id>` then re-read.
|
|
70
72
|
- POST artifacts to `/runs/{id}/artifacts`; POST `/complete` on exit 0, else `/error` with the exit code.
|
|
@@ -16,6 +16,8 @@ import sys
|
|
|
16
16
|
READ_TIMEOUT_SECONDS = 30
|
|
17
17
|
MAX_ATTEMPTS = 3
|
|
18
18
|
BASE_DELAY_SECONDS = 0.5
|
|
19
|
+
MAX_LOG_MESSAGE_BYTES = 25 * 1024 * 1024
|
|
20
|
+
LOG_DROPPED_MESSAGE = "Log entry dropped: too large"
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def utcnow() -> datetime:
|
|
@@ -43,7 +45,7 @@ def post_log(
|
|
|
43
45
|
extra: Mapping[str, object] | None = None,
|
|
44
46
|
) -> None:
|
|
45
47
|
"""Send a log entry to the backend."""
|
|
46
|
-
_post(
|
|
48
|
+
_, status = _post(
|
|
47
49
|
f"{server_url.rstrip('/')}/runs/{run_id}/logs",
|
|
48
50
|
token,
|
|
49
51
|
{
|
|
@@ -52,7 +54,29 @@ def post_log(
|
|
|
52
54
|
"message": message,
|
|
53
55
|
"extra": extra or {},
|
|
54
56
|
},
|
|
57
|
+
non_retryable_statuses={413},
|
|
55
58
|
)
|
|
59
|
+
if status == 413:
|
|
60
|
+
placeholder = {
|
|
61
|
+
"created_at": utcnow().isoformat(),
|
|
62
|
+
"level": "warning",
|
|
63
|
+
"message": LOG_DROPPED_MESSAGE,
|
|
64
|
+
"extra": {
|
|
65
|
+
"dropped": True,
|
|
66
|
+
"drop_reason": "payload_too_large",
|
|
67
|
+
"message_bytes": len(message.encode("utf-8")),
|
|
68
|
+
"limit_bytes": MAX_LOG_MESSAGE_BYTES,
|
|
69
|
+
"original_level": level,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
_post(
|
|
73
|
+
f"{server_url.rstrip('/')}/runs/{run_id}/logs",
|
|
74
|
+
token,
|
|
75
|
+
placeholder,
|
|
76
|
+
non_retryable_statuses={413},
|
|
77
|
+
attempts=1,
|
|
78
|
+
base_delay_seconds=0,
|
|
79
|
+
)
|
|
56
80
|
|
|
57
81
|
|
|
58
82
|
def post_artifacts(
|
|
@@ -98,7 +122,15 @@ def post_error(
|
|
|
98
122
|
)
|
|
99
123
|
|
|
100
124
|
|
|
101
|
-
def _post(
|
|
125
|
+
def _post(
|
|
126
|
+
url: str,
|
|
127
|
+
token: str,
|
|
128
|
+
body: Mapping[str, object],
|
|
129
|
+
*,
|
|
130
|
+
non_retryable_statuses: set[int] | None = None,
|
|
131
|
+
attempts: int = MAX_ATTEMPTS,
|
|
132
|
+
base_delay_seconds: float = BASE_DELAY_SECONDS,
|
|
133
|
+
) -> tuple[bool, int | None]:
|
|
102
134
|
data = json.dumps(body).encode("utf-8")
|
|
103
135
|
req = urllib.request.Request(
|
|
104
136
|
url,
|
|
@@ -109,15 +141,19 @@ def _post(url: str, token: str, body: Mapping[str, object]) -> None:
|
|
|
109
141
|
},
|
|
110
142
|
method="POST",
|
|
111
143
|
)
|
|
112
|
-
success = _urlopen_with_retries(
|
|
144
|
+
success, status = _urlopen_with_retries(
|
|
113
145
|
req,
|
|
114
146
|
timeout_seconds=READ_TIMEOUT_SECONDS,
|
|
115
|
-
attempts=
|
|
116
|
-
base_delay_seconds=
|
|
147
|
+
attempts=attempts,
|
|
148
|
+
base_delay_seconds=base_delay_seconds,
|
|
149
|
+
non_retryable_statuses=non_retryable_statuses,
|
|
117
150
|
)
|
|
118
|
-
if not success
|
|
151
|
+
if not success and (
|
|
152
|
+
not non_retryable_statuses or status not in non_retryable_statuses
|
|
153
|
+
):
|
|
119
154
|
# Best-effort: log locally but do not raise, to avoid failing the run on transient stalls.
|
|
120
155
|
print(f"telemetry POST to {url} failed after retries", file=sys.stderr)
|
|
156
|
+
return success, status
|
|
121
157
|
|
|
122
158
|
|
|
123
159
|
def _urlopen_with_retries(
|
|
@@ -125,7 +161,8 @@ def _urlopen_with_retries(
|
|
|
125
161
|
timeout_seconds: int,
|
|
126
162
|
attempts: int,
|
|
127
163
|
base_delay_seconds: float,
|
|
128
|
-
|
|
164
|
+
non_retryable_statuses: set[int] | None = None,
|
|
165
|
+
) -> tuple[bool, int | None]:
|
|
129
166
|
"""Best-effort POST with retries for transient network errors."""
|
|
130
167
|
for i in range(attempts):
|
|
131
168
|
try:
|
|
@@ -133,7 +170,14 @@ def _urlopen_with_retries(
|
|
|
133
170
|
req, timeout=timeout_seconds
|
|
134
171
|
) as resp: # pragma: no cover - network
|
|
135
172
|
resp.read()
|
|
136
|
-
return True
|
|
173
|
+
return True, getattr(resp, "status", None)
|
|
174
|
+
except urllib.error.HTTPError as exc:
|
|
175
|
+
if non_retryable_statuses and exc.code in non_retryable_statuses:
|
|
176
|
+
return False, exc.code
|
|
177
|
+
if i == attempts - 1:
|
|
178
|
+
return False, exc.code
|
|
179
|
+
delay = base_delay_seconds * (2**i)
|
|
180
|
+
time.sleep(delay)
|
|
137
181
|
except (
|
|
138
182
|
TimeoutError,
|
|
139
183
|
urllib.error.URLError,
|
|
@@ -144,4 +188,4 @@ def _urlopen_with_retries(
|
|
|
144
188
|
break
|
|
145
189
|
delay = base_delay_seconds * (2**i)
|
|
146
190
|
time.sleep(delay)
|
|
147
|
-
return False
|
|
191
|
+
return False, None
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from bootstrap import telemetry
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_post_does_not_warn_on_non_retryable_413(monkeypatch, capsys) -> None:
|
|
7
|
+
def fake_urlopen_with_retries(
|
|
8
|
+
req,
|
|
9
|
+
timeout_seconds: int,
|
|
10
|
+
attempts: int,
|
|
11
|
+
base_delay_seconds: float,
|
|
12
|
+
non_retryable_statuses: set[int] | None = None,
|
|
13
|
+
) -> tuple[bool, int | None]:
|
|
14
|
+
return False, 413
|
|
15
|
+
|
|
16
|
+
monkeypatch.setattr(telemetry, "_urlopen_with_retries", fake_urlopen_with_retries)
|
|
17
|
+
|
|
18
|
+
success, status = telemetry._post(
|
|
19
|
+
"http://example.test/runs/logs",
|
|
20
|
+
"token",
|
|
21
|
+
{"message": "hello"},
|
|
22
|
+
non_retryable_statuses={413},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
captured = capsys.readouterr()
|
|
26
|
+
assert success is False
|
|
27
|
+
assert status == 413
|
|
28
|
+
assert captured.err == ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_post_log_sends_placeholder_on_413(monkeypatch) -> None:
|
|
32
|
+
calls: list[tuple[str, str, dict, dict]] = []
|
|
33
|
+
|
|
34
|
+
def fake_post(url: str, token: str, body: dict, **kwargs):
|
|
35
|
+
calls.append((url, token, body, kwargs))
|
|
36
|
+
if len(calls) == 1:
|
|
37
|
+
return False, 413
|
|
38
|
+
return True, 202
|
|
39
|
+
|
|
40
|
+
monkeypatch.setattr(telemetry, "_post", fake_post)
|
|
41
|
+
|
|
42
|
+
telemetry.post_log(
|
|
43
|
+
"http://example.test",
|
|
44
|
+
"run-123",
|
|
45
|
+
"token",
|
|
46
|
+
"info",
|
|
47
|
+
"hello",
|
|
48
|
+
{"foo": "bar"},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert len(calls) == 2
|
|
52
|
+
assert calls[0][3]["non_retryable_statuses"] == {413}
|
|
53
|
+
|
|
54
|
+
placeholder = calls[1][2]
|
|
55
|
+
assert placeholder["message"] == telemetry.LOG_DROPPED_MESSAGE
|
|
56
|
+
assert placeholder["level"] == "warning"
|
|
57
|
+
extra = placeholder["extra"]
|
|
58
|
+
assert extra["dropped"] is True
|
|
59
|
+
assert extra["drop_reason"] == "payload_too_large"
|
|
60
|
+
assert extra["original_level"] == "info"
|
|
61
|
+
assert extra["message_bytes"] == len("hello".encode("utf-8"))
|
|
62
|
+
assert extra["limit_bytes"] == telemetry.MAX_LOG_MESSAGE_BYTES
|
|
63
|
+
assert calls[1][3]["attempts"] == 1
|
|
64
|
+
assert calls[1][3]["base_delay_seconds"] == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/git_ops.py
RENAMED
|
File without changes
|
{flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/install.py
RENAMED
|
File without changes
|
|
File without changes
|
{flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/payload.py
RENAMED
|
File without changes
|
{flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/prompts.py
RENAMED
|
File without changes
|
{flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/py.typed
RENAMED
|
File without changes
|
{flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/runner.py
RENAMED
|
File without changes
|
{flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|