cortex-training 0.0.3__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,389 @@
1
+ # Copyright 2025 Snowflake Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """Pure formatting helpers for the Cortex Training log/event TUI.
17
+
18
+ Kept free of any ``textual`` import so they can be unit-tested without the
19
+ optional TUI dependency installed.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import textwrap
26
+ from datetime import datetime
27
+ from datetime import timezone
28
+ from typing import Any
29
+
30
+ EVENTS_SOURCE_ID = "__events__"
31
+ EVENTS_LABEL = "▸ scheduling events"
32
+
33
+
34
+ def format_log_entry(entry: Any) -> str:
35
+ """Render one log entry (a parsed JSONL object or ``{"_raw": ...}``)."""
36
+ if not isinstance(entry, dict):
37
+ return str(entry)
38
+ if "_raw" in entry:
39
+ text = str(entry["_raw"])
40
+ return text + " …[truncated]" if entry.get("_truncated") else text
41
+ ts = str(entry.get("ts", "")).strip()
42
+ level = str(entry.get("level", "")).strip()
43
+ logger = str(entry.get("logger", "")).strip()
44
+ msg = entry.get("msg")
45
+ if msg is None:
46
+ msg = entry.get("message")
47
+ if msg is None:
48
+ # Unknown shape: show the raw JSON so nothing is silently dropped.
49
+ msg = json.dumps(entry, sort_keys=True)
50
+ parts = []
51
+ if ts:
52
+ parts.append(ts)
53
+ if level:
54
+ parts.append(f"{level:<5}")
55
+ if logger:
56
+ parts.append(f"{logger}:")
57
+ parts.append(str(msg))
58
+ line = " ".join(parts)
59
+ if entry.get("_truncated"):
60
+ line += " …[truncated]"
61
+ return line
62
+
63
+
64
+ def wrap_log_line(text: Any, width: int) -> list[str]:
65
+ """Soft-wrap one log line to ``width`` columns for display.
66
+
67
+ The ``Log`` widget (unlike ``RichLog``) does not wrap, so we pre-wrap. Word
68
+ boundaries are preferred but long tokens (URLs, JSON blobs) are hard-broken
69
+ so nothing overflows. Embedded newlines split into separate lines first, and
70
+ leading whitespace (indentation) is preserved. ``width <= 0`` (not laid out
71
+ yet) returns the line unwrapped.
72
+ """
73
+ out: list[str] = []
74
+ for logical in str(text).split("\n"):
75
+ if width <= 0 or len(logical) <= width:
76
+ out.append(logical)
77
+ continue
78
+ pieces = textwrap.wrap(
79
+ logical,
80
+ width=width,
81
+ break_long_words=True,
82
+ break_on_hyphens=False,
83
+ replace_whitespace=False,
84
+ drop_whitespace=False,
85
+ )
86
+ out.extend(pieces or [""])
87
+ return out or [""]
88
+
89
+
90
+ def format_event(ev: Any) -> str:
91
+ """Render one zone scheduling event."""
92
+ if not isinstance(ev, dict):
93
+ return str(ev)
94
+ parts = []
95
+ ts = str(ev.get("ts", "")).strip()
96
+ if ts:
97
+ parts.append(ts)
98
+ parts.append(str(ev.get("kind", "event")))
99
+ if zone := ev.get("zone"):
100
+ parts.append(f"zone={zone}")
101
+ frm, to = ev.get("from"), ev.get("to")
102
+ if frm or to:
103
+ parts.append(f"{frm or '?'}→{to or '?'}")
104
+ if sub := ev.get("sub_job_id"):
105
+ parts.append(f"sub_job={sub}")
106
+ if detail := ev.get("detail"):
107
+ parts.append(str(detail))
108
+ return " ".join(parts)
109
+
110
+
111
+ def source_label(source: Any) -> str:
112
+ """Side-panel label for a sub-job source.
113
+
114
+ A source is a sub-job: ``{"sub_job_id", "job_type"}`` (or just a sub_job_id
115
+ string). Rendered as ``"<type> #<index>"`` — the type comes from job_type
116
+ when present, else the middle field of the ``<uuid>:<type>:<index>`` id.
117
+ """
118
+ sjid = ""
119
+ job_type = None
120
+ if isinstance(source, dict):
121
+ sjid = str(source.get("sub_job_id") or source.get("id") or source.get("source_id") or "")
122
+ job_type = source.get("job_type") or source.get("type")
123
+ else:
124
+ sjid = str(source)
125
+ parts = sjid.split(":")
126
+ if not job_type and len(parts) >= 2:
127
+ job_type = parts[1] # <uuid>:<type>:<index>
128
+ idx = parts[2] if len(parts) >= 3 else ""
129
+ if job_type:
130
+ return f"{job_type} #{idx}" if idx != "" else str(job_type)
131
+ if isinstance(source, dict) and source.get("label"):
132
+ return str(source["label"])
133
+ return sjid or str(source)
134
+
135
+
136
+ _ACTIVE_STATUSES = {
137
+ "running",
138
+ "initializing",
139
+ "pending",
140
+ "placing",
141
+ "queued",
142
+ "creating",
143
+ }
144
+
145
+ _TERMINAL_STATUSES = {
146
+ "failed",
147
+ "cancelled",
148
+ "canceled",
149
+ "terminated",
150
+ "completed",
151
+ "succeeded",
152
+ "done",
153
+ "error",
154
+ }
155
+
156
+
157
+ def is_active_status(status: Any) -> bool:
158
+ """Whether a job is still live, i.e. its log tail should keep following.
159
+
160
+ Missing/unknown statuses default to active so a running-but-unclassified job
161
+ never stops tailing; only a recognized terminal status stops the follow.
162
+ """
163
+ if not status:
164
+ return True
165
+ s = str(status).lower().removeprefix("job_state_")
166
+ if s in _ACTIVE_STATUSES:
167
+ return True
168
+ if s in _TERMINAL_STATUSES:
169
+ return False
170
+ return True
171
+
172
+
173
+ def format_job_row(job: Any) -> str:
174
+ """One job-picker row with │-separated columns: status │ created │ id │ types."""
175
+ if not isinstance(job, dict):
176
+ return str(job)
177
+ jid = job.get("job_id") or job.get("id") or "?"
178
+ status = str(job.get("status", "")).strip() or "?"
179
+ status = status.removeprefix("JOB_STATE_")
180
+ created = format_created_at(job.get("created_at")) or "—"
181
+ # Fixed-width columns + separators so fields stay visually distinct.
182
+ row = f"{status:<10} │ {created:<16} │ {jid}"
183
+ subs = job.get("sub_jobs") or []
184
+ types = [s.get("job_type") or s.get("type") for s in subs if isinstance(s, dict)]
185
+ types = [t for t in types if t]
186
+ if not types and job.get("job_type"):
187
+ types = [job["job_type"]]
188
+ if types:
189
+ row += f" │ {', '.join(map(str, types))}"
190
+ return row
191
+
192
+
193
+ def job_status_color(status: Any) -> str:
194
+ """Subtle accent for a job's *status word only* (empty = default color).
195
+
196
+ Deliberately minimal: a gentle green marks live jobs so you can spot the one
197
+ you're running; every other status (failed/cancelled/terminated/unknown)
198
+ keeps the default foreground — no alarm colors.
199
+ """
200
+ s = str(status or "").strip().lower().removeprefix("job_state_")
201
+ if s in _ACTIVE_STATUSES:
202
+ return "green"
203
+ return ""
204
+
205
+
206
+ def format_created_at(raw: Any) -> str:
207
+ """Render an ISO-8601 created_at (e.g. ``2026-06-13T18:49:54Z``) as local
208
+ ``YYYY-MM-DD HH:MM``. Returns ``""`` for missing/unparseable values."""
209
+ if not raw:
210
+ return ""
211
+ s = str(raw).strip()
212
+ if s.endswith("Z"):
213
+ s = s[:-1] + "+00:00"
214
+ try:
215
+ dt = datetime.fromisoformat(s)
216
+ except ValueError:
217
+ return ""
218
+ if dt.tzinfo is None:
219
+ dt = dt.replace(tzinfo=timezone.utc)
220
+ return dt.astimezone().strftime("%Y-%m-%d %H:%M")
221
+
222
+
223
+ def _created_epoch(raw: Any) -> float:
224
+ """Parse a created_at into epoch seconds for sorting. Missing/unparseable
225
+ values sort oldest (``-inf``)."""
226
+ if not raw:
227
+ return float("-inf")
228
+ s = str(raw).strip()
229
+ if s.endswith("Z"):
230
+ s = s[:-1] + "+00:00"
231
+ try:
232
+ dt = datetime.fromisoformat(s)
233
+ except ValueError:
234
+ return float("-inf")
235
+ if dt.tzinfo is None:
236
+ dt = dt.replace(tzinfo=timezone.utc)
237
+ return dt.timestamp()
238
+
239
+
240
+ def sort_jobs(jobs: Any) -> list:
241
+ """Order jobs for the picker newest-first by created_at. Ties keep their
242
+ original order (stable); jobs without a parseable created_at sort last."""
243
+ if not isinstance(jobs, list):
244
+ return []
245
+ return sorted(
246
+ [j for j in jobs if isinstance(j, dict)],
247
+ key=lambda job: _created_epoch(job.get("created_at")),
248
+ reverse=True,
249
+ )
250
+
251
+
252
+ # ─── Job summary (whitelisted create-job params) ─────────────────────────────
253
+
254
+ # Only the parameters a customer passes to SubJobConfig.training_job /
255
+ # sampling_job are surfaced — short label + the config key it reads.
256
+ _SUMMARY_CONFIG_KEYS = [
257
+ ("n_gpus", "gpus"),
258
+ ("max_seq_len", "seq"),
259
+ ("train_batch_size", "bs"),
260
+ ("gradient_clipping", "clip"),
261
+ ]
262
+ _SUMMARY_SUBJOB_KEYS = [
263
+ ("global_batch_size", "gbs"),
264
+ ("dtype", "dtype"),
265
+ ("seed", "seed"),
266
+ ]
267
+
268
+
269
+ def _num(v: Any) -> str:
270
+ """Render a value compactly: integral floats lose the .0 (n_gpus 1.0 -> 1)."""
271
+ if isinstance(v, bool):
272
+ return str(v)
273
+ if isinstance(v, float) and v.is_integer():
274
+ return str(int(v))
275
+ return str(v)
276
+
277
+
278
+ def _subjob_config(sj: dict) -> dict:
279
+ for key in ("training_config", "inference_config", "sampling_config"):
280
+ cfg = sj.get(key)
281
+ if isinstance(cfg, dict):
282
+ return cfg
283
+ return {}
284
+
285
+
286
+ def _job_config_segments(job: dict) -> list:
287
+ """Per-sub-job static config segments (type / model / whitelisted params)."""
288
+ segs = []
289
+ for sj in job.get("sub_jobs") or []:
290
+ if not isinstance(sj, dict):
291
+ continue
292
+ seg = []
293
+ jt = str(sj.get("job_type") or sj.get("type") or "").lower()
294
+ if jt:
295
+ seg.append(jt)
296
+ if sj.get("model_name"):
297
+ seg.append(str(sj["model_name"]))
298
+ cfg = _subjob_config(sj)
299
+ for key, label in _SUMMARY_CONFIG_KEYS:
300
+ if cfg.get(key) is not None:
301
+ seg.append(f"{label}={_num(cfg[key])}")
302
+ opt = cfg.get("optimizer")
303
+ if isinstance(opt, dict):
304
+ o = [str(opt["name"])] if opt.get("name") else []
305
+ if opt.get("lr") is not None:
306
+ o.append(f"lr={_num(opt['lr'])}")
307
+ if o:
308
+ seg.append("opt=" + "/".join(o))
309
+ for key, label in _SUMMARY_SUBJOB_KEYS:
310
+ if sj.get(key) is not None:
311
+ seg.append(f"{label}={_num(sj[key])}")
312
+ if seg:
313
+ segs.append(" ".join(seg))
314
+ return segs
315
+
316
+
317
+ def format_job_config(job: Any) -> str:
318
+ """Static create-time config only (no status / reason) — for the line we
319
+ reserve to describe the job. Shows only whitelisted params that are present,
320
+ so it stays compact."""
321
+ if not isinstance(job, dict):
322
+ return ""
323
+ return " · ".join(_job_config_segments(job))
324
+
325
+
326
+ def format_job_summary(job: Any) -> str:
327
+ """One-line summary of a job's create-time config (whitelisted) + status.
328
+
329
+ Shows only the parameters the customer passed to create_job that are
330
+ present, so it stays compact. Failure/cancel ``reason`` is appended.
331
+ """
332
+ if not isinstance(job, dict):
333
+ return ""
334
+ status = (str(job.get("status", "")).strip() or "?").removeprefix("JOB_STATE_")
335
+ parts = [status, *_job_config_segments(job)]
336
+ if job.get("reason"):
337
+ parts.append(f"reason: {job['reason']}")
338
+ return " · ".join(p for p in parts if p)
339
+
340
+
341
+ # ─── Search / filter helpers ─────────────────────────────────────────────────
342
+
343
+
344
+ def job_matches(job: Any, query: str) -> bool:
345
+ """Case-insensitive match of query against a job's id, status, sub-job
346
+ types and model names. Empty query matches everything."""
347
+ if not query:
348
+ return True
349
+ if not isinstance(job, dict):
350
+ return False
351
+ hay = [str(job.get("job_id") or job.get("id") or ""), str(job.get("status") or "")]
352
+ for sj in job.get("sub_jobs") or []:
353
+ if isinstance(sj, dict):
354
+ hay.append(str(sj.get("job_type") or sj.get("type") or ""))
355
+ hay.append(str(sj.get("model_name") or ""))
356
+ return query.lower() in " ".join(hay).lower()
357
+
358
+
359
+ def entry_matches(entry: Any, query: str) -> bool:
360
+ """Whether a log entry's rendered text contains query (case-insensitive)."""
361
+ if not query:
362
+ return True
363
+ return query.lower() in format_log_entry(entry).lower()
364
+
365
+
366
+ _LEVELS = {
367
+ "DEBUG": 10,
368
+ "INFO": 20,
369
+ "WARNING": 30,
370
+ "WARN": 30,
371
+ "ERROR": 40,
372
+ "CRITICAL": 50,
373
+ "FATAL": 50,
374
+ }
375
+
376
+
377
+ def entry_at_level(entry: Any, min_level: Any) -> bool:
378
+ """Whether a structured entry is at or above min_level. Entries without a
379
+ parseable level (raw stdout lines) always pass, so the filter only trims
380
+ lower-severity *structured* logs and never hides raw output."""
381
+ if not min_level:
382
+ return True
383
+ floor = _LEVELS.get(str(min_level).upper())
384
+ if floor is None:
385
+ return True
386
+ lvl = entry.get("level") if isinstance(entry, dict) else None
387
+ if not lvl:
388
+ return True
389
+ return _LEVELS.get(str(lvl).upper(), 0) >= floor