orchestra-cli 0.1.2__tar.gz → 0.2.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.
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/PKG-INFO +63 -3
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/README.md +62 -2
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/src/cli.py +4 -0
- orchestra_cli-0.2.0/orchestra_cli/src/create_pipeline.py +65 -0
- orchestra_cli-0.2.0/orchestra_cli/src/pipeline_upsert.py +74 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/src/run_pipeline.py +4 -0
- orchestra_cli-0.2.0/orchestra_cli/src/update_pipeline.py +65 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/tests/test_cli.py +2 -0
- orchestra_cli-0.2.0/orchestra_cli/tests/test_create_pipeline.py +177 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/tests/test_run_pipeline.py +2 -0
- orchestra_cli-0.2.0/orchestra_cli/tests/test_update_pipeline.py +176 -0
- orchestra_cli-0.2.0/orchestra_cli/utils/constants.py +28 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli.egg-info/PKG-INFO +63 -3
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli.egg-info/SOURCES.txt +5 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/pyproject.toml +1 -1
- orchestra_cli-0.1.2/orchestra_cli/utils/constants.py +0 -7
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/src/__init__.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/src/import_pipeline.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/src/validate_pipeline.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/tests/__init__.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/tests/conftest.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/tests/test_import_pipeline.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/tests/test_validate_pipeline.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/utils/git.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli/utils/styling.py +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli.egg-info/dependency_links.txt +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli.egg-info/entry_points.txt +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli.egg-info/requires.txt +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/orchestra_cli.egg-info/top_level.txt +0 -0
- {orchestra_cli-0.1.2 → orchestra_cli-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orchestra-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Orchestra CLI. Perform operations with Orchestra locally.
|
|
5
5
|
Author-email: Orchestra Technologies <support@getorchestra.io>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -15,6 +15,8 @@ Orchestra CLI for working with Orchestra pipelines from your terminal.
|
|
|
15
15
|
|
|
16
16
|
Two entrypoints are available: `orchestra` and `orchestra-cli` (they are equivalent).
|
|
17
17
|
|
|
18
|
+
See [`AGENTS.md`](AGENTS.md) for contributor and AI agent guidance (environment setup, code conventions, testing).
|
|
19
|
+
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
20
22
|
```bash
|
|
@@ -30,13 +32,14 @@ pipx install orchestra-cli
|
|
|
30
32
|
## Environment variables
|
|
31
33
|
|
|
32
34
|
- `ORCHESTRA_API_KEY`: Required for actions that call the API (`import`, `run`).
|
|
33
|
-
- `BASE_URL`: Optional. Override the default
|
|
34
|
-
(`https://app.getorchestra.io/api/engine/public/pipelines/{}`) for non‑production/testing.
|
|
35
|
+
- `BASE_URL`: Optional. Override the default Orchestra host (`https://app.getorchestra.io`) for non‑production/testing.
|
|
35
36
|
|
|
36
37
|
## Commands overview
|
|
37
38
|
|
|
38
39
|
- `validate`: Validate a pipeline YAML locally against the Orchestra API schema.
|
|
39
40
|
- `import`: Register a pipeline YAML (from a git repo) with Orchestra under an alias.
|
|
41
|
+
- `create-pipeline`: Create an Orchestra-backed pipeline from a local YAML file.
|
|
42
|
+
- `update-pipeline`: Update an existing Orchestra-backed pipeline from a local YAML file.
|
|
40
43
|
- `run`: Start a pipeline run by alias, optionally pinning branch/commit and waiting for completion.
|
|
41
44
|
|
|
42
45
|
Use `orchestra --help` or `orchestra <command> --help` for built-in help.
|
|
@@ -118,6 +121,63 @@ Common errors
|
|
|
118
121
|
|
|
119
122
|
---
|
|
120
123
|
|
|
124
|
+
## create-pipeline
|
|
125
|
+
|
|
126
|
+
Create an Orchestra-backed pipeline directly from a local YAML file.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
export ORCHESTRA_API_KEY=...
|
|
130
|
+
|
|
131
|
+
orchestra create-pipeline \
|
|
132
|
+
--alias my-pipeline \
|
|
133
|
+
--path ./pipelines/pipeline.yaml \
|
|
134
|
+
--publish # optional, defaults to --no-publish
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Options
|
|
138
|
+
|
|
139
|
+
- `-a, --alias` (required): Pipeline alias.
|
|
140
|
+
- `-p, --path` (required): Path to pipeline YAML file.
|
|
141
|
+
- `--publish/--no-publish` (optional, default `--no-publish`): Whether the pipeline is published.
|
|
142
|
+
|
|
143
|
+
Behavior
|
|
144
|
+
|
|
145
|
+
- Validates YAML against the Orchestra schema endpoint before creating.
|
|
146
|
+
- Sends pipeline data to `POST /pipelines` with `storage_provider=ORCHESTRA`.
|
|
147
|
+
- On success, prints the pipeline edit URL (`/pipelines/{pipeline_id}/edit`) when an ID is returned.
|
|
148
|
+
- Exit codes: `0` on success, `1` on failure.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## update-pipeline
|
|
153
|
+
|
|
154
|
+
Update an existing Orchestra-backed pipeline from a local YAML file.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
export ORCHESTRA_API_KEY=...
|
|
158
|
+
|
|
159
|
+
orchestra update-pipeline \
|
|
160
|
+
--alias my-pipeline \
|
|
161
|
+
--path ./pipelines/pipeline.yaml \
|
|
162
|
+
--no-publish # default
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Options
|
|
166
|
+
|
|
167
|
+
- `-a, --alias` (required): Pipeline alias to update.
|
|
168
|
+
- `-p, --path` (required): Path to pipeline YAML file.
|
|
169
|
+
- `--publish/--no-publish` (optional, default `--no-publish`): Whether the pipeline is published.
|
|
170
|
+
|
|
171
|
+
Behavior
|
|
172
|
+
|
|
173
|
+
- Validates YAML against the Orchestra schema endpoint before updating.
|
|
174
|
+
- Sends pipeline data to `PUT /pipelines/{alias}` with `storage_provider=ORCHESTRA`.
|
|
175
|
+
- Only Orchestra-backed pipelines can be updated via this endpoint (Git-backed pipelines are rejected).
|
|
176
|
+
- On success, prints the pipeline edit URL (`/pipelines/{pipeline_id}/edit`) when an ID is returned.
|
|
177
|
+
- Exit codes: `0` on success, `1` on failure.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
121
181
|
## run
|
|
122
182
|
|
|
123
183
|
Start a pipeline run by alias. Optionally specify a branch and/or commit. By default, the command waits and polls the run status until completion.
|
|
@@ -4,6 +4,8 @@ Orchestra CLI for working with Orchestra pipelines from your terminal.
|
|
|
4
4
|
|
|
5
5
|
Two entrypoints are available: `orchestra` and `orchestra-cli` (they are equivalent).
|
|
6
6
|
|
|
7
|
+
See [`AGENTS.md`](AGENTS.md) for contributor and AI agent guidance (environment setup, code conventions, testing).
|
|
8
|
+
|
|
7
9
|
## Installation
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -19,13 +21,14 @@ pipx install orchestra-cli
|
|
|
19
21
|
## Environment variables
|
|
20
22
|
|
|
21
23
|
- `ORCHESTRA_API_KEY`: Required for actions that call the API (`import`, `run`).
|
|
22
|
-
- `BASE_URL`: Optional. Override the default
|
|
23
|
-
(`https://app.getorchestra.io/api/engine/public/pipelines/{}`) for non‑production/testing.
|
|
24
|
+
- `BASE_URL`: Optional. Override the default Orchestra host (`https://app.getorchestra.io`) for non‑production/testing.
|
|
24
25
|
|
|
25
26
|
## Commands overview
|
|
26
27
|
|
|
27
28
|
- `validate`: Validate a pipeline YAML locally against the Orchestra API schema.
|
|
28
29
|
- `import`: Register a pipeline YAML (from a git repo) with Orchestra under an alias.
|
|
30
|
+
- `create-pipeline`: Create an Orchestra-backed pipeline from a local YAML file.
|
|
31
|
+
- `update-pipeline`: Update an existing Orchestra-backed pipeline from a local YAML file.
|
|
29
32
|
- `run`: Start a pipeline run by alias, optionally pinning branch/commit and waiting for completion.
|
|
30
33
|
|
|
31
34
|
Use `orchestra --help` or `orchestra <command> --help` for built-in help.
|
|
@@ -107,6 +110,63 @@ Common errors
|
|
|
107
110
|
|
|
108
111
|
---
|
|
109
112
|
|
|
113
|
+
## create-pipeline
|
|
114
|
+
|
|
115
|
+
Create an Orchestra-backed pipeline directly from a local YAML file.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
export ORCHESTRA_API_KEY=...
|
|
119
|
+
|
|
120
|
+
orchestra create-pipeline \
|
|
121
|
+
--alias my-pipeline \
|
|
122
|
+
--path ./pipelines/pipeline.yaml \
|
|
123
|
+
--publish # optional, defaults to --no-publish
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Options
|
|
127
|
+
|
|
128
|
+
- `-a, --alias` (required): Pipeline alias.
|
|
129
|
+
- `-p, --path` (required): Path to pipeline YAML file.
|
|
130
|
+
- `--publish/--no-publish` (optional, default `--no-publish`): Whether the pipeline is published.
|
|
131
|
+
|
|
132
|
+
Behavior
|
|
133
|
+
|
|
134
|
+
- Validates YAML against the Orchestra schema endpoint before creating.
|
|
135
|
+
- Sends pipeline data to `POST /pipelines` with `storage_provider=ORCHESTRA`.
|
|
136
|
+
- On success, prints the pipeline edit URL (`/pipelines/{pipeline_id}/edit`) when an ID is returned.
|
|
137
|
+
- Exit codes: `0` on success, `1` on failure.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## update-pipeline
|
|
142
|
+
|
|
143
|
+
Update an existing Orchestra-backed pipeline from a local YAML file.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
export ORCHESTRA_API_KEY=...
|
|
147
|
+
|
|
148
|
+
orchestra update-pipeline \
|
|
149
|
+
--alias my-pipeline \
|
|
150
|
+
--path ./pipelines/pipeline.yaml \
|
|
151
|
+
--no-publish # default
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Options
|
|
155
|
+
|
|
156
|
+
- `-a, --alias` (required): Pipeline alias to update.
|
|
157
|
+
- `-p, --path` (required): Path to pipeline YAML file.
|
|
158
|
+
- `--publish/--no-publish` (optional, default `--no-publish`): Whether the pipeline is published.
|
|
159
|
+
|
|
160
|
+
Behavior
|
|
161
|
+
|
|
162
|
+
- Validates YAML against the Orchestra schema endpoint before updating.
|
|
163
|
+
- Sends pipeline data to `PUT /pipelines/{alias}` with `storage_provider=ORCHESTRA`.
|
|
164
|
+
- Only Orchestra-backed pipelines can be updated via this endpoint (Git-backed pipelines are rejected).
|
|
165
|
+
- On success, prints the pipeline edit URL (`/pipelines/{pipeline_id}/edit`) when an ID is returned.
|
|
166
|
+
- Exit codes: `0` on success, `1` on failure.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
110
170
|
## run
|
|
111
171
|
|
|
112
172
|
Start a pipeline run by alias. Optionally specify a branch and/or commit. By default, the command waits and polls the run status until completion.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
|
|
3
|
+
from .create_pipeline import create_pipeline
|
|
3
4
|
from .import_pipeline import import_pipeline
|
|
4
5
|
from .run_pipeline import run_pipeline
|
|
6
|
+
from .update_pipeline import update_pipeline
|
|
5
7
|
from .validate_pipeline import validate
|
|
6
8
|
|
|
7
9
|
app = typer.Typer(help="Orchestra CLI – perform operations with Orchestra locally.")
|
|
@@ -10,3 +12,5 @@ app = typer.Typer(help="Orchestra CLI – perform operations with Orchestra loca
|
|
|
10
12
|
app.command(name="validate")(validate)
|
|
11
13
|
app.command(name="import")(import_pipeline)
|
|
12
14
|
app.command(name="run")(run_pipeline)
|
|
15
|
+
app.command(name="create-pipeline")(create_pipeline)
|
|
16
|
+
app.command(name="update-pipeline")(update_pipeline)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..utils.constants import get_create_pipeline_url
|
|
8
|
+
from ..utils.styling import indent_message, red, yellow
|
|
9
|
+
from .pipeline_upsert import (
|
|
10
|
+
build_upsert_payload,
|
|
11
|
+
emit_success_with_edit_url,
|
|
12
|
+
load_validated_pipeline_data,
|
|
13
|
+
require_api_key,
|
|
14
|
+
require_pipeline_id_from_success_response,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_pipeline(
|
|
19
|
+
alias: str = typer.Option(..., "--alias", "-a", help="Pipeline alias"),
|
|
20
|
+
path: Path = typer.Option(
|
|
21
|
+
...,
|
|
22
|
+
"--path",
|
|
23
|
+
"-p",
|
|
24
|
+
exists=True,
|
|
25
|
+
file_okay=True,
|
|
26
|
+
dir_okay=False,
|
|
27
|
+
readable=True,
|
|
28
|
+
resolve_path=True,
|
|
29
|
+
help="Path to pipeline YAML",
|
|
30
|
+
),
|
|
31
|
+
publish: bool = typer.Option(
|
|
32
|
+
False,
|
|
33
|
+
"--publish/--no-publish",
|
|
34
|
+
help="Whether the pipeline is published and can be triggered",
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Create an Orchestra-backed pipeline from a local YAML file.
|
|
39
|
+
"""
|
|
40
|
+
api_key = require_api_key()
|
|
41
|
+
data = load_validated_pipeline_data(path)
|
|
42
|
+
payload = build_upsert_payload(data, publish, alias=alias)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
response = httpx.post(
|
|
46
|
+
get_create_pipeline_url(),
|
|
47
|
+
json=payload,
|
|
48
|
+
timeout=30,
|
|
49
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
typer.echo(red(f"HTTP request failed: {e}"))
|
|
53
|
+
raise typer.Exit(code=1)
|
|
54
|
+
|
|
55
|
+
if response.status_code == 201:
|
|
56
|
+
pipeline_id = require_pipeline_id_from_success_response(response, "Create")
|
|
57
|
+
emit_success_with_edit_url(alias, "created", pipeline_id)
|
|
58
|
+
raise typer.Exit(code=0)
|
|
59
|
+
|
|
60
|
+
typer.echo(red(f"❌ Create failed with status {response.status_code}"))
|
|
61
|
+
try:
|
|
62
|
+
typer.echo(yellow(indent_message(json.dumps(response.json(), indent=2))))
|
|
63
|
+
except Exception:
|
|
64
|
+
typer.echo(yellow(indent_message(response.text)))
|
|
65
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..utils.constants import get_pipeline_edit_url
|
|
9
|
+
from ..utils.styling import green, indent_message, red, yellow
|
|
10
|
+
from .import_pipeline import _load_yaml, _validate_yaml_with_api
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def require_api_key() -> str:
|
|
14
|
+
api_key = os.getenv("ORCHESTRA_API_KEY")
|
|
15
|
+
if not api_key:
|
|
16
|
+
typer.echo(red("ORCHESTRA_API_KEY is not set"))
|
|
17
|
+
raise typer.Exit(code=1)
|
|
18
|
+
return api_key
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_validated_pipeline_data(path: Path) -> dict:
|
|
22
|
+
if not path.exists():
|
|
23
|
+
typer.echo(red(f"File not found: {path}"))
|
|
24
|
+
raise typer.Exit(code=1)
|
|
25
|
+
|
|
26
|
+
data, err = _load_yaml(path)
|
|
27
|
+
if err is not None:
|
|
28
|
+
typer.echo(red(f"Invalid YAML: {err}"))
|
|
29
|
+
raise typer.Exit(code=1)
|
|
30
|
+
|
|
31
|
+
ok, err_msg = _validate_yaml_with_api(data or {})
|
|
32
|
+
if not ok:
|
|
33
|
+
typer.echo(red("❌ Validation failed"))
|
|
34
|
+
if err_msg:
|
|
35
|
+
typer.echo(yellow(indent_message(err_msg)))
|
|
36
|
+
raise typer.Exit(code=1)
|
|
37
|
+
|
|
38
|
+
return data or {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_upsert_payload(data: dict, publish: bool, alias: str | None = None) -> dict:
|
|
42
|
+
payload: dict[str, object] = {
|
|
43
|
+
"data": data,
|
|
44
|
+
"published": publish,
|
|
45
|
+
"storage_provider": "ORCHESTRA",
|
|
46
|
+
}
|
|
47
|
+
if alias is not None:
|
|
48
|
+
payload["alias"] = alias
|
|
49
|
+
return payload
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def require_pipeline_id_from_success_response(
|
|
53
|
+
response: httpx.Response,
|
|
54
|
+
action: str,
|
|
55
|
+
) -> str:
|
|
56
|
+
try:
|
|
57
|
+
body = response.json()
|
|
58
|
+
except Exception:
|
|
59
|
+
typer.echo(red(f"❌ {action} failed: success response was not valid JSON"))
|
|
60
|
+
typer.echo(yellow(indent_message(response.text)))
|
|
61
|
+
raise typer.Exit(code=1)
|
|
62
|
+
|
|
63
|
+
pipeline_id = body.get("id")
|
|
64
|
+
if not pipeline_id:
|
|
65
|
+
typer.echo(red(f"❌ {action} failed: success response did not include pipeline id"))
|
|
66
|
+
typer.echo(yellow(indent_message(json.dumps(body, indent=2))))
|
|
67
|
+
raise typer.Exit(code=1)
|
|
68
|
+
|
|
69
|
+
return str(pipeline_id)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def emit_success_with_edit_url(alias: str, action: str, pipeline_id: str) -> None:
|
|
73
|
+
typer.echo(green(f"✅ Pipeline '{alias}' {action} successfully: {pipeline_id}"))
|
|
74
|
+
typer.echo(yellow(f"Edit URL: {get_pipeline_edit_url(pipeline_id)}"))
|
|
@@ -106,6 +106,7 @@ def run_pipeline(
|
|
|
106
106
|
poll_interval_seconds = 5
|
|
107
107
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
108
108
|
status_url = f"{api_prefix}/pipeline_runs/{pipeline_run_id}/status"
|
|
109
|
+
in_progress_statuses = {"RUNNING", "QUEUED", "CREATED"}
|
|
109
110
|
|
|
110
111
|
while True:
|
|
111
112
|
time.sleep(poll_interval_seconds)
|
|
@@ -158,6 +159,9 @@ def run_pipeline(
|
|
|
158
159
|
typer.echo(yellow(lineage_url))
|
|
159
160
|
raise typer.Exit(code=1)
|
|
160
161
|
|
|
162
|
+
if status_value in in_progress_statuses:
|
|
163
|
+
continue
|
|
164
|
+
|
|
161
165
|
typer.echo(
|
|
162
166
|
red(f"❌ Invalid status value: {status_value}\nResponse body: {status_body}"),
|
|
163
167
|
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..utils.constants import get_update_pipeline_url
|
|
8
|
+
from ..utils.styling import indent_message, red, yellow
|
|
9
|
+
from .pipeline_upsert import (
|
|
10
|
+
build_upsert_payload,
|
|
11
|
+
emit_success_with_edit_url,
|
|
12
|
+
load_validated_pipeline_data,
|
|
13
|
+
require_api_key,
|
|
14
|
+
require_pipeline_id_from_success_response,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def update_pipeline(
|
|
19
|
+
alias: str = typer.Option(..., "--alias", "-a", help="Pipeline alias"),
|
|
20
|
+
path: Path = typer.Option(
|
|
21
|
+
...,
|
|
22
|
+
"--path",
|
|
23
|
+
"-p",
|
|
24
|
+
exists=True,
|
|
25
|
+
file_okay=True,
|
|
26
|
+
dir_okay=False,
|
|
27
|
+
readable=True,
|
|
28
|
+
resolve_path=True,
|
|
29
|
+
help="Path to pipeline YAML",
|
|
30
|
+
),
|
|
31
|
+
publish: bool = typer.Option(
|
|
32
|
+
False,
|
|
33
|
+
"--publish/--no-publish",
|
|
34
|
+
help="Whether the pipeline is published and can be triggered",
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Update an Orchestra-backed pipeline from a local YAML file.
|
|
39
|
+
"""
|
|
40
|
+
api_key = require_api_key()
|
|
41
|
+
data = load_validated_pipeline_data(path)
|
|
42
|
+
payload = build_upsert_payload(data, publish)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
response = httpx.put(
|
|
46
|
+
get_update_pipeline_url(alias),
|
|
47
|
+
json=payload,
|
|
48
|
+
timeout=30,
|
|
49
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
typer.echo(red(f"HTTP request failed: {e}"))
|
|
53
|
+
raise typer.Exit(code=1)
|
|
54
|
+
|
|
55
|
+
if response.status_code == 200:
|
|
56
|
+
pipeline_id = require_pipeline_id_from_success_response(response, "Update")
|
|
57
|
+
emit_success_with_edit_url(alias, "updated", pipeline_id)
|
|
58
|
+
raise typer.Exit(code=0)
|
|
59
|
+
|
|
60
|
+
typer.echo(red(f"❌ Update failed with status {response.status_code}"))
|
|
61
|
+
try:
|
|
62
|
+
typer.echo(yellow(indent_message(json.dumps(response.json(), indent=2))))
|
|
63
|
+
except Exception:
|
|
64
|
+
typer.echo(yellow(indent_message(response.text)))
|
|
65
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pytest_httpx import HTTPXMock
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from orchestra_cli.src.cli import app
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def mock_env(monkeypatch):
|
|
14
|
+
monkeypatch.setenv("ORCHESTRA_API_KEY", "fake-key")
|
|
15
|
+
monkeypatch.setenv("BASE_URL", "")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_create_success_default_no_publish(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
19
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
20
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
21
|
+
|
|
22
|
+
httpx_mock.add_response(
|
|
23
|
+
method="POST",
|
|
24
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
25
|
+
json={"ok": True},
|
|
26
|
+
status_code=200,
|
|
27
|
+
)
|
|
28
|
+
httpx_mock.add_response(
|
|
29
|
+
method="POST",
|
|
30
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines",
|
|
31
|
+
json={"id": "pipeline-id"},
|
|
32
|
+
status_code=201,
|
|
33
|
+
match_json={
|
|
34
|
+
"alias": "demo",
|
|
35
|
+
"data": {"name": "demo", "version": 1},
|
|
36
|
+
"published": False,
|
|
37
|
+
"storage_provider": "ORCHESTRA",
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
result = runner.invoke(app, ["create-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
42
|
+
assert result.exit_code == 0
|
|
43
|
+
assert "created successfully" in result.output
|
|
44
|
+
assert "https://app.getorchestra.io/pipelines/pipeline-id/edit" in result.output
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_create_publish_flag(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
48
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
49
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
50
|
+
|
|
51
|
+
httpx_mock.add_response(
|
|
52
|
+
method="POST",
|
|
53
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
54
|
+
json={"ok": True},
|
|
55
|
+
status_code=200,
|
|
56
|
+
)
|
|
57
|
+
httpx_mock.add_response(
|
|
58
|
+
method="POST",
|
|
59
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines",
|
|
60
|
+
json={"id": "pipeline-id"},
|
|
61
|
+
status_code=201,
|
|
62
|
+
match_json={
|
|
63
|
+
"alias": "demo",
|
|
64
|
+
"data": {"name": "demo", "version": 1},
|
|
65
|
+
"published": True,
|
|
66
|
+
"storage_provider": "ORCHESTRA",
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
result = runner.invoke(
|
|
71
|
+
app,
|
|
72
|
+
["create-pipeline", "--alias", "demo", "--path", str(yaml_file), "--publish"],
|
|
73
|
+
)
|
|
74
|
+
assert result.exit_code == 0
|
|
75
|
+
assert "created successfully" in result.output
|
|
76
|
+
assert "https://app.getorchestra.io/pipelines/pipeline-id/edit" in result.output
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_create_missing_api_key(monkeypatch, tmp_path: Path):
|
|
80
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
81
|
+
yaml_file.write_text("name: demo\n")
|
|
82
|
+
|
|
83
|
+
monkeypatch.delenv("ORCHESTRA_API_KEY", raising=False)
|
|
84
|
+
result = runner.invoke(app, ["create-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
85
|
+
assert result.exit_code == 1
|
|
86
|
+
assert "ORCHESTRA_API_KEY is not set" in result.output
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_create_invalid_yaml(tmp_path: Path):
|
|
90
|
+
bad = tmp_path / "bad.yaml"
|
|
91
|
+
bad.write_text("name: [oops\n")
|
|
92
|
+
|
|
93
|
+
result = runner.invoke(app, ["create-pipeline", "--alias", "demo", "--path", str(bad)])
|
|
94
|
+
assert result.exit_code == 1
|
|
95
|
+
assert "Invalid YAML" in result.output
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_create_schema_validation_error(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
99
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
100
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
101
|
+
|
|
102
|
+
httpx_mock.add_response(
|
|
103
|
+
method="POST",
|
|
104
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
105
|
+
json={"detail": [{"loc": ["root"], "msg": "bad"}]},
|
|
106
|
+
status_code=400,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
result = runner.invoke(app, ["create-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
110
|
+
assert result.exit_code == 1
|
|
111
|
+
assert "Validation failed" in result.output
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_create_api_error(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
115
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
116
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
117
|
+
|
|
118
|
+
httpx_mock.add_response(
|
|
119
|
+
method="POST",
|
|
120
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
121
|
+
json={"ok": True},
|
|
122
|
+
status_code=200,
|
|
123
|
+
)
|
|
124
|
+
httpx_mock.add_response(
|
|
125
|
+
method="POST",
|
|
126
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines",
|
|
127
|
+
json={"detail": "bad"},
|
|
128
|
+
status_code=400,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
result = runner.invoke(app, ["create-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
132
|
+
assert result.exit_code == 1
|
|
133
|
+
assert "Create failed" in result.output
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_create_success_without_pipeline_id_fails(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
137
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
138
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
139
|
+
|
|
140
|
+
httpx_mock.add_response(
|
|
141
|
+
method="POST",
|
|
142
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
143
|
+
json={"ok": True},
|
|
144
|
+
status_code=200,
|
|
145
|
+
)
|
|
146
|
+
httpx_mock.add_response(
|
|
147
|
+
method="POST",
|
|
148
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines",
|
|
149
|
+
json={"alias": "demo"},
|
|
150
|
+
status_code=201,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
result = runner.invoke(app, ["create-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
154
|
+
assert result.exit_code == 1
|
|
155
|
+
assert "success response did not include pipeline id" in result.output
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_create_success_with_invalid_json_fails(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
159
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
160
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
161
|
+
|
|
162
|
+
httpx_mock.add_response(
|
|
163
|
+
method="POST",
|
|
164
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
165
|
+
json={"ok": True},
|
|
166
|
+
status_code=200,
|
|
167
|
+
)
|
|
168
|
+
httpx_mock.add_response(
|
|
169
|
+
method="POST",
|
|
170
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines",
|
|
171
|
+
text="ok",
|
|
172
|
+
status_code=201,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
result = runner.invoke(app, ["create-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
176
|
+
assert result.exit_code == 1
|
|
177
|
+
assert "success response was not valid JSON" in result.output
|
|
@@ -187,6 +187,7 @@ def test_run_wait_success(httpx_mock: HTTPXMock, monkeypatch, tmp_path: Path):
|
|
|
187
187
|
result.output.strip().splitlines()[1]
|
|
188
188
|
== f"Started pipeline (alias: demo), run id: {mock_pipeline_run_id}"
|
|
189
189
|
) # noqa: E501
|
|
190
|
+
assert "Invalid status value: RUNNING" not in result.output
|
|
190
191
|
assert result.output.strip().splitlines()[-2] == "✅ Pipeline succeeded"
|
|
191
192
|
assert result.output.strip().splitlines()[-1] == mock_pipeline_run_id
|
|
192
193
|
|
|
@@ -225,6 +226,7 @@ def test_run_wait_failed(httpx_mock: HTTPXMock, monkeypatch, tmp_path: Path):
|
|
|
225
226
|
|
|
226
227
|
result = runner.invoke(app, ["run", "--alias", "demo", "--wait"])
|
|
227
228
|
assert result.exit_code == 1
|
|
229
|
+
assert "Invalid status value: RUNNING" not in result.output
|
|
228
230
|
assert "status FAILED" in result.output
|
|
229
231
|
assert "/pipeline-runs/run-xyz/lineage" in result.output
|
|
230
232
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pytest_httpx import HTTPXMock
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from orchestra_cli.src.cli import app
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def mock_env(monkeypatch):
|
|
14
|
+
monkeypatch.setenv("ORCHESTRA_API_KEY", "fake-key")
|
|
15
|
+
monkeypatch.setenv("BASE_URL", "")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_update_success_default_no_publish(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
19
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
20
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
21
|
+
|
|
22
|
+
httpx_mock.add_response(
|
|
23
|
+
method="POST",
|
|
24
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
25
|
+
json={"ok": True},
|
|
26
|
+
status_code=200,
|
|
27
|
+
)
|
|
28
|
+
httpx_mock.add_response(
|
|
29
|
+
method="PUT",
|
|
30
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/demo",
|
|
31
|
+
json={"id": "pipeline-id"},
|
|
32
|
+
status_code=200,
|
|
33
|
+
match_json={
|
|
34
|
+
"data": {"name": "demo", "version": 1},
|
|
35
|
+
"published": False,
|
|
36
|
+
"storage_provider": "ORCHESTRA",
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
result = runner.invoke(app, ["update-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
41
|
+
assert result.exit_code == 0
|
|
42
|
+
assert "updated successfully" in result.output
|
|
43
|
+
assert "https://app.getorchestra.io/pipelines/pipeline-id/edit" in result.output
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_update_publish_flag(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
47
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
48
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
49
|
+
|
|
50
|
+
httpx_mock.add_response(
|
|
51
|
+
method="POST",
|
|
52
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
53
|
+
json={"ok": True},
|
|
54
|
+
status_code=200,
|
|
55
|
+
)
|
|
56
|
+
httpx_mock.add_response(
|
|
57
|
+
method="PUT",
|
|
58
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/demo",
|
|
59
|
+
json={"id": "pipeline-id"},
|
|
60
|
+
status_code=200,
|
|
61
|
+
match_json={
|
|
62
|
+
"data": {"name": "demo", "version": 1},
|
|
63
|
+
"published": True,
|
|
64
|
+
"storage_provider": "ORCHESTRA",
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
result = runner.invoke(
|
|
69
|
+
app,
|
|
70
|
+
["update-pipeline", "--alias", "demo", "--path", str(yaml_file), "--publish"],
|
|
71
|
+
)
|
|
72
|
+
assert result.exit_code == 0
|
|
73
|
+
assert "updated successfully" in result.output
|
|
74
|
+
assert "https://app.getorchestra.io/pipelines/pipeline-id/edit" in result.output
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_update_missing_api_key(monkeypatch, tmp_path: Path):
|
|
78
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
79
|
+
yaml_file.write_text("name: demo\n")
|
|
80
|
+
|
|
81
|
+
monkeypatch.delenv("ORCHESTRA_API_KEY", raising=False)
|
|
82
|
+
result = runner.invoke(app, ["update-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
83
|
+
assert result.exit_code == 1
|
|
84
|
+
assert "ORCHESTRA_API_KEY is not set" in result.output
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_update_invalid_yaml(tmp_path: Path):
|
|
88
|
+
bad = tmp_path / "bad.yaml"
|
|
89
|
+
bad.write_text("name: [oops\n")
|
|
90
|
+
|
|
91
|
+
result = runner.invoke(app, ["update-pipeline", "--alias", "demo", "--path", str(bad)])
|
|
92
|
+
assert result.exit_code == 1
|
|
93
|
+
assert "Invalid YAML" in result.output
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_update_schema_validation_error(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
97
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
98
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
99
|
+
|
|
100
|
+
httpx_mock.add_response(
|
|
101
|
+
method="POST",
|
|
102
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
103
|
+
json={"detail": [{"loc": ["root"], "msg": "bad"}]},
|
|
104
|
+
status_code=400,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result = runner.invoke(app, ["update-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
108
|
+
assert result.exit_code == 1
|
|
109
|
+
assert "Validation failed" in result.output
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_update_api_error_orchestra_backed_only(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
113
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
114
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
115
|
+
|
|
116
|
+
httpx_mock.add_response(
|
|
117
|
+
method="POST",
|
|
118
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
119
|
+
json={"ok": True},
|
|
120
|
+
status_code=200,
|
|
121
|
+
)
|
|
122
|
+
httpx_mock.add_response(
|
|
123
|
+
method="PUT",
|
|
124
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/demo",
|
|
125
|
+
json={"detail": "Only orchestra-backed pipelines can be updated via this endpoint."},
|
|
126
|
+
status_code=400,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = runner.invoke(app, ["update-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
130
|
+
assert result.exit_code == 1
|
|
131
|
+
assert "Update failed" in result.output
|
|
132
|
+
assert "Only orchestra-backed pipelines can be updated via this endpoint." in result.output
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_update_success_without_pipeline_id_fails(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
136
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
137
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
138
|
+
|
|
139
|
+
httpx_mock.add_response(
|
|
140
|
+
method="POST",
|
|
141
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
142
|
+
json={"ok": True},
|
|
143
|
+
status_code=200,
|
|
144
|
+
)
|
|
145
|
+
httpx_mock.add_response(
|
|
146
|
+
method="PUT",
|
|
147
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/demo",
|
|
148
|
+
json={"alias": "demo"},
|
|
149
|
+
status_code=200,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
result = runner.invoke(app, ["update-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
153
|
+
assert result.exit_code == 1
|
|
154
|
+
assert "success response did not include pipeline id" in result.output
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_update_success_with_invalid_json_fails(tmp_path: Path, httpx_mock: HTTPXMock):
|
|
158
|
+
yaml_file = tmp_path / "pipe.yaml"
|
|
159
|
+
yaml_file.write_text("name: demo\nversion: 1\n")
|
|
160
|
+
|
|
161
|
+
httpx_mock.add_response(
|
|
162
|
+
method="POST",
|
|
163
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/schema",
|
|
164
|
+
json={"ok": True},
|
|
165
|
+
status_code=200,
|
|
166
|
+
)
|
|
167
|
+
httpx_mock.add_response(
|
|
168
|
+
method="PUT",
|
|
169
|
+
url="https://app.getorchestra.io/api/engine/public/pipelines/demo",
|
|
170
|
+
text="ok",
|
|
171
|
+
status_code=200,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
result = runner.invoke(app, ["update-pipeline", "--alias", "demo", "--path", str(yaml_file)])
|
|
175
|
+
assert result.exit_code == 1
|
|
176
|
+
assert "success response was not valid JSON" in result.output
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
_PROD_BASE_URL = "https://app.getorchestra.io"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_api_url(path: str) -> str:
|
|
7
|
+
suffix = f"/{path.lstrip('/')}" if path else ""
|
|
8
|
+
return f"{get_base_url()}/api/engine/public/pipelines{suffix}"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_base_url() -> str:
|
|
12
|
+
return (os.getenv("BASE_URL") or _PROD_BASE_URL).strip().rstrip("/")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_public_api_url(path: str) -> str:
|
|
16
|
+
return f"{get_base_url()}/api/engine/public/{path.lstrip('/')}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_create_pipeline_url() -> str:
|
|
20
|
+
return get_api_url("")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_update_pipeline_url(alias: str) -> str:
|
|
24
|
+
return get_api_url(alias)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_pipeline_edit_url(pipeline_id: str) -> str:
|
|
28
|
+
return f"{get_base_url()}/pipelines/{pipeline_id}/edit"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orchestra-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Orchestra CLI. Perform operations with Orchestra locally.
|
|
5
5
|
Author-email: Orchestra Technologies <support@getorchestra.io>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -15,6 +15,8 @@ Orchestra CLI for working with Orchestra pipelines from your terminal.
|
|
|
15
15
|
|
|
16
16
|
Two entrypoints are available: `orchestra` and `orchestra-cli` (they are equivalent).
|
|
17
17
|
|
|
18
|
+
See [`AGENTS.md`](AGENTS.md) for contributor and AI agent guidance (environment setup, code conventions, testing).
|
|
19
|
+
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
20
22
|
```bash
|
|
@@ -30,13 +32,14 @@ pipx install orchestra-cli
|
|
|
30
32
|
## Environment variables
|
|
31
33
|
|
|
32
34
|
- `ORCHESTRA_API_KEY`: Required for actions that call the API (`import`, `run`).
|
|
33
|
-
- `BASE_URL`: Optional. Override the default
|
|
34
|
-
(`https://app.getorchestra.io/api/engine/public/pipelines/{}`) for non‑production/testing.
|
|
35
|
+
- `BASE_URL`: Optional. Override the default Orchestra host (`https://app.getorchestra.io`) for non‑production/testing.
|
|
35
36
|
|
|
36
37
|
## Commands overview
|
|
37
38
|
|
|
38
39
|
- `validate`: Validate a pipeline YAML locally against the Orchestra API schema.
|
|
39
40
|
- `import`: Register a pipeline YAML (from a git repo) with Orchestra under an alias.
|
|
41
|
+
- `create-pipeline`: Create an Orchestra-backed pipeline from a local YAML file.
|
|
42
|
+
- `update-pipeline`: Update an existing Orchestra-backed pipeline from a local YAML file.
|
|
40
43
|
- `run`: Start a pipeline run by alias, optionally pinning branch/commit and waiting for completion.
|
|
41
44
|
|
|
42
45
|
Use `orchestra --help` or `orchestra <command> --help` for built-in help.
|
|
@@ -118,6 +121,63 @@ Common errors
|
|
|
118
121
|
|
|
119
122
|
---
|
|
120
123
|
|
|
124
|
+
## create-pipeline
|
|
125
|
+
|
|
126
|
+
Create an Orchestra-backed pipeline directly from a local YAML file.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
export ORCHESTRA_API_KEY=...
|
|
130
|
+
|
|
131
|
+
orchestra create-pipeline \
|
|
132
|
+
--alias my-pipeline \
|
|
133
|
+
--path ./pipelines/pipeline.yaml \
|
|
134
|
+
--publish # optional, defaults to --no-publish
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Options
|
|
138
|
+
|
|
139
|
+
- `-a, --alias` (required): Pipeline alias.
|
|
140
|
+
- `-p, --path` (required): Path to pipeline YAML file.
|
|
141
|
+
- `--publish/--no-publish` (optional, default `--no-publish`): Whether the pipeline is published.
|
|
142
|
+
|
|
143
|
+
Behavior
|
|
144
|
+
|
|
145
|
+
- Validates YAML against the Orchestra schema endpoint before creating.
|
|
146
|
+
- Sends pipeline data to `POST /pipelines` with `storage_provider=ORCHESTRA`.
|
|
147
|
+
- On success, prints the pipeline edit URL (`/pipelines/{pipeline_id}/edit`) when an ID is returned.
|
|
148
|
+
- Exit codes: `0` on success, `1` on failure.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## update-pipeline
|
|
153
|
+
|
|
154
|
+
Update an existing Orchestra-backed pipeline from a local YAML file.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
export ORCHESTRA_API_KEY=...
|
|
158
|
+
|
|
159
|
+
orchestra update-pipeline \
|
|
160
|
+
--alias my-pipeline \
|
|
161
|
+
--path ./pipelines/pipeline.yaml \
|
|
162
|
+
--no-publish # default
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Options
|
|
166
|
+
|
|
167
|
+
- `-a, --alias` (required): Pipeline alias to update.
|
|
168
|
+
- `-p, --path` (required): Path to pipeline YAML file.
|
|
169
|
+
- `--publish/--no-publish` (optional, default `--no-publish`): Whether the pipeline is published.
|
|
170
|
+
|
|
171
|
+
Behavior
|
|
172
|
+
|
|
173
|
+
- Validates YAML against the Orchestra schema endpoint before updating.
|
|
174
|
+
- Sends pipeline data to `PUT /pipelines/{alias}` with `storage_provider=ORCHESTRA`.
|
|
175
|
+
- Only Orchestra-backed pipelines can be updated via this endpoint (Git-backed pipelines are rejected).
|
|
176
|
+
- On success, prints the pipeline edit URL (`/pipelines/{pipeline_id}/edit`) when an ID is returned.
|
|
177
|
+
- Exit codes: `0` on success, `1` on failure.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
121
181
|
## run
|
|
122
182
|
|
|
123
183
|
Start a pipeline run by alias. Optionally specify a branch and/or commit. By default, the command waits and polls the run status until completion.
|
|
@@ -8,14 +8,19 @@ orchestra_cli.egg-info/requires.txt
|
|
|
8
8
|
orchestra_cli.egg-info/top_level.txt
|
|
9
9
|
orchestra_cli/src/__init__.py
|
|
10
10
|
orchestra_cli/src/cli.py
|
|
11
|
+
orchestra_cli/src/create_pipeline.py
|
|
11
12
|
orchestra_cli/src/import_pipeline.py
|
|
13
|
+
orchestra_cli/src/pipeline_upsert.py
|
|
12
14
|
orchestra_cli/src/run_pipeline.py
|
|
15
|
+
orchestra_cli/src/update_pipeline.py
|
|
13
16
|
orchestra_cli/src/validate_pipeline.py
|
|
14
17
|
orchestra_cli/tests/__init__.py
|
|
15
18
|
orchestra_cli/tests/conftest.py
|
|
16
19
|
orchestra_cli/tests/test_cli.py
|
|
20
|
+
orchestra_cli/tests/test_create_pipeline.py
|
|
17
21
|
orchestra_cli/tests/test_import_pipeline.py
|
|
18
22
|
orchestra_cli/tests/test_run_pipeline.py
|
|
23
|
+
orchestra_cli/tests/test_update_pipeline.py
|
|
19
24
|
orchestra_cli/tests/test_validate_pipeline.py
|
|
20
25
|
orchestra_cli/utils/constants.py
|
|
21
26
|
orchestra_cli/utils/git.py
|
|
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
|