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.
Files changed (26) hide show
  1. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/PKG-INFO +3 -1
  2. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/README.md +2 -0
  3. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/telemetry.py +53 -9
  4. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/pyproject.toml +1 -1
  5. flywheel_bootstrap-0.1.9.202602032202/tests/test_telemetry.py +64 -0
  6. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/.gitignore +0 -0
  7. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/__init__.py +0 -0
  8. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/__main__.py +0 -0
  9. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/artifacts.py +0 -0
  10. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/config_loader.py +0 -0
  11. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/constants.py +0 -0
  12. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/git_ops.py +0 -0
  13. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/install.py +0 -0
  14. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/orchestrator.py +0 -0
  15. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/payload.py +0 -0
  16. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/prompts.py +0 -0
  17. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/py.typed +0 -0
  18. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap/runner.py +0 -0
  19. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/bootstrap.sh +0 -0
  20. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/examples/config.example.toml +0 -0
  21. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_artifacts.py +0 -0
  22. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_entrypoint.py +0 -0
  23. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_git_ops.py +0 -0
  24. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_orchestrator.py +0 -0
  25. {flywheel_bootstrap-0.1.9.202602032022 → flywheel_bootstrap-0.1.9.202602032202}/tests/test_prompts.py +0 -0
  26. {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.202602032022
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(url: str, token: str, body: Mapping[str, object]) -> None:
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=MAX_ATTEMPTS,
116
- base_delay_seconds=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
- ) -> bool:
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flywheel-bootstrap"
3
- version = "0.1.9.202602032022"
3
+ version = "0.1.9.202602032202"
4
4
  description = "Bootstrap runner for Flywheel provisioned GPU instances"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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