orchestra-cli 0.5.0__tar.gz → 0.5.1__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.
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/PKG-INFO +1 -1
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/run_pipeline.py +29 -4
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_run_pipeline.py +94 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/PKG-INFO +1 -1
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/pyproject.toml +1 -1
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/README.md +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/__init__.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/build_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/cli.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/create_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/delete_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/fetch_pipelines.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/get_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/import_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/list_accounts.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/login.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/login_page.html +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/migrate_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/pipeline_upsert.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/task_logs.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/update_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/use_account.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/src/validate_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/__init__.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/conftest.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_build_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_cli.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_create_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_delete_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_fetch_pipelines.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_get_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_import_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_list_accounts.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_login.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_migrate_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_pipeline_selector.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_task_logs.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_update_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_use_account.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_validate_pipeline.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/utils/__init__.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/utils/test_api.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/utils/test_git.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/utils/test_picker.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/accounts.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/api.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/constants.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/credentials.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/git.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/picker.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/pipeline_lookup.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/pipeline_selector.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/pipeline_update.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/styling.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/yaml_loader.py +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/SOURCES.txt +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/dependency_links.txt +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/entry_points.txt +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/requires.txt +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/top_level.txt +0 -0
- {orchestra_cli-0.5.0 → orchestra_cli-0.5.1}/setup.cfg +0 -0
|
@@ -37,13 +37,14 @@ STATUS_STYLES = {
|
|
|
37
37
|
"CREATED": "cyan",
|
|
38
38
|
"QUEUED": "yellow",
|
|
39
39
|
"RUNNING": "blue",
|
|
40
|
+
"CANCELLING": "red",
|
|
40
41
|
"SUCCEEDED": "green",
|
|
41
42
|
"WARNING": "yellow",
|
|
42
43
|
"SKIPPED": "yellow",
|
|
43
44
|
"FAILED": "red",
|
|
44
45
|
"CANCELLED": "red",
|
|
45
46
|
}
|
|
46
|
-
IN_PROGRESS_RUN_STATUSES = {"RUNNING", "QUEUED", "CREATED"}
|
|
47
|
+
IN_PROGRESS_RUN_STATUSES = {"RUNNING", "QUEUED", "CREATED", "CANCELLING"}
|
|
47
48
|
TERMINAL_RUN_STATUSES = {"SUCCEEDED", "WARNING", "SKIPPED", "FAILED", "CANCELLED"}
|
|
48
49
|
|
|
49
50
|
|
|
@@ -307,13 +308,23 @@ def _format_pipeline_duration(created_at_utc: datetime, now_utc: datetime | None
|
|
|
307
308
|
return f"{seconds} {unit}"
|
|
308
309
|
|
|
309
310
|
|
|
311
|
+
def _run_message(status_body: dict[str, object]) -> str | None:
|
|
312
|
+
message = status_body.get("message")
|
|
313
|
+
if isinstance(message, str) and message.strip():
|
|
314
|
+
return message.strip()
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
310
318
|
def _build_poll_status_text(
|
|
311
319
|
status_value: str,
|
|
312
320
|
created_at_utc: datetime | None = None,
|
|
313
321
|
now_utc: datetime | None = None,
|
|
322
|
+
status_message: str | None = None,
|
|
314
323
|
) -> Text:
|
|
315
324
|
text = Text("Pipeline status: ")
|
|
316
325
|
text.append(status_value, style=f"bold {STATUS_STYLES.get(status_value, 'white')}")
|
|
326
|
+
if status_message is not None:
|
|
327
|
+
text.append(f" - {status_message}")
|
|
317
328
|
if created_at_utc is not None:
|
|
318
329
|
text.append(f" ({_format_pipeline_duration(created_at_utc, now_utc)})", style="dim")
|
|
319
330
|
return text
|
|
@@ -419,10 +430,11 @@ def _build_poll_display(
|
|
|
419
430
|
task_runs: list[dict[str, object]],
|
|
420
431
|
can_fetch_task_runs: bool,
|
|
421
432
|
now_utc: datetime | None = None,
|
|
433
|
+
status_message: str | None = None,
|
|
422
434
|
) -> Group:
|
|
423
435
|
return Group(
|
|
424
436
|
_build_live_status_text(seconds_since_check),
|
|
425
|
-
_build_poll_status_text(status_value, created_at_utc, now_utc),
|
|
437
|
+
_build_poll_status_text(status_value, created_at_utc, now_utc, status_message),
|
|
426
438
|
_build_task_runs_table(
|
|
427
439
|
task_runs,
|
|
428
440
|
can_fetch_task_runs=can_fetch_task_runs,
|
|
@@ -452,6 +464,7 @@ def _sleep_with_status_updates(
|
|
|
452
464
|
created_at_utc: datetime | None,
|
|
453
465
|
task_runs: list[dict[str, object]],
|
|
454
466
|
can_fetch_task_runs: bool,
|
|
467
|
+
status_message: str | None = None,
|
|
455
468
|
sleep_fn: Callable[[float], None] | None = None,
|
|
456
469
|
) -> None:
|
|
457
470
|
sleep_fn = sleep_fn or time.sleep
|
|
@@ -468,6 +481,7 @@ def _sleep_with_status_updates(
|
|
|
468
481
|
created_at_utc=created_at_utc,
|
|
469
482
|
task_runs=task_runs,
|
|
470
483
|
can_fetch_task_runs=can_fetch_task_runs,
|
|
484
|
+
status_message=status_message,
|
|
471
485
|
),
|
|
472
486
|
)
|
|
473
487
|
|
|
@@ -489,6 +503,7 @@ def _poll_until_terminal(
|
|
|
489
503
|
status_url = get_api_url(f"pipeline_runs/{pipeline_run_id}/status")
|
|
490
504
|
poll_status_display = _create_poll_status_display()
|
|
491
505
|
last_status_value: str | None = None
|
|
506
|
+
last_status_message: str | None = None
|
|
492
507
|
last_task_runs: list[dict[str, object]] = []
|
|
493
508
|
can_fetch_task_runs = False
|
|
494
509
|
missing_run_status_polls = 0
|
|
@@ -502,6 +517,7 @@ def _poll_until_terminal(
|
|
|
502
517
|
created_at_utc=created_at_utc,
|
|
503
518
|
task_runs=last_task_runs,
|
|
504
519
|
can_fetch_task_runs=can_fetch_task_runs,
|
|
520
|
+
status_message=last_status_message,
|
|
505
521
|
)
|
|
506
522
|
try:
|
|
507
523
|
status_resp = client.get(status_url)
|
|
@@ -530,11 +546,13 @@ def _poll_until_terminal(
|
|
|
530
546
|
status_body = {}
|
|
531
547
|
|
|
532
548
|
status_value = status_body.get("runStatus")
|
|
549
|
+
run_message = _run_message(status_body)
|
|
533
550
|
created_at_utc = created_at_utc or _parse_created_at_utc(status_body)
|
|
534
551
|
|
|
535
552
|
if isinstance(status_value, str):
|
|
536
553
|
missing_run_status_polls = 0
|
|
537
554
|
last_status_value = status_value
|
|
555
|
+
last_status_message = run_message if status_value == "QUEUED" else None
|
|
538
556
|
can_fetch_task_runs = status_value in (
|
|
539
557
|
IN_PROGRESS_RUN_STATUSES | TERMINAL_RUN_STATUSES
|
|
540
558
|
)
|
|
@@ -548,10 +566,14 @@ def _poll_until_terminal(
|
|
|
548
566
|
created_at_utc=created_at_utc,
|
|
549
567
|
task_runs=last_task_runs,
|
|
550
568
|
can_fetch_task_runs=can_fetch_task_runs,
|
|
569
|
+
status_message=last_status_message,
|
|
551
570
|
),
|
|
552
571
|
)
|
|
553
572
|
else:
|
|
554
|
-
|
|
573
|
+
status_line = f"Pipeline ({selector_name}) status: {status_value}"
|
|
574
|
+
if last_status_message is not None:
|
|
575
|
+
status_line += f" - {last_status_message}"
|
|
576
|
+
typer.echo(status_line)
|
|
555
577
|
else:
|
|
556
578
|
missing_run_status_polls += 1
|
|
557
579
|
if missing_run_status_polls <= max_missing_run_status_polls:
|
|
@@ -572,7 +594,10 @@ def _poll_until_terminal(
|
|
|
572
594
|
if status_value == "SKIPPED":
|
|
573
595
|
_stop_poll_status_display(poll_status_display)
|
|
574
596
|
poll_status_display = None
|
|
575
|
-
|
|
597
|
+
skipped_text = (
|
|
598
|
+
f"⚠ Pipeline skipped: {run_message}" if run_message else "⚠ Pipeline skipped"
|
|
599
|
+
)
|
|
600
|
+
typer.echo(yellow(skipped_text))
|
|
576
601
|
raise typer.Exit(code=0)
|
|
577
602
|
|
|
578
603
|
if status_value in {"FAILED", "CANCELLED"}:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import time
|
|
1
3
|
from datetime import UTC, datetime
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
|
|
@@ -96,6 +98,11 @@ def test_build_poll_status_text_includes_duration() -> None:
|
|
|
96
98
|
assert text.plain == "Pipeline status: RUNNING (1:30 minutes)"
|
|
97
99
|
|
|
98
100
|
|
|
101
|
+
def test_build_poll_status_text_includes_status_message() -> None:
|
|
102
|
+
text = _build_poll_status_text("QUEUED", status_message="Waiting for a free slot")
|
|
103
|
+
assert text.plain == "Pipeline status: QUEUED - Waiting for a free slot"
|
|
104
|
+
|
|
105
|
+
|
|
99
106
|
def test_build_live_status_text_includes_update_age() -> None:
|
|
100
107
|
text = _build_live_status_text(3)
|
|
101
108
|
assert text.plain == "Live status (updated 3 seconds ago)"
|
|
@@ -1094,6 +1101,93 @@ def test_run_wait_warning(httpx_mock: HTTPXMock, monkeypatch, tmp_path: Path):
|
|
|
1094
1101
|
assert result.output.strip().splitlines()[-1] == "⚠ Pipeline completed with warnings"
|
|
1095
1102
|
|
|
1096
1103
|
|
|
1104
|
+
def _invoke_run_through_statuses(
|
|
1105
|
+
httpx_mock: HTTPXMock,
|
|
1106
|
+
monkeypatch,
|
|
1107
|
+
tmp_path: Path,
|
|
1108
|
+
status_bodies: list[dict[str, str]],
|
|
1109
|
+
):
|
|
1110
|
+
mapping = {
|
|
1111
|
+
("rev-parse", "--show-toplevel"): (0, str(tmp_path), ""),
|
|
1112
|
+
("status", "--porcelain"): (0, "", ""),
|
|
1113
|
+
("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"): (1, "", ""),
|
|
1114
|
+
}
|
|
1115
|
+
monkeypatch.setattr(time, "sleep", lambda _: None)
|
|
1116
|
+
monkeypatch.setattr(subprocess, "run", make_git_subprocess_mock(mapping))
|
|
1117
|
+
|
|
1118
|
+
base = "https://app.getorchestra.io/api/engine/public"
|
|
1119
|
+
httpx_mock.add_response(
|
|
1120
|
+
method="POST",
|
|
1121
|
+
url=f"{base}/pipelines/demo/start",
|
|
1122
|
+
json={"pipelineRunId": "run-q"},
|
|
1123
|
+
)
|
|
1124
|
+
for status_body in status_bodies:
|
|
1125
|
+
httpx_mock.add_response(
|
|
1126
|
+
method="GET",
|
|
1127
|
+
url=f"{base}/pipeline_runs/run-q/status",
|
|
1128
|
+
json={**status_body, "pipelineName": "demo"},
|
|
1129
|
+
)
|
|
1130
|
+
httpx_mock.add_response(
|
|
1131
|
+
method="GET",
|
|
1132
|
+
url=f"{base}/pipeline_runs/run-q/task_runs?page_size=50&page=1",
|
|
1133
|
+
json={"results": []},
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
return runner.invoke(app, ["pipeline", "run", "--alias", "demo", "--wait"])
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def test_run_wait_keeps_polling_while_cancelling(
|
|
1140
|
+
httpx_mock: HTTPXMock,
|
|
1141
|
+
monkeypatch,
|
|
1142
|
+
tmp_path: Path,
|
|
1143
|
+
):
|
|
1144
|
+
result = _invoke_run_through_statuses(
|
|
1145
|
+
httpx_mock,
|
|
1146
|
+
monkeypatch,
|
|
1147
|
+
tmp_path,
|
|
1148
|
+
[{"runStatus": "QUEUED"}, {"runStatus": "CANCELLING"}, {"runStatus": "CANCELLED"}],
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
assert result.exit_code == 1
|
|
1152
|
+
assert "Invalid status value" not in result.output
|
|
1153
|
+
assert "status: CANCELLING" in result.output
|
|
1154
|
+
assert "status CANCELLED" in result.output
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def test_run_wait_shows_queued_message_then_final_status(
|
|
1158
|
+
httpx_mock: HTTPXMock,
|
|
1159
|
+
monkeypatch,
|
|
1160
|
+
tmp_path: Path,
|
|
1161
|
+
):
|
|
1162
|
+
result = _invoke_run_through_statuses(
|
|
1163
|
+
httpx_mock,
|
|
1164
|
+
monkeypatch,
|
|
1165
|
+
tmp_path,
|
|
1166
|
+
[
|
|
1167
|
+
{"runStatus": "QUEUED", "message": "Waiting for a free slot"},
|
|
1168
|
+
{"runStatus": "RUNNING", "message": "Waiting for a free slot"},
|
|
1169
|
+
{"runStatus": "SUCCEEDED"},
|
|
1170
|
+
],
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
assert result.exit_code == 0
|
|
1174
|
+
assert "Pipeline (alias: demo) status: QUEUED - Waiting for a free slot" in result.output
|
|
1175
|
+
assert "Pipeline (alias: demo) status: RUNNING\n" in result.output
|
|
1176
|
+
assert result.output.strip().splitlines()[-1] == "✅ Pipeline succeeded"
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def test_run_wait_skipped_prints_reason(httpx_mock: HTTPXMock, monkeypatch, tmp_path: Path):
|
|
1180
|
+
result = _invoke_run_through_statuses(
|
|
1181
|
+
httpx_mock,
|
|
1182
|
+
monkeypatch,
|
|
1183
|
+
tmp_path,
|
|
1184
|
+
[{"runStatus": "SKIPPED", "message": "Replaced by a newer run"}],
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
assert result.exit_code == 0
|
|
1188
|
+
assert result.output.strip().splitlines()[-1] == "⚠ Pipeline skipped: Replaced by a newer run"
|
|
1189
|
+
|
|
1190
|
+
|
|
1097
1191
|
def test_run_git_backed_path_uses_prepared_branch_and_commit(
|
|
1098
1192
|
httpx_mock: HTTPXMock,
|
|
1099
1193
|
monkeypatch,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|