euler-train 2.7.0__tar.gz → 2.8.0__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.
- {euler_train-2.7.0 → euler_train-2.8.0}/PKG-INFO +1 -1
- {euler_train-2.7.0 → euler_train-2.8.0}/README.md +38 -1
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/__init__.py +2 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/stream.py +196 -50
- euler_train-2.8.0/euler_train/stream_check_cli.py +104 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train.egg-info/PKG-INFO +1 -1
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train.egg-info/SOURCES.txt +2 -0
- euler_train-2.8.0/euler_train.egg-info/entry_points.txt +2 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/pyproject.toml +4 -1
- {euler_train-2.7.0 → euler_train-2.8.0}/tests/test_runlog.py +116 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/_dataset_contract.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/architecture.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/environment.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/git_info.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/outputs.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/run.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/serialization.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train/slurm.py +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train.egg-info/dependency_links.txt +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train.egg-info/requires.txt +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/euler_train.egg-info/top_level.txt +0 -0
- {euler_train-2.7.0 → euler_train-2.8.0}/setup.cfg +0 -0
|
@@ -75,7 +75,7 @@ What each field is for:
|
|
|
75
75
|
|
|
76
76
|
- `base_url`: Euler View server base URL, for example `https://view.example.com`.
|
|
77
77
|
- `model_id`: Euler View model that should receive this run.
|
|
78
|
-
- `api_token` or `access_token`: credential used to open a stream session. This is
|
|
78
|
+
- `api_token` or `access_token`: credential used to open a stream session. This is required whenever the package needs to negotiate its own short-lived ingest token, even if `stream_attach_token` is also provided.
|
|
79
79
|
- `stream_attach_token`: optional opaque launch-attachment token. When present, the producer sends it during the session handshake and Euler View attaches the stream to that launch directly. This is the preferred path for interactive or otherwise non-SLURM launches. Internally Euler View currently maps this token to `model_launches.id`, but the package treats it as an opaque token.
|
|
80
80
|
- `stream_token`: optional pre-issued ingest token. If you pass this, the package skips the session handshake entirely and posts events directly to the ingest endpoint.
|
|
81
81
|
- `datasource_id`: optional Euler View datasource hint for the run record.
|
|
@@ -91,6 +91,43 @@ How the handshake works:
|
|
|
91
91
|
5. Euler View returns a short-lived ingest token plus the canonical `streamAttachToken`; the package caches that returned token and reuses it on later session refreshes.
|
|
92
92
|
6. Subsequent events are sent to `/api/model-run-stream/ingest`.
|
|
93
93
|
|
|
94
|
+
`stream_attach_token` is only the attachment/correlation key. It does not authenticate the session request by itself. In the current implementation you still need `api_token` or `access_token` for the session handshake, unless you already have a pre-issued `stream_token`.
|
|
95
|
+
|
|
96
|
+
### Dry-run connectivity check
|
|
97
|
+
|
|
98
|
+
If you need to verify from a cluster node whether Euler View is reachable and whether the stream handshake would be accepted before starting a real run, the package exposes the same dry-run check used by the stream protocol:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
euler-train.stream.check \
|
|
102
|
+
--api-url https://view.example.com \
|
|
103
|
+
--model-id 42 \
|
|
104
|
+
--api-key "$EULER_VIEW_API_TOKEN" \
|
|
105
|
+
--stream-attach-token "$EULER_VIEW_STREAM_ATTACH_TOKEN"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This command calls:
|
|
109
|
+
|
|
110
|
+
- `POST /api/models/{model_id}/runs/{run_id}/stream/check`
|
|
111
|
+
|
|
112
|
+
It validates:
|
|
113
|
+
|
|
114
|
+
- network reachability to Euler View
|
|
115
|
+
- API token validity and scope
|
|
116
|
+
- model existence
|
|
117
|
+
- stream attachment resolution via `stream_attach_token`, when provided
|
|
118
|
+
- SLURM fallback matching via `--slurm-job-id`, when no attach token is provided
|
|
119
|
+
|
|
120
|
+
Useful flags:
|
|
121
|
+
|
|
122
|
+
- `--run-id`: override the dry-run run ID. If omitted, the CLI generates a temporary `stream-check-*` ID.
|
|
123
|
+
- `--stream-attach-token`: test the explicit launch attachment path.
|
|
124
|
+
- `--slurm-job-id`: test the fallback SLURM matching path.
|
|
125
|
+
- `--datasource-id`: include the datasource hint that a real stream session would send.
|
|
126
|
+
- `--euler-train-dir` and `--run-dir`: include path hints in the dry-run payload.
|
|
127
|
+
- `--json`: print the raw JSON response for automation.
|
|
128
|
+
|
|
129
|
+
The dry-run check does not start a run and does not mint an ingest token. It only verifies that the real session handshake would be accepted with the same request shape.
|
|
130
|
+
|
|
94
131
|
What has to be configured where:
|
|
95
132
|
|
|
96
133
|
- In the training script or job wrapper: pass `stream={...}` to `euler_train.init(...)`.
|
|
@@ -12,6 +12,7 @@ from .stream import (
|
|
|
12
12
|
OutputStream,
|
|
13
13
|
OutputStreamConsumer,
|
|
14
14
|
StreamContext,
|
|
15
|
+
check_stream_handshake,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
@@ -20,6 +21,7 @@ __all__ = [
|
|
|
20
21
|
"OutputStream",
|
|
21
22
|
"OutputStreamConsumer",
|
|
22
23
|
"StreamContext",
|
|
24
|
+
"check_stream_handshake",
|
|
23
25
|
"init",
|
|
24
26
|
"Run",
|
|
25
27
|
"export_architecture",
|
|
@@ -316,22 +316,17 @@ class EulerViewStreamConsumer:
|
|
|
316
316
|
f"{self.config.base_url}/api/models/{self.config.model_id}"
|
|
317
317
|
f"/runs/{quote(self._context.run_id, safe='')}/stream/session"
|
|
318
318
|
)
|
|
319
|
-
session_body
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
319
|
+
session_body = _build_stream_session_body(
|
|
320
|
+
run_dir=self.config.run_dir or str(self._context.run_dir),
|
|
321
|
+
euler_train_dir=self.config.euler_train_dir or str(self._context.runs_dir),
|
|
322
|
+
stream_attach_token=(
|
|
323
|
+
self.config.stream_attach_token
|
|
324
|
+
or self._resolved_stream_attach_token
|
|
325
|
+
),
|
|
326
|
+
slurm_job_id=_extract_slurm_job_id(self._context.meta),
|
|
327
|
+
datasource_id=self.config.datasource_id,
|
|
328
|
+
session_expires_in_sec=self.config.session_expires_in_sec,
|
|
325
329
|
)
|
|
326
|
-
if stream_attach_token is not None:
|
|
327
|
-
session_body["streamAttachToken"] = stream_attach_token
|
|
328
|
-
slurm_job_id = _extract_slurm_job_id(self._context.meta)
|
|
329
|
-
if slurm_job_id is not None:
|
|
330
|
-
session_body["slurmJobId"] = slurm_job_id
|
|
331
|
-
if self.config.datasource_id is not None:
|
|
332
|
-
session_body["datasourceId"] = self.config.datasource_id
|
|
333
|
-
if self.config.session_expires_in_sec is not None:
|
|
334
|
-
session_body["expiresInSec"] = self.config.session_expires_in_sec
|
|
335
330
|
|
|
336
331
|
try:
|
|
337
332
|
payload = self._post_json(
|
|
@@ -372,44 +367,13 @@ class EulerViewStreamConsumer:
|
|
|
372
367
|
*,
|
|
373
368
|
token: str | None,
|
|
374
369
|
) -> dict[str, Any]:
|
|
375
|
-
|
|
376
|
-
"Accept": "application/json",
|
|
377
|
-
"Content-Type": "application/json",
|
|
378
|
-
}
|
|
379
|
-
if token:
|
|
380
|
-
headers["Authorization"] = f"Bearer {token}"
|
|
381
|
-
|
|
382
|
-
request = Request(
|
|
370
|
+
return _post_json_request(
|
|
383
371
|
url,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
372
|
+
payload,
|
|
373
|
+
token=token,
|
|
374
|
+
timeout_sec=self.config.timeout_sec,
|
|
387
375
|
)
|
|
388
376
|
|
|
389
|
-
try:
|
|
390
|
-
with urlopen(request, timeout=self.config.timeout_sec) as response:
|
|
391
|
-
raw = response.read()
|
|
392
|
-
except HTTPError as exc:
|
|
393
|
-
message = _extract_http_error_message(exc)
|
|
394
|
-
raise RuntimeError(message) from exc
|
|
395
|
-
except URLError as exc:
|
|
396
|
-
reason = exc.reason if exc.reason else str(exc)
|
|
397
|
-
raise RuntimeError(str(reason)) from exc
|
|
398
|
-
except OSError as exc:
|
|
399
|
-
raise RuntimeError(str(exc)) from exc
|
|
400
|
-
|
|
401
|
-
if not raw:
|
|
402
|
-
return {}
|
|
403
|
-
|
|
404
|
-
try:
|
|
405
|
-
parsed = json.loads(raw.decode("utf-8"))
|
|
406
|
-
except json.JSONDecodeError as exc:
|
|
407
|
-
raise RuntimeError("Invalid JSON response from stream API") from exc
|
|
408
|
-
|
|
409
|
-
if isinstance(parsed, dict):
|
|
410
|
-
return parsed
|
|
411
|
-
raise RuntimeError("Unexpected stream API response payload")
|
|
412
|
-
|
|
413
377
|
def _trim_pending(self) -> None:
|
|
414
378
|
overflow = len(self._pending) - self.config.max_pending_events
|
|
415
379
|
if overflow <= 0:
|
|
@@ -459,6 +423,111 @@ def coerce_output_stream(spec: Any) -> OutputStream | None:
|
|
|
459
423
|
)
|
|
460
424
|
|
|
461
425
|
|
|
426
|
+
def check_stream_handshake(
|
|
427
|
+
config: EulerViewStreamConfig | Mapping[str, Any],
|
|
428
|
+
*,
|
|
429
|
+
run_id: str | None = None,
|
|
430
|
+
stream_attach_token: str | None = None,
|
|
431
|
+
slurm_job_id: int | None = None,
|
|
432
|
+
datasource_id: int | None = None,
|
|
433
|
+
euler_train_dir: str | None = None,
|
|
434
|
+
run_dir: str | None = None,
|
|
435
|
+
) -> dict[str, Any]:
|
|
436
|
+
if isinstance(config, Mapping):
|
|
437
|
+
resolved_config = _config_from_mapping(config)
|
|
438
|
+
elif isinstance(config, EulerViewStreamConfig):
|
|
439
|
+
resolved_config = config
|
|
440
|
+
else:
|
|
441
|
+
raise TypeError(
|
|
442
|
+
"config must be a mapping or EulerViewStreamConfig",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
if resolved_config.stream_token is not None:
|
|
446
|
+
raise ValueError(
|
|
447
|
+
"stream.stream_token bypasses the session handshake; a dry-run handshake requires api_token or access_token instead",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if resolved_config.model_id is None:
|
|
451
|
+
raise ValueError("stream.model_id is required for handshake checks")
|
|
452
|
+
|
|
453
|
+
normalized_run_id = (
|
|
454
|
+
_normalize_optional_text(run_id, field_name="run_id")
|
|
455
|
+
if run_id is not None
|
|
456
|
+
else _default_stream_check_run_id()
|
|
457
|
+
)
|
|
458
|
+
if normalized_run_id is None:
|
|
459
|
+
normalized_run_id = _default_stream_check_run_id()
|
|
460
|
+
normalized_stream_attach_token = (
|
|
461
|
+
_normalize_optional_text(
|
|
462
|
+
stream_attach_token,
|
|
463
|
+
field_name="stream_attach_token",
|
|
464
|
+
)
|
|
465
|
+
if stream_attach_token is not None
|
|
466
|
+
else None
|
|
467
|
+
)
|
|
468
|
+
if (
|
|
469
|
+
normalized_stream_attach_token is not None
|
|
470
|
+
and resolved_config.stream_attach_token is not None
|
|
471
|
+
and normalized_stream_attach_token != resolved_config.stream_attach_token
|
|
472
|
+
):
|
|
473
|
+
raise ValueError(
|
|
474
|
+
"stream_attach_token argument does not match stream.stream_attach_token",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
resolved_datasource_id = (
|
|
478
|
+
_require_positive_int(
|
|
479
|
+
datasource_id,
|
|
480
|
+
field_name="datasource_id",
|
|
481
|
+
allow_none=True,
|
|
482
|
+
)
|
|
483
|
+
if datasource_id is not None
|
|
484
|
+
else resolved_config.datasource_id
|
|
485
|
+
)
|
|
486
|
+
resolved_slurm_job_id = (
|
|
487
|
+
_require_positive_int(
|
|
488
|
+
slurm_job_id,
|
|
489
|
+
field_name="slurm_job_id",
|
|
490
|
+
allow_none=True,
|
|
491
|
+
)
|
|
492
|
+
if slurm_job_id is not None
|
|
493
|
+
else None
|
|
494
|
+
)
|
|
495
|
+
resolved_euler_train_dir = (
|
|
496
|
+
_normalize_optional_text(
|
|
497
|
+
euler_train_dir,
|
|
498
|
+
field_name="euler_train_dir",
|
|
499
|
+
)
|
|
500
|
+
if euler_train_dir is not None
|
|
501
|
+
else resolved_config.euler_train_dir
|
|
502
|
+
)
|
|
503
|
+
resolved_run_dir = (
|
|
504
|
+
_normalize_optional_text(run_dir, field_name="run_dir")
|
|
505
|
+
if run_dir is not None
|
|
506
|
+
else resolved_config.run_dir
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
url = (
|
|
510
|
+
f"{resolved_config.base_url}/api/models/{resolved_config.model_id}"
|
|
511
|
+
f"/runs/{quote(normalized_run_id, safe='')}/stream/check"
|
|
512
|
+
)
|
|
513
|
+
payload = _build_stream_session_body(
|
|
514
|
+
run_dir=resolved_run_dir,
|
|
515
|
+
euler_train_dir=resolved_euler_train_dir,
|
|
516
|
+
stream_attach_token=(
|
|
517
|
+
normalized_stream_attach_token or resolved_config.stream_attach_token
|
|
518
|
+
),
|
|
519
|
+
slurm_job_id=resolved_slurm_job_id,
|
|
520
|
+
datasource_id=resolved_datasource_id,
|
|
521
|
+
session_expires_in_sec=resolved_config.session_expires_in_sec,
|
|
522
|
+
)
|
|
523
|
+
return _post_json_request(
|
|
524
|
+
url,
|
|
525
|
+
payload,
|
|
526
|
+
token=resolved_config.api_token or resolved_config.access_token,
|
|
527
|
+
timeout_sec=resolved_config.timeout_sec,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
462
531
|
def _looks_like_consumer(value: Any) -> bool:
|
|
463
532
|
return all(
|
|
464
533
|
callable(getattr(value, name, None))
|
|
@@ -562,6 +631,36 @@ def _extract_session_stream_attach_token(payload: Mapping[str, Any]) -> str | No
|
|
|
562
631
|
return normalized or None
|
|
563
632
|
|
|
564
633
|
|
|
634
|
+
def _build_stream_session_body(
|
|
635
|
+
*,
|
|
636
|
+
run_dir: str | None,
|
|
637
|
+
euler_train_dir: str | None,
|
|
638
|
+
stream_attach_token: str | None,
|
|
639
|
+
slurm_job_id: int | None,
|
|
640
|
+
datasource_id: int | None,
|
|
641
|
+
session_expires_in_sec: int | None,
|
|
642
|
+
) -> dict[str, Any]:
|
|
643
|
+
body: dict[str, Any] = {}
|
|
644
|
+
if run_dir is not None:
|
|
645
|
+
body["runDir"] = run_dir
|
|
646
|
+
if euler_train_dir is not None:
|
|
647
|
+
body["eulerTrainDir"] = euler_train_dir
|
|
648
|
+
if stream_attach_token is not None:
|
|
649
|
+
body["streamAttachToken"] = stream_attach_token
|
|
650
|
+
if slurm_job_id is not None:
|
|
651
|
+
body["slurmJobId"] = slurm_job_id
|
|
652
|
+
if datasource_id is not None:
|
|
653
|
+
body["datasourceId"] = datasource_id
|
|
654
|
+
if session_expires_in_sec is not None:
|
|
655
|
+
body["expiresInSec"] = session_expires_in_sec
|
|
656
|
+
return body
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _default_stream_check_run_id() -> str:
|
|
660
|
+
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
|
|
661
|
+
return f"stream-check-{timestamp}"
|
|
662
|
+
|
|
663
|
+
|
|
565
664
|
def _normalize_optional_text(
|
|
566
665
|
value: Any,
|
|
567
666
|
*,
|
|
@@ -636,6 +735,52 @@ def _extract_http_error_message(error: HTTPError) -> str:
|
|
|
636
735
|
return f"HTTP {error.code}: {error.reason}"
|
|
637
736
|
|
|
638
737
|
|
|
738
|
+
def _post_json_request(
|
|
739
|
+
url: str,
|
|
740
|
+
payload: dict[str, Any],
|
|
741
|
+
*,
|
|
742
|
+
token: str | None,
|
|
743
|
+
timeout_sec: float,
|
|
744
|
+
) -> dict[str, Any]:
|
|
745
|
+
headers = {
|
|
746
|
+
"Accept": "application/json",
|
|
747
|
+
"Content-Type": "application/json",
|
|
748
|
+
}
|
|
749
|
+
if token:
|
|
750
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
751
|
+
|
|
752
|
+
request = Request(
|
|
753
|
+
url,
|
|
754
|
+
data=json.dumps(payload, default=_json_default).encode("utf-8"),
|
|
755
|
+
headers=headers,
|
|
756
|
+
method="POST",
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
with urlopen(request, timeout=timeout_sec) as response:
|
|
761
|
+
raw = response.read()
|
|
762
|
+
except HTTPError as exc:
|
|
763
|
+
message = _extract_http_error_message(exc)
|
|
764
|
+
raise RuntimeError(message) from exc
|
|
765
|
+
except URLError as exc:
|
|
766
|
+
reason = exc.reason if exc.reason else str(exc)
|
|
767
|
+
raise RuntimeError(str(reason)) from exc
|
|
768
|
+
except OSError as exc:
|
|
769
|
+
raise RuntimeError(str(exc)) from exc
|
|
770
|
+
|
|
771
|
+
if not raw:
|
|
772
|
+
return {}
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
parsed = json.loads(raw.decode("utf-8"))
|
|
776
|
+
except json.JSONDecodeError as exc:
|
|
777
|
+
raise RuntimeError("Invalid JSON response from stream API") from exc
|
|
778
|
+
|
|
779
|
+
if isinstance(parsed, dict):
|
|
780
|
+
return parsed
|
|
781
|
+
raise RuntimeError("Unexpected stream API response payload")
|
|
782
|
+
|
|
783
|
+
|
|
639
784
|
def _to_jsonable(value: Any) -> Any:
|
|
640
785
|
return json.loads(json.dumps(value, default=_json_default))
|
|
641
786
|
|
|
@@ -646,5 +791,6 @@ __all__ = [
|
|
|
646
791
|
"OutputStream",
|
|
647
792
|
"OutputStreamConsumer",
|
|
648
793
|
"StreamContext",
|
|
794
|
+
"check_stream_handshake",
|
|
649
795
|
"coerce_output_stream",
|
|
650
796
|
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from .stream import check_stream_handshake
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="euler-train.stream.check",
|
|
14
|
+
description="Dry-run the Euler View stream handshake",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("--api-url", required=True, help="Euler View base URL")
|
|
17
|
+
parser.add_argument("--model-id", required=True, type=int, help="Euler View model ID")
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--api-key",
|
|
20
|
+
required=True,
|
|
21
|
+
help="Euler View API token used to authorize the handshake check",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--run-id",
|
|
25
|
+
help="Run ID to use for the dry-run handshake. Defaults to an ephemeral stream-check ID.",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--stream-attach-token",
|
|
29
|
+
help="Optional explicit launch attachment token to validate",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--slurm-job-id",
|
|
33
|
+
type=int,
|
|
34
|
+
help="Optional SLURM job ID to test the fallback matching path",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--datasource-id",
|
|
38
|
+
type=int,
|
|
39
|
+
help="Optional datasource hint to include in the handshake payload",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--euler-train-dir",
|
|
43
|
+
help="Optional euler_train runs directory path to include in the handshake payload",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--run-dir",
|
|
47
|
+
help="Optional run directory path to include in the handshake payload",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--timeout-sec",
|
|
51
|
+
type=float,
|
|
52
|
+
default=10.0,
|
|
53
|
+
help="HTTP timeout in seconds (default: 10)",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--json",
|
|
57
|
+
action="store_true",
|
|
58
|
+
help="Print the raw JSON response instead of a human-readable summary",
|
|
59
|
+
)
|
|
60
|
+
return parser
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
64
|
+
parser = build_parser()
|
|
65
|
+
args = parser.parse_args(argv)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
result = check_stream_handshake(
|
|
69
|
+
{
|
|
70
|
+
"base_url": args.api_url,
|
|
71
|
+
"model_id": args.model_id,
|
|
72
|
+
"api_token": args.api_key,
|
|
73
|
+
"stream_attach_token": args.stream_attach_token,
|
|
74
|
+
"datasource_id": args.datasource_id,
|
|
75
|
+
"euler_train_dir": args.euler_train_dir,
|
|
76
|
+
"run_dir": args.run_dir,
|
|
77
|
+
"timeout_sec": args.timeout_sec,
|
|
78
|
+
},
|
|
79
|
+
run_id=args.run_id,
|
|
80
|
+
slurm_job_id=args.slurm_job_id,
|
|
81
|
+
)
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
if args.json:
|
|
84
|
+
print(json.dumps({"success": False, "error": str(exc)}, indent=2))
|
|
85
|
+
else:
|
|
86
|
+
print(f"Handshake check failed: {exc}", file=sys.stderr)
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
if args.json:
|
|
90
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
run = result.get("run", {})
|
|
94
|
+
print("Handshake check succeeded")
|
|
95
|
+
print(f" model_id: {run.get('modelId')}")
|
|
96
|
+
print(f" run_id: {run.get('runId')}")
|
|
97
|
+
print(f" resolution: {result.get('resolution')}")
|
|
98
|
+
print(f" stream_attach_token: {run.get('streamAttachToken')}")
|
|
99
|
+
print(f" ingest_url: {result.get('ingestUrl')}")
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
raise SystemExit(main())
|
|
@@ -10,9 +10,11 @@ euler_train/run.py
|
|
|
10
10
|
euler_train/serialization.py
|
|
11
11
|
euler_train/slurm.py
|
|
12
12
|
euler_train/stream.py
|
|
13
|
+
euler_train/stream_check_cli.py
|
|
13
14
|
euler_train.egg-info/PKG-INFO
|
|
14
15
|
euler_train.egg-info/SOURCES.txt
|
|
15
16
|
euler_train.egg-info/dependency_links.txt
|
|
17
|
+
euler_train.egg-info/entry_points.txt
|
|
16
18
|
euler_train.egg-info/requires.txt
|
|
17
19
|
euler_train.egg-info/top_level.txt
|
|
18
20
|
tests/test_runlog.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "euler-train"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.8.0"
|
|
8
8
|
description = "Lightweight file-based experiment logger for PyTorch"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = ["numpy", "euler-dataset-contract"]
|
|
@@ -16,5 +16,8 @@ architecture = ["onnx", "onnxruntime", "onnxsim"]
|
|
|
16
16
|
naming = ["euler-metric-naming"]
|
|
17
17
|
dev = ["pytest", "Pillow", "torch", "nvidia-ml-py", "onnx", "onnxruntime", "onnxsim"]
|
|
18
18
|
|
|
19
|
+
[project.scripts]
|
|
20
|
+
"euler-train.stream.check" = "euler_train.stream_check_cli:main"
|
|
21
|
+
|
|
19
22
|
[tool.pytest.ini_options]
|
|
20
23
|
pythonpath = ["."]
|
|
@@ -16,6 +16,7 @@ from PIL import Image
|
|
|
16
16
|
|
|
17
17
|
import euler_train
|
|
18
18
|
import euler_train.stream as stream_mod
|
|
19
|
+
from euler_train.stream_check_cli import main as stream_check_cli_main
|
|
19
20
|
|
|
20
21
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
21
22
|
# Helpers
|
|
@@ -2053,6 +2054,121 @@ class TestStreaming:
|
|
|
2053
2054
|
meta = _read_json(run.dir / "meta.json")
|
|
2054
2055
|
assert meta["status"] == "completed"
|
|
2055
2056
|
|
|
2057
|
+
def test_check_stream_handshake_uses_dry_run_route(
|
|
2058
|
+
self,
|
|
2059
|
+
monkeypatch,
|
|
2060
|
+
):
|
|
2061
|
+
calls: list[dict] = []
|
|
2062
|
+
|
|
2063
|
+
def fake_urlopen(request, timeout=0):
|
|
2064
|
+
headers = {k.lower(): v for k, v in request.header_items()}
|
|
2065
|
+
body = json.loads(request.data.decode("utf-8"))
|
|
2066
|
+
calls.append(
|
|
2067
|
+
{
|
|
2068
|
+
"url": request.full_url,
|
|
2069
|
+
"method": request.get_method(),
|
|
2070
|
+
"headers": headers,
|
|
2071
|
+
"body": body,
|
|
2072
|
+
"timeout": timeout,
|
|
2073
|
+
},
|
|
2074
|
+
)
|
|
2075
|
+
if request.full_url.endswith("/stream/check"):
|
|
2076
|
+
return _FakeHttpResponse(
|
|
2077
|
+
{
|
|
2078
|
+
"success": True,
|
|
2079
|
+
"resolution": "stream_attach_token",
|
|
2080
|
+
"ingestUrl": "https://sync.example/api/model-run-stream/ingest",
|
|
2081
|
+
"run": {
|
|
2082
|
+
"modelId": 42,
|
|
2083
|
+
"runId": "dry-run-1",
|
|
2084
|
+
"streamAttachToken": "attach-123",
|
|
2085
|
+
"launchId": "launch-abc",
|
|
2086
|
+
"datasourceId": 7,
|
|
2087
|
+
"eulerTrainDir": "/outputs",
|
|
2088
|
+
"runDir": "/outputs/dry-run-1",
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
)
|
|
2092
|
+
raise AssertionError(f"Unexpected URL {request.full_url}")
|
|
2093
|
+
|
|
2094
|
+
monkeypatch.setattr(stream_mod, "urlopen", fake_urlopen)
|
|
2095
|
+
|
|
2096
|
+
payload = stream_mod.check_stream_handshake(
|
|
2097
|
+
{
|
|
2098
|
+
"base_url": "https://sync.example",
|
|
2099
|
+
"model_id": 42,
|
|
2100
|
+
"api_token": "user-token",
|
|
2101
|
+
"stream_attach_token": "attach-123",
|
|
2102
|
+
"datasource_id": 7,
|
|
2103
|
+
},
|
|
2104
|
+
run_id="dry-run-1",
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
assert payload["success"] is True
|
|
2108
|
+
assert calls == [
|
|
2109
|
+
{
|
|
2110
|
+
"url": "https://sync.example/api/models/42/runs/dry-run-1/stream/check",
|
|
2111
|
+
"method": "POST",
|
|
2112
|
+
"headers": {
|
|
2113
|
+
"accept": "application/json",
|
|
2114
|
+
"authorization": "Bearer user-token",
|
|
2115
|
+
"content-type": "application/json",
|
|
2116
|
+
},
|
|
2117
|
+
"body": {
|
|
2118
|
+
"streamAttachToken": "attach-123",
|
|
2119
|
+
"datasourceId": 7,
|
|
2120
|
+
},
|
|
2121
|
+
"timeout": 10.0,
|
|
2122
|
+
},
|
|
2123
|
+
]
|
|
2124
|
+
|
|
2125
|
+
def test_stream_check_cli_prints_json_payload(
|
|
2126
|
+
self,
|
|
2127
|
+
monkeypatch,
|
|
2128
|
+
capsys,
|
|
2129
|
+
):
|
|
2130
|
+
def fake_urlopen(request, timeout=0):
|
|
2131
|
+
if not request.full_url.endswith("/stream/check"):
|
|
2132
|
+
raise AssertionError(f"Unexpected URL {request.full_url}")
|
|
2133
|
+
return _FakeHttpResponse(
|
|
2134
|
+
{
|
|
2135
|
+
"success": True,
|
|
2136
|
+
"resolution": "unresolved",
|
|
2137
|
+
"ingestUrl": "https://sync.example/api/model-run-stream/ingest",
|
|
2138
|
+
"run": {
|
|
2139
|
+
"modelId": 42,
|
|
2140
|
+
"runId": "dry-run-2",
|
|
2141
|
+
"streamAttachToken": None,
|
|
2142
|
+
"launchId": None,
|
|
2143
|
+
"datasourceId": None,
|
|
2144
|
+
"eulerTrainDir": None,
|
|
2145
|
+
"runDir": None,
|
|
2146
|
+
},
|
|
2147
|
+
},
|
|
2148
|
+
)
|
|
2149
|
+
|
|
2150
|
+
monkeypatch.setattr(stream_mod, "urlopen", fake_urlopen)
|
|
2151
|
+
|
|
2152
|
+
exit_code = stream_check_cli_main(
|
|
2153
|
+
[
|
|
2154
|
+
"--api-url",
|
|
2155
|
+
"https://sync.example",
|
|
2156
|
+
"--model-id",
|
|
2157
|
+
"42",
|
|
2158
|
+
"--api-key",
|
|
2159
|
+
"user-token",
|
|
2160
|
+
"--run-id",
|
|
2161
|
+
"dry-run-2",
|
|
2162
|
+
"--json",
|
|
2163
|
+
],
|
|
2164
|
+
)
|
|
2165
|
+
|
|
2166
|
+
assert exit_code == 0
|
|
2167
|
+
payload = json.loads(capsys.readouterr().out)
|
|
2168
|
+
assert payload["success"] is True
|
|
2169
|
+
assert payload["resolution"] == "unresolved"
|
|
2170
|
+
assert payload["run"]["runId"] == "dry-run-2"
|
|
2171
|
+
|
|
2056
2172
|
|
|
2057
2173
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
2058
2174
|
# lifecycle (finish, context manager, crash)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|