orchestra-cli 0.4.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.
Files changed (61) hide show
  1. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/PKG-INFO +45 -3
  2. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/README.md +44 -2
  3. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/build_pipeline.py +19 -8
  4. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/cli.py +6 -0
  5. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/create_pipeline.py +5 -5
  6. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/delete_pipeline.py +5 -5
  7. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/fetch_pipelines.py +5 -6
  8. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/get_pipeline.py +5 -5
  9. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/import_pipeline.py +5 -5
  10. orchestra_cli-0.5.1/orchestra_cli/src/list_accounts.py +16 -0
  11. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/login.py +49 -3
  12. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/migrate_pipeline.py +18 -10
  13. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/run_pipeline.py +55 -21
  14. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/task_logs.py +23 -15
  15. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/update_pipeline.py +6 -6
  16. orchestra_cli-0.5.1/orchestra_cli/src/use_account.py +61 -0
  17. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/conftest.py +16 -0
  18. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_delete_pipeline.py +4 -15
  19. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_fetch_pipelines.py +41 -0
  20. orchestra_cli-0.5.1/orchestra_cli/tests/test_list_accounts.py +84 -0
  21. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_login.py +136 -0
  22. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_run_pipeline.py +101 -1
  23. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_task_logs.py +34 -0
  24. orchestra_cli-0.5.1/orchestra_cli/tests/test_use_account.py +214 -0
  25. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/utils/test_api.py +35 -0
  26. orchestra_cli-0.5.1/orchestra_cli/tests/utils/test_picker.py +45 -0
  27. orchestra_cli-0.5.1/orchestra_cli/utils/accounts.py +44 -0
  28. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/api.py +60 -11
  29. orchestra_cli-0.5.1/orchestra_cli/utils/picker.py +58 -0
  30. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/pipeline_lookup.py +3 -4
  31. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/PKG-INFO +45 -3
  32. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/SOURCES.txt +7 -0
  33. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/pyproject.toml +1 -1
  34. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/__init__.py +0 -0
  35. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/login_page.html +0 -0
  36. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/pipeline_upsert.py +0 -0
  37. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/src/validate_pipeline.py +0 -0
  38. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/__init__.py +0 -0
  39. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_build_pipeline.py +0 -0
  40. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_cli.py +0 -0
  41. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_create_pipeline.py +0 -0
  42. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_get_pipeline.py +0 -0
  43. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_import_pipeline.py +0 -0
  44. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_migrate_pipeline.py +0 -0
  45. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_pipeline_selector.py +0 -0
  46. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_update_pipeline.py +0 -0
  47. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/test_validate_pipeline.py +0 -0
  48. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/utils/__init__.py +0 -0
  49. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/tests/utils/test_git.py +0 -0
  50. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/constants.py +0 -0
  51. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/credentials.py +0 -0
  52. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/git.py +0 -0
  53. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/pipeline_selector.py +0 -0
  54. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/pipeline_update.py +0 -0
  55. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/styling.py +0 -0
  56. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli/utils/yaml_loader.py +0 -0
  57. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/dependency_links.txt +0 -0
  58. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/entry_points.txt +0 -0
  59. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/requires.txt +0 -0
  60. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/orchestra_cli.egg-info/top_level.txt +0 -0
  61. {orchestra_cli-0.4.0 → orchestra_cli-0.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestra-cli
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -34,12 +34,13 @@ pipx install orchestra-cli
34
34
 
35
35
  ## Environment variables
36
36
 
37
- - `ORCHESTRA_API_KEY`: Credential for actions that call the API (`pipeline import`, `pipeline new`, `pipeline update`, `pipeline migrate`, `pipeline get`, `pipeline list`, `pipeline delete`, `pipeline run`, `pipeline build`). Not needed once you have run `orchestra login`: a login takes precedence, and the key is used only when you are not logged in or your login has expired.
37
+ - `ORCHESTRA_API_KEY`: Credential for actions that call the API (`pipeline import`, `pipeline new`, `pipeline update`, `pipeline migrate`, `pipeline get`, `pipeline list`, `pipeline delete`, `pipeline run`, `pipeline build`, `accounts list`). Not needed once you have run `orchestra login`: a login takes precedence, and the key is used only when you are not logged in or your login has expired.
38
+ - `ORCHESTRA_ACCOUNT_ID`: Optional. The workspace to act in when your `orchestra login` covers several. Every `pipeline` and `task` command that calls the API also takes `--account-id`, which wins over the variable, and both win over the default saved by `orchestra accounts use`. API keys belong to one account and don't need it.
38
39
  - `BASE_URL`: Optional. Override the default Orchestra host (`https://app.getorchestra.io`) for non‑production/testing.
39
40
 
40
41
  ## Command structure
41
42
 
42
- Commands follow a `noun verb` shape. The current nouns are `pipeline` and `task`:
43
+ Commands follow a `noun verb` shape. The current nouns are `pipeline`, `task` and `accounts`:
43
44
 
44
45
  | Command | Description |
45
46
  | ------------------------------------ | ------------------------------------------------------------------------------------------- |
@@ -55,6 +56,8 @@ Commands follow a `noun verb` shape. The current nouns are `pipeline` and `task`
55
56
  | `orchestra pipeline run` | Start a pipeline run by selector, optionally pinning branch/commit and waiting. |
56
57
  | `orchestra pipeline build` | Validate local YAML, create or update a draft pipeline, and start that draft version. |
57
58
  | `orchestra task logs` | Fetch or follow logs for a single task run. |
59
+ | `orchestra accounts list` | List the workspaces your login or API key covers, with their ids, as JSON. |
60
+ | `orchestra accounts use [id\|name]` | Save the workspace later commands act in when no `--account-id` is given. |
58
61
 
59
62
  Commands that read a local pipeline file (`pipeline validate`, `import`, `new`, `update`, `build`, and `run --path` with `--task`) accept `.yaml`, `.yml` and `.oml` files; any other extension is rejected. `.oml` files are read as YAML, so only OML written in YAML or JSON syntax is supported. OML block terminators (`.`) and embeds (`%sql ... %%`) fail with an `Invalid YAML` error.
60
63
 
@@ -123,6 +126,8 @@ orchestra login
123
126
 
124
127
  Logins are per host: a login made with `BASE_URL` pointing at another environment is only used against that environment. CI and scripts, where nobody is logged in, keep using `ORCHESTRA_API_KEY`.
125
128
 
129
+ After logging in it saves a default workspace, as `accounts use` does: the only one if your login covers one, or the one you pick from a list if it covers several. Esc skips the default, and without a terminal it prints a hint to run `orchestra accounts use` instead. If the accounts can't be listed, the login still succeeds with no default set.
130
+
126
131
  ---
127
132
 
128
133
  ## pipeline import
@@ -413,6 +418,43 @@ Behavior
413
418
 
414
419
  ---
415
420
 
421
+ ## accounts list
422
+
423
+ List the workspaces the current credential covers. An `orchestra login` token can cover several workspaces; an API key lists just its own.
424
+
425
+ ```bash
426
+ orchestra accounts list
427
+ ```
428
+
429
+ Behavior
430
+
431
+ - Sends `GET /public/v1/accounts`, with no account header.
432
+ - Prints the `[{id, name}]` response as pretty JSON, adding `default: true` to the account saved by `accounts use` and `false` to the rest.
433
+ - Exit codes: `0` on success, `1` on failure.
434
+
435
+ ---
436
+
437
+ ## accounts use
438
+
439
+ Save a default workspace for an `orchestra login` that covers several, so commands no longer need `--account-id`.
440
+
441
+ ```bash
442
+ orchestra accounts use 'some great account'
443
+ orchestra accounts use 00000000-0000-0000-0000-000000000000
444
+ orchestra accounts use # pick from a list
445
+ ```
446
+
447
+ Behavior
448
+
449
+ - Fetches the accounts as `accounts list` does, then matches an exact id first and a case-insensitive exact name second.
450
+ - Exits `1`, listing the valid names, when nothing matches, and asks for the id when several workspaces share the name.
451
+ - With no argument, lists the workspaces with the cursor on the current default: ↑/↓ moves, Enter saves, and Esc, Ctrl+C or Ctrl+D exits `1` leaving the default unchanged. Without a terminal (CI, piped input) it exits `1` and asks for an id or name instead.
452
+ - Saves the account into the login's entry in `~/.orchestra/credentials.json`. It survives token refreshes; running `orchestra login` again resets it.
453
+ - `--account-id` and `ORCHESTRA_ACCOUNT_ID` still override the default. It is never sent with an API key.
454
+ - Exits `1` when you are using an API key rather than a login, since a key covers one account.
455
+
456
+ ---
457
+
416
458
  ## task logs
417
459
 
418
460
  Fetch or follow logs for a single task run.
@@ -20,12 +20,13 @@ pipx install orchestra-cli
20
20
 
21
21
  ## Environment variables
22
22
 
23
- - `ORCHESTRA_API_KEY`: Credential for actions that call the API (`pipeline import`, `pipeline new`, `pipeline update`, `pipeline migrate`, `pipeline get`, `pipeline list`, `pipeline delete`, `pipeline run`, `pipeline build`). Not needed once you have run `orchestra login`: a login takes precedence, and the key is used only when you are not logged in or your login has expired.
23
+ - `ORCHESTRA_API_KEY`: Credential for actions that call the API (`pipeline import`, `pipeline new`, `pipeline update`, `pipeline migrate`, `pipeline get`, `pipeline list`, `pipeline delete`, `pipeline run`, `pipeline build`, `accounts list`). Not needed once you have run `orchestra login`: a login takes precedence, and the key is used only when you are not logged in or your login has expired.
24
+ - `ORCHESTRA_ACCOUNT_ID`: Optional. The workspace to act in when your `orchestra login` covers several. Every `pipeline` and `task` command that calls the API also takes `--account-id`, which wins over the variable, and both win over the default saved by `orchestra accounts use`. API keys belong to one account and don't need it.
24
25
  - `BASE_URL`: Optional. Override the default Orchestra host (`https://app.getorchestra.io`) for non‑production/testing.
25
26
 
26
27
  ## Command structure
27
28
 
28
- Commands follow a `noun verb` shape. The current nouns are `pipeline` and `task`:
29
+ Commands follow a `noun verb` shape. The current nouns are `pipeline`, `task` and `accounts`:
29
30
 
30
31
  | Command | Description |
31
32
  | ------------------------------------ | ------------------------------------------------------------------------------------------- |
@@ -41,6 +42,8 @@ Commands follow a `noun verb` shape. The current nouns are `pipeline` and `task`
41
42
  | `orchestra pipeline run` | Start a pipeline run by selector, optionally pinning branch/commit and waiting. |
42
43
  | `orchestra pipeline build` | Validate local YAML, create or update a draft pipeline, and start that draft version. |
43
44
  | `orchestra task logs` | Fetch or follow logs for a single task run. |
45
+ | `orchestra accounts list` | List the workspaces your login or API key covers, with their ids, as JSON. |
46
+ | `orchestra accounts use [id\|name]` | Save the workspace later commands act in when no `--account-id` is given. |
44
47
 
45
48
  Commands that read a local pipeline file (`pipeline validate`, `import`, `new`, `update`, `build`, and `run --path` with `--task`) accept `.yaml`, `.yml` and `.oml` files; any other extension is rejected. `.oml` files are read as YAML, so only OML written in YAML or JSON syntax is supported. OML block terminators (`.`) and embeds (`%sql ... %%`) fail with an `Invalid YAML` error.
46
49
 
@@ -109,6 +112,8 @@ orchestra login
109
112
 
110
113
  Logins are per host: a login made with `BASE_URL` pointing at another environment is only used against that environment. CI and scripts, where nobody is logged in, keep using `ORCHESTRA_API_KEY`.
111
114
 
115
+ After logging in it saves a default workspace, as `accounts use` does: the only one if your login covers one, or the one you pick from a list if it covers several. Esc skips the default, and without a terminal it prints a hint to run `orchestra accounts use` instead. If the accounts can't be listed, the login still succeeds with no default set.
116
+
112
117
  ---
113
118
 
114
119
  ## pipeline import
@@ -399,6 +404,43 @@ Behavior
399
404
 
400
405
  ---
401
406
 
407
+ ## accounts list
408
+
409
+ List the workspaces the current credential covers. An `orchestra login` token can cover several workspaces; an API key lists just its own.
410
+
411
+ ```bash
412
+ orchestra accounts list
413
+ ```
414
+
415
+ Behavior
416
+
417
+ - Sends `GET /public/v1/accounts`, with no account header.
418
+ - Prints the `[{id, name}]` response as pretty JSON, adding `default: true` to the account saved by `accounts use` and `false` to the rest.
419
+ - Exit codes: `0` on success, `1` on failure.
420
+
421
+ ---
422
+
423
+ ## accounts use
424
+
425
+ Save a default workspace for an `orchestra login` that covers several, so commands no longer need `--account-id`.
426
+
427
+ ```bash
428
+ orchestra accounts use 'some great account'
429
+ orchestra accounts use 00000000-0000-0000-0000-000000000000
430
+ orchestra accounts use # pick from a list
431
+ ```
432
+
433
+ Behavior
434
+
435
+ - Fetches the accounts as `accounts list` does, then matches an exact id first and a case-insensitive exact name second.
436
+ - Exits `1`, listing the valid names, when nothing matches, and asks for the id when several workspaces share the name.
437
+ - With no argument, lists the workspaces with the cursor on the current default: ↑/↓ moves, Enter saves, and Esc, Ctrl+C or Ctrl+D exits `1` leaving the default unchanged. Without a terminal (CI, piped input) it exits `1` and asks for an id or name instead.
438
+ - Saves the account into the login's entry in `~/.orchestra/credentials.json`. It survives token refreshes; running `orchestra login` again resets it.
439
+ - `--account-id` and `ORCHESTRA_ACCOUNT_ID` still override the default. It is never sent with an API key.
440
+ - Exits `1` when you are using an API key rather than a login, since a key covers one account.
441
+
442
+ ---
443
+
402
444
  ## task logs
403
445
 
404
446
  Fetch or follow logs for a single task run.
@@ -3,7 +3,13 @@ from pathlib import Path
3
3
  import httpx
4
4
  import typer
5
5
 
6
- from ..utils.api import auth_headers, fail_with_response, request_or_exit, require_credential
6
+ from ..utils.api import (
7
+ account_id_option,
8
+ api_client,
9
+ fail_with_response,
10
+ request_or_exit,
11
+ require_credential,
12
+ )
7
13
  from ..utils.constants import get_create_pipeline_url, get_update_pipeline_url
8
14
  from ..utils.git import confirm_git_warnings_or_exit, prepare_git_backed_run_target
9
15
  from ..utils.pipeline_lookup import lookup_existing_pipeline
@@ -64,6 +70,7 @@ def _build_create_selector(
64
70
 
65
71
 
66
72
  def _create_draft_pipeline(
73
+ client: httpx.Client,
67
74
  path: Path,
68
75
  lookup_selector: PipelineSelector,
69
76
  pipeline_data: dict[str, object],
@@ -74,11 +81,9 @@ def _create_draft_pipeline(
74
81
 
75
82
  typer.echo(f"Creating draft pipeline ({create_selector.display()})")
76
83
  create_response = request_or_exit(
77
- httpx.post,
84
+ client.post,
78
85
  get_create_pipeline_url(),
79
86
  json=payload,
80
- timeout=30,
81
- headers=auth_headers(),
82
87
  )
83
88
 
84
89
  if create_response.status_code == 201:
@@ -95,6 +100,7 @@ def _create_draft_pipeline(
95
100
 
96
101
 
97
102
  def _update_draft_pipeline(
103
+ client: httpx.Client,
98
104
  existing_pipeline: dict[str, object],
99
105
  pipeline_data: dict[str, object],
100
106
  ) -> tuple[PipelineSelector, int]:
@@ -103,11 +109,9 @@ def _update_draft_pipeline(
103
109
 
104
110
  typer.echo(f"Updating draft pipeline ({update_selector.display()})")
105
111
  update_response = request_or_exit(
106
- httpx.put,
112
+ client.put,
107
113
  get_update_pipeline_url(),
108
114
  json=payload,
109
- timeout=30,
110
- headers=auth_headers(),
111
115
  )
112
116
 
113
117
  if update_response.status_code == 200:
@@ -139,11 +143,13 @@ def build_pipeline(
139
143
  "--force/--no-force",
140
144
  help="Ignore any warnings and run the pipeline anyway",
141
145
  ),
146
+ account_id: str | None = account_id_option(),
142
147
  ) -> None:
143
148
  """
144
149
  Validate local YAML, create or update a draft pipeline, and start the draft version.
145
150
  """
146
151
  require_credential()
152
+ client = api_client(account_id)
147
153
  if path is None:
148
154
  typer.echo(
149
155
  red("A pipeline YAML file path is required (use -p or --path with your YAML file)"),
@@ -153,16 +159,18 @@ def build_pipeline(
153
159
 
154
160
  confirm_git_warnings_or_exit(force, path)
155
161
  pipeline_data = load_validated_pipeline_data(path)
156
- existing_pipeline = lookup_existing_pipeline(lookup_selector, "Build", allow_404=True)
162
+ existing_pipeline = lookup_existing_pipeline(client, lookup_selector, "Build", allow_404=True)
157
163
 
158
164
  if existing_pipeline is None:
159
165
  run_selector, version_number = _create_draft_pipeline(
166
+ client=client,
160
167
  path=path,
161
168
  lookup_selector=lookup_selector,
162
169
  pipeline_data=pipeline_data,
163
170
  force=force,
164
171
  )
165
172
  start_pipeline_run(
173
+ client=client,
166
174
  selector=run_selector,
167
175
  payload=build_run_payload(
168
176
  branch=branch,
@@ -177,11 +185,13 @@ def build_pipeline(
177
185
  pipeline_storage_provider = storage_provider(existing_pipeline)
178
186
  if pipeline_storage_provider is None or pipeline_storage_provider == "ORCHESTRA":
179
187
  run_selector, version_number = _update_draft_pipeline(
188
+ client=client,
180
189
  existing_pipeline=existing_pipeline,
181
190
  pipeline_data=pipeline_data,
182
191
  )
183
192
 
184
193
  start_pipeline_run(
194
+ client=client,
185
195
  selector=run_selector,
186
196
  payload=build_run_payload(
187
197
  branch=branch,
@@ -215,6 +225,7 @@ def build_pipeline(
215
225
  )
216
226
 
217
227
  start_pipeline_run(
228
+ client=client,
218
229
  selector=run_selector,
219
230
  payload=build_run_payload(
220
231
  branch=git_branch,
@@ -6,11 +6,13 @@ from .delete_pipeline import delete_pipeline
6
6
  from .fetch_pipelines import fetch_pipelines
7
7
  from .get_pipeline import get_pipeline
8
8
  from .import_pipeline import import_pipeline
9
+ from .list_accounts import list_accounts
9
10
  from .login import login
10
11
  from .migrate_pipeline import migrate_pipeline
11
12
  from .run_pipeline import run_pipeline
12
13
  from .task_logs import task_logs
13
14
  from .update_pipeline import update_pipeline
15
+ from .use_account import use_account
14
16
  from .validate_pipeline import validate
15
17
 
16
18
  app = typer.Typer(help="Orchestra CLI – perform operations with Orchestra locally.")
@@ -20,6 +22,8 @@ app.add_typer(pipeline_app, name="pipeline")
20
22
  task_app = typer.Typer(help="Inspect Orchestra task runs.")
21
23
  app.add_typer(task_app, name="task")
22
24
  app.command(name="login")(login)
25
+ accounts_app = typer.Typer(help="List and choose the workspaces your credential covers.")
26
+ app.add_typer(accounts_app, name="accounts")
23
27
 
24
28
  pipeline_app.command(name="validate")(validate)
25
29
  pipeline_app.command(name="import")(import_pipeline)
@@ -32,6 +36,8 @@ pipeline_app.command(name="build")(build_pipeline)
32
36
  pipeline_app.command(name="delete")(delete_pipeline)
33
37
  pipeline_app.command(name="migrate")(migrate_pipeline)
34
38
  task_app.command(name="logs")(task_logs)
39
+ accounts_app.command(name="list")(list_accounts)
40
+ accounts_app.command(name="use")(use_account)
35
41
 
36
42
  # Legacy top-level aliases (hidden) - keep the old `orchestra <command>` syntax working
37
43
  # so existing scripts and CI pipelines do not break. Hidden from `--help` to keep the
@@ -1,10 +1,10 @@
1
1
  from pathlib import Path
2
2
 
3
- import httpx
4
3
  import typer
5
4
 
6
5
  from ..utils.api import (
7
- auth_headers,
6
+ account_id_option,
7
+ api_client,
8
8
  fail_with_response,
9
9
  request_or_exit,
10
10
  require_credential,
@@ -32,11 +32,13 @@ def create_pipeline(
32
32
  "--publish/--no-publish",
33
33
  help="Whether the pipeline is published and can be triggered",
34
34
  ),
35
+ account_id: str | None = account_id_option(),
35
36
  ):
36
37
  """
37
38
  Create an Orchestra-backed pipeline from a local YAML file.
38
39
  """
39
40
  require_credential()
41
+ client = api_client(account_id)
40
42
  if path is None:
41
43
  typer.echo(red("Provide --path to create a pipeline from YAML"))
42
44
  raise typer.Exit(code=1)
@@ -50,11 +52,9 @@ def create_pipeline(
50
52
  payload = build_upsert_payload(data, publish, selector)
51
53
 
52
54
  response = request_or_exit(
53
- httpx.post,
55
+ client.post,
54
56
  get_create_pipeline_url(),
55
57
  json=payload,
56
- timeout=30,
57
- headers=auth_headers(),
58
58
  )
59
59
 
60
60
  if response.status_code == 201:
@@ -1,10 +1,10 @@
1
1
  from pathlib import Path
2
2
 
3
- import httpx
4
3
  import typer
5
4
 
6
5
  from ..utils.api import (
7
- auth_headers,
6
+ account_id_option,
7
+ api_client,
8
8
  fail_with_response,
9
9
  request_or_exit,
10
10
  require_credential,
@@ -23,11 +23,13 @@ def delete_pipeline(
23
23
  path: Path | None = pipeline_path_option(),
24
24
  alias: str | None = pipeline_alias_option(),
25
25
  pipeline_id: str | None = pipeline_id_option(),
26
+ account_id: str | None = account_id_option(),
26
27
  ):
27
28
  """
28
29
  Delete a pipeline by selector.
29
30
  """
30
31
  require_credential()
32
+ client = api_client(account_id)
31
33
  selector = resolve_pipeline_selector(alias, pipeline_id, path)
32
34
 
33
35
  if not typer.confirm(f"Delete pipeline ({selector.display()})?"):
@@ -35,11 +37,9 @@ def delete_pipeline(
35
37
  raise typer.Exit(code=1)
36
38
 
37
39
  response = request_or_exit(
38
- httpx.delete,
40
+ client.delete,
39
41
  get_delete_pipeline_url(),
40
42
  params=selector.to_payload(),
41
- timeout=30,
42
- headers=auth_headers(),
43
43
  )
44
44
 
45
45
  if response.status_code == 204:
@@ -1,10 +1,10 @@
1
1
  import json
2
2
 
3
- import httpx
4
3
  import typer
5
4
 
6
5
  from ..utils.api import (
7
- auth_headers,
6
+ account_id_option,
7
+ api_client,
8
8
  fail_with_response,
9
9
  request_or_exit,
10
10
  require_credential,
@@ -13,19 +13,18 @@ from ..utils.constants import get_api_url
13
13
  from ..utils.styling import indent_message, red, yellow
14
14
 
15
15
 
16
- def fetch_pipelines():
16
+ def fetch_pipelines(account_id: str | None = account_id_option()):
17
17
  """
18
18
  Fetch pipelines available to the current Orchestra API key.
19
19
 
20
20
  The API always includes each pipeline's latest run metadata.
21
21
  """
22
22
  require_credential()
23
+ client = api_client(account_id)
23
24
 
24
25
  response = request_or_exit(
25
- httpx.get,
26
+ client.get,
26
27
  get_api_url("pipelines"),
27
- timeout=30,
28
- headers=auth_headers(),
29
28
  )
30
29
 
31
30
  if response.status_code == 200:
@@ -3,11 +3,11 @@ import re
3
3
  from pathlib import Path
4
4
  from typing import cast
5
5
 
6
- import httpx
7
6
  import typer
8
7
 
9
8
  from ..utils.api import (
10
- auth_headers,
9
+ account_id_option,
10
+ api_client,
11
11
  fail_with_response,
12
12
  request_or_exit,
13
13
  require_credential,
@@ -46,19 +46,19 @@ def get_pipeline(
46
46
  path: Path | None = pipeline_path_option(),
47
47
  alias: str | None = pipeline_alias_option(),
48
48
  pipeline_id: str | None = pipeline_id_option(),
49
+ account_id: str | None = account_id_option(),
49
50
  ):
50
51
  """
51
52
  Fetch one pipeline using the shared selector model.
52
53
  """
53
54
  require_credential()
55
+ client = api_client(account_id)
54
56
  selector = resolve_pipeline_selector(alias, pipeline_id, path)
55
57
 
56
58
  response = request_or_exit(
57
- httpx.get,
59
+ client.get,
58
60
  get_pipeline_url(),
59
61
  params=selector.to_payload(),
60
- timeout=30,
61
- headers=auth_headers(),
62
62
  )
63
63
 
64
64
  if response.status_code == 200:
@@ -1,11 +1,11 @@
1
1
  import json
2
2
  from pathlib import Path
3
3
 
4
- import httpx
5
4
  import typer
6
5
 
7
6
  from ..utils.api import (
8
- auth_headers,
7
+ account_id_option,
8
+ api_client,
9
9
  fail_with_response,
10
10
  request_or_exit,
11
11
  require_credential,
@@ -39,11 +39,13 @@ def import_pipeline(
39
39
  "-w",
40
40
  help="Git branch to use for the imported pipeline (defaults to current local branch)",
41
41
  ),
42
+ account_id: str | None = account_id_option(),
42
43
  ):
43
44
  """
44
45
  Create a pipeline in Orchestra by referencing a YAML file in your git repository.
45
46
  """
46
47
  require_credential()
48
+ client = api_client(account_id)
47
49
  if path is None:
48
50
  typer.echo(red("Provide --path to import a pipeline from git"))
49
51
  raise typer.Exit(code=1)
@@ -98,11 +100,9 @@ def import_pipeline(
98
100
  payload["alias"] = alias
99
101
 
100
102
  response = request_or_exit(
101
- httpx.post,
103
+ client.post,
102
104
  get_api_url("pipelines/import"),
103
105
  json=payload,
104
- timeout=30,
105
- headers=auth_headers(),
106
106
  )
107
107
 
108
108
  if response.status_code == 201:
@@ -0,0 +1,16 @@
1
+ import json
2
+
3
+ import typer
4
+
5
+ from ..utils.accounts import fetch_accounts
6
+ from ..utils.api import api_client
7
+ from ..utils.credentials import load_credentials
8
+
9
+
10
+ def list_accounts():
11
+ """List the Orchestra workspaces the current login or API key covers."""
12
+ accounts = fetch_accounts(api_client(scoped=False))
13
+ # Read after fetching, which forgets a dead login whose default no longer applies.
14
+ default_id = (load_credentials() or {}).get("account_id")
15
+ accounts = [{**account, "default": account["id"] == default_id} for account in accounts]
16
+ typer.echo(json.dumps(accounts, indent=2))
@@ -10,9 +10,16 @@ from urllib.parse import parse_qs, urlencode, urlparse
10
10
  import httpx
11
11
  import typer
12
12
 
13
- from ..utils.api import fail_with_response, request_or_exit, token_response_to_credentials
13
+ from ..utils.accounts import fetch_accounts, pick_account
14
+ from ..utils.api import (
15
+ api_client,
16
+ fail_with_response,
17
+ request_or_exit,
18
+ token_response_to_credentials,
19
+ )
14
20
  from ..utils.constants import get_base_url
15
- from ..utils.credentials import save_credentials
21
+ from ..utils.credentials import load_credentials, save_credentials
22
+ from ..utils.picker import can_pick
16
23
  from ..utils.styling import green, red, yellow
17
24
 
18
25
  SCOPES = "orchestra:read orchestra:write offline_access"
@@ -159,4 +166,43 @@ def login():
159
166
  {"client_id": client_id, "token_endpoint": metadata["token_endpoint"]},
160
167
  ),
161
168
  )
162
- typer.echo(green(f"✅ Logged in to {base_url}"))
169
+ _choose_default_account(f"✅ Logged in to {base_url}")
170
+
171
+
172
+ def _choose_default_account(logged_in: str) -> None:
173
+ """Save the workspace later commands act in, so a multi-account login works from the start.
174
+
175
+ The login is already saved, so nothing here can fail it.
176
+ """
177
+ try:
178
+ accounts = fetch_accounts(api_client(scoped=False))
179
+ except typer.Exit:
180
+ typer.echo(green(logged_in))
181
+ typer.echo(yellow("Could not list your accounts, so no default account is set."))
182
+ return
183
+ if len(accounts) == 1:
184
+ _save_default_account(accounts[0])
185
+ typer.echo(green(f"{logged_in} ({accounts[0]['name']})"))
186
+ return
187
+ typer.echo(green(logged_in))
188
+ if not accounts:
189
+ return
190
+ if not can_pick():
191
+ typer.echo(yellow("Your login covers several accounts; run `orchestra accounts use`."))
192
+ return
193
+ typer.echo("Pick a default account:")
194
+ account = pick_account(accounts)
195
+ if account is None:
196
+ typer.echo(yellow("No default account set; run `orchestra accounts use` to pick one."))
197
+ return
198
+ _save_default_account(account)
199
+ typer.echo(green(f"Default account: {account['name']}"))
200
+
201
+
202
+ def _save_default_account(account: dict) -> None:
203
+ # Read again after fetching, which may have refreshed and rotated the saved tokens.
204
+ credentials = load_credentials()
205
+ if credentials is not None:
206
+ save_credentials(
207
+ {**credentials, "account_id": account["id"], "account_name": account["name"]},
208
+ )
@@ -5,7 +5,13 @@ from typing import Any
5
5
  import httpx
6
6
  import typer
7
7
 
8
- from ..utils.api import auth_headers, fail_with_response, request_or_exit, require_credential
8
+ from ..utils.api import (
9
+ account_id_option,
10
+ api_client,
11
+ fail_with_response,
12
+ request_or_exit,
13
+ require_credential,
14
+ )
9
15
  from ..utils.constants import get_api_url
10
16
  from ..utils.git import (
11
17
  GitAction,
@@ -121,17 +127,19 @@ def _choose_migration_version(existing_pipeline: dict[str, object], force: bool)
121
127
  raise typer.Exit(code=1)
122
128
 
123
129
 
124
- def _download_pipeline_yaml(selector: PipelineSelector, version: int | None) -> str:
130
+ def _download_pipeline_yaml(
131
+ client: httpx.Client,
132
+ selector: PipelineSelector,
133
+ version: int | None,
134
+ ) -> str:
125
135
  params = selector.to_payload()
126
136
  if version is not None:
127
137
  params["version"] = str(version)
128
138
 
129
139
  response = request_or_exit(
130
- httpx.get,
140
+ client.get,
131
141
  get_api_url("pipeline/data"),
132
142
  params=params,
133
- timeout=30,
134
- headers=auth_headers(),
135
143
  )
136
144
  if response.status_code != 200:
137
145
  raise fail_with_response("Migrate", response)
@@ -340,11 +348,13 @@ def migrate_pipeline(
340
348
  "--force/--no-force",
341
349
  help="Skip interactive prompts and continue with inferred migration choices",
342
350
  ),
351
+ account_id: str | None = account_id_option(),
343
352
  ) -> None:
344
353
  """
345
354
  Migrate an Orchestra-backed pipeline to git-backed storage.
346
355
  """
347
356
  require_credential()
357
+ client = api_client(account_id)
348
358
  selector = _resolve_migrate_selector(alias, pipeline_id)
349
359
 
350
360
  repo_root = require_repo_root(path, GitAction.MIGRATE)
@@ -370,7 +380,7 @@ def migrate_pipeline(
370
380
  typer.echo(red("Could not detect current branch from git"))
371
381
  raise typer.Exit(code=1)
372
382
 
373
- existing_pipeline = lookup_existing_pipeline(selector, "Migrate")
383
+ existing_pipeline = lookup_existing_pipeline(client, selector, "Migrate")
374
384
  if existing_pipeline is None:
375
385
  typer.echo(red("❌ Migrate failed: pipeline lookup returned no pipeline"))
376
386
  raise typer.Exit(code=1)
@@ -382,7 +392,7 @@ def migrate_pipeline(
382
392
  raise typer.Exit(code=1)
383
393
 
384
394
  target_version = _choose_migration_version(existing_pipeline, force)
385
- downloaded_yaml = _download_pipeline_yaml(selector, target_version)
395
+ downloaded_yaml = _download_pipeline_yaml(client, selector, target_version)
386
396
  target_path, selected_yaml = _resolve_target_path_and_yaml(path, downloaded_yaml, force)
387
397
  relative_path = ensure_repo_relative_path(target_path, repo_root, GitAction.MIGRATE)
388
398
  _write_yaml(target_path, selected_yaml)
@@ -406,12 +416,10 @@ def migrate_pipeline(
406
416
  payload["working_branch"] = effective_working_branch
407
417
 
408
418
  response = request_or_exit(
409
- httpx.patch,
419
+ client.patch,
410
420
  get_api_url("pipelines/storage-settings"),
411
421
  params=selector.to_payload(),
412
422
  json=payload,
413
- timeout=30,
414
- headers=auth_headers(),
415
423
  )
416
424
  if not (200 <= response.status_code < 300):
417
425
  raise fail_with_response("Migrate", response)