orchestra-cli 0.5.2__tar.gz → 0.5.3__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.5.2 → orchestra_cli-0.5.3}/PKG-INFO +1 -1
  2. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/login.py +4 -0
  3. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/migrate_pipeline.py +1 -1
  4. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/run_pipeline.py +3 -2
  5. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/validate_pipeline.py +9 -3
  6. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_migrate_pipeline.py +8 -8
  7. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_run_pipeline.py +4 -4
  8. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/utils/test_api.py +61 -1
  9. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/api.py +27 -3
  10. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/yaml_loader.py +8 -2
  11. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli.egg-info/PKG-INFO +1 -1
  12. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/pyproject.toml +1 -1
  13. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/README.md +0 -0
  14. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/__init__.py +0 -0
  15. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/build_pipeline.py +0 -0
  16. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/cli.py +0 -0
  17. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/create_pipeline.py +0 -0
  18. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/delete_pipeline.py +0 -0
  19. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/fetch_pipelines.py +0 -0
  20. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/get_pipeline.py +0 -0
  21. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/import_pipeline.py +0 -0
  22. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/list_accounts.py +0 -0
  23. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/login_page.html +0 -0
  24. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/pipeline_upsert.py +0 -0
  25. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/task_logs.py +0 -0
  26. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/update_pipeline.py +0 -0
  27. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/src/use_account.py +0 -0
  28. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/__init__.py +0 -0
  29. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/conftest.py +0 -0
  30. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_build_pipeline.py +0 -0
  31. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_cli.py +0 -0
  32. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_create_pipeline.py +0 -0
  33. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_delete_pipeline.py +0 -0
  34. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_fetch_pipelines.py +0 -0
  35. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_get_pipeline.py +0 -0
  36. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_import_pipeline.py +0 -0
  37. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_list_accounts.py +0 -0
  38. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_login.py +0 -0
  39. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_pipeline_selector.py +0 -0
  40. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_task_logs.py +0 -0
  41. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_update_pipeline.py +0 -0
  42. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_use_account.py +0 -0
  43. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/test_validate_pipeline.py +0 -0
  44. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/utils/__init__.py +0 -0
  45. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/utils/test_git.py +0 -0
  46. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/tests/utils/test_picker.py +0 -0
  47. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/accounts.py +0 -0
  48. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/constants.py +0 -0
  49. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/credentials.py +0 -0
  50. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/git.py +0 -0
  51. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/picker.py +0 -0
  52. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/pipeline_lookup.py +0 -0
  53. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/pipeline_selector.py +0 -0
  54. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/pipeline_update.py +0 -0
  55. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli/utils/styling.py +0 -0
  56. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli.egg-info/SOURCES.txt +0 -0
  57. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli.egg-info/dependency_links.txt +0 -0
  58. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli.egg-info/entry_points.txt +0 -0
  59. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli.egg-info/requires.txt +0 -0
  60. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/orchestra_cli.egg-info/top_level.txt +0 -0
  61. {orchestra_cli-0.5.2 → orchestra_cli-0.5.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestra-cli
3
- Version: 0.5.2
3
+ Version: 0.5.3
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,7 @@ from ..utils.api import (
15
15
  api_client,
16
16
  fail_with_response,
17
17
  request_or_exit,
18
+ support_id_headers,
18
19
  token_response_to_credentials,
19
20
  )
20
21
  from ..utils.constants import get_base_url
@@ -83,6 +84,7 @@ def login():
83
84
  response = request_or_exit(
84
85
  httpx.get,
85
86
  f"{base_url}/.well-known/oauth-authorization-server",
87
+ headers=support_id_headers(),
86
88
  timeout=30,
87
89
  )
88
90
  if response.status_code != 200:
@@ -111,6 +113,7 @@ def login():
111
113
  "response_types": ["code"],
112
114
  "token_endpoint_auth_method": "none",
113
115
  },
116
+ headers=support_id_headers(),
114
117
  timeout=30,
115
118
  )
116
119
  if response.status_code not in (200, 201):
@@ -151,6 +154,7 @@ def login():
151
154
  "client_id": client_id,
152
155
  "code_verifier": verifier,
153
156
  },
157
+ headers=support_id_headers(),
154
158
  timeout=30,
155
159
  )
156
160
  if response.status_code != 200:
@@ -138,7 +138,7 @@ def _download_pipeline_yaml(
138
138
 
139
139
  response = request_or_exit(
140
140
  client.get,
141
- get_api_url("pipeline/data"),
141
+ get_api_url("pipelines/data"),
142
142
  params=params,
143
143
  )
144
144
  if response.status_code != 200:
@@ -18,6 +18,7 @@ from ..utils.api import (
18
18
  fail_with_response,
19
19
  request_or_exit,
20
20
  require_credential,
21
+ response_error_text,
21
22
  )
22
23
  from ..utils.constants import get_api_url, get_base_url
23
24
  from ..utils.git import GitAction, confirm_git_warnings_or_exit, prepare_git_backed_run_target
@@ -163,7 +164,7 @@ def _load_pipeline_data_for_task_resolution(
163
164
 
164
165
  response = request_or_exit(
165
166
  client.get,
166
- get_api_url("pipeline/data"),
167
+ get_api_url("pipelines/data"),
167
168
  params=selector.to_payload(),
168
169
  )
169
170
  if response.status_code != 200:
@@ -535,7 +536,7 @@ def _poll_until_terminal(
535
536
  poll_status_display = None
536
537
  typer.echo(red(f"❌ Status check failed with HTTP {status_resp.status_code}"))
537
538
  try:
538
- typer.echo(yellow(indent_message(status_resp.text)))
539
+ typer.echo(yellow(indent_message(response_error_text(status_resp))))
539
540
  except Exception:
540
541
  pass
541
542
  raise typer.Exit(code=1)
@@ -6,7 +6,7 @@ import httpx
6
6
  import typer
7
7
  import yaml
8
8
 
9
- from ..utils.api import request_or_exit
9
+ from ..utils.api import request_or_exit, response_error_text, support_id_headers
10
10
  from ..utils.constants import get_api_url
11
11
  from ..utils.styling import bold, green, indent_message, red, yellow
12
12
  from ..utils.yaml_loader import echo_invalid_yaml, exit_if_unsupported_extension, load_yaml
@@ -67,7 +67,13 @@ def validate(
67
67
  echo_invalid_yaml(file, err)
68
68
  raise typer.Exit(code=1)
69
69
 
70
- response = request_or_exit(httpx.post, get_api_url("pipelines/schema"), json=data, timeout=10)
70
+ response = request_or_exit(
71
+ httpx.post,
72
+ get_api_url("pipelines/schema"),
73
+ json=data,
74
+ headers=support_id_headers(),
75
+ timeout=10,
76
+ )
71
77
 
72
78
  if response.status_code == 200:
73
79
  typer.echo(green("✅ Validation passed!"))
@@ -82,5 +88,5 @@ def validate(
82
88
  else:
83
89
  typer.echo(errors)
84
90
  except Exception:
85
- typer.echo(response.text)
91
+ typer.echo(response_error_text(response))
86
92
  raise typer.Exit(code=1)
@@ -38,7 +38,7 @@ def test_migrate_success_uses_default_branch_without_working_branch(
38
38
  )
39
39
  httpx_mock.add_response(
40
40
  method="GET",
41
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=3",
41
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=3",
42
42
  text="name: remote\nversion: 1\n",
43
43
  status_code=200,
44
44
  )
@@ -100,7 +100,7 @@ def test_migrate_prompts_for_latest_version_and_prints_compare_link(
100
100
  )
101
101
  httpx_mock.add_response(
102
102
  method="GET",
103
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=5",
103
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=5",
104
104
  text="name: remote\n",
105
105
  status_code=200,
106
106
  )
@@ -170,7 +170,7 @@ def test_migrate_uses_local_file_when_conflict_choice_is_local(
170
170
  )
171
171
  httpx_mock.add_response(
172
172
  method="GET",
173
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=1",
173
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=1",
174
174
  text="name: remote\n",
175
175
  status_code=200,
176
176
  )
@@ -271,7 +271,7 @@ def test_migrate_branch_protection_offer_retries_with_suggested_branch(
271
271
  )
272
272
  httpx_mock.add_response(
273
273
  method="GET",
274
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=1",
274
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=1",
275
275
  text="name: remote\n",
276
276
  status_code=200,
277
277
  )
@@ -337,7 +337,7 @@ def test_migrate_accepts_nonexistent_parent_directory_inside_repo(
337
337
  )
338
338
  httpx_mock.add_response(
339
339
  method="GET",
340
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=1",
340
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=1",
341
341
  text="name: remote\n",
342
342
  status_code=200,
343
343
  )
@@ -398,7 +398,7 @@ def test_migrate_force_skips_version_prompt(monkeypatch, tmp_path: Path, httpx_m
398
398
  )
399
399
  httpx_mock.add_response(
400
400
  method="GET",
401
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=2",
401
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=2",
402
402
  text="name: remote\n",
403
403
  status_code=200,
404
404
  )
@@ -469,7 +469,7 @@ def test_migrate_force_skips_branch_recovery_prompt(
469
469
  )
470
470
  httpx_mock.add_response(
471
471
  method="GET",
472
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=1",
472
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=1",
473
473
  text="name: remote\n",
474
474
  status_code=200,
475
475
  )
@@ -536,7 +536,7 @@ def test_migrate_fails_before_writing_when_reprompted_path_is_outside_repo(
536
536
  )
537
537
  httpx_mock.add_response(
538
538
  method="GET",
539
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo&version=1",
539
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo&version=1",
540
540
  text="name: remote\n",
541
541
  status_code=200,
542
542
  )
@@ -288,7 +288,7 @@ def test_run_with_task_and_overrides(httpx_mock: HTTPXMock, monkeypatch, tmp_pat
288
288
 
289
289
  httpx_mock.add_response(
290
290
  method="GET",
291
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo",
291
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo",
292
292
  text="\n".join(
293
293
  [
294
294
  "name: demo",
@@ -366,7 +366,7 @@ def test_run_task_name_resolves_from_remote_pipeline_data(
366
366
 
367
367
  httpx_mock.add_response(
368
368
  method="GET",
369
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo",
369
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo",
370
370
  text="\n".join(
371
371
  [
372
372
  "name: demo",
@@ -419,7 +419,7 @@ def test_run_task_name_resolves_from_remote_json_pipeline_definition(
419
419
 
420
420
  httpx_mock.add_response(
421
421
  method="GET",
422
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo",
422
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo",
423
423
  json={
424
424
  "name": "demo",
425
425
  "pipeline": {
@@ -471,7 +471,7 @@ def test_run_task_name_ignores_metadata_only_wrapper_and_uses_top_level_pipeline
471
471
 
472
472
  httpx_mock.add_response(
473
473
  method="GET",
474
- url="https://app.getorchestra.io/api/engine/public/pipeline/data?alias=demo",
474
+ url="https://app.getorchestra.io/api/engine/public/pipelines/data?alias=demo",
475
475
  json={
476
476
  "data": {"page": 1, "total": 1},
477
477
  "pipeline": {
@@ -1,3 +1,4 @@
1
+ import re
1
2
  import time
2
3
  from types import SimpleNamespace
3
4
  from urllib.parse import parse_qs
@@ -8,7 +9,7 @@ import typer
8
9
  from pytest_httpx import HTTPXMock
9
10
 
10
11
  import orchestra_cli.utils.api as api_module
11
- from orchestra_cli.utils.api import require_credential
12
+ from orchestra_cli.utils.api import SUPPORT_ID_HEADER, api_client, require_credential
12
13
  from orchestra_cli.utils.credentials import load_credentials, save_credentials
13
14
 
14
15
  TOKEN_ENDPOINT = "https://app.getorchestra.io/oauth/token"
@@ -125,6 +126,25 @@ def test_refresh_server_error_is_not_reported_as_expired_login(httpx_mock: HTTPX
125
126
  assert "expired" not in output
126
127
 
127
128
 
129
+ def test_waf_blocked_refresh_keeps_login_and_shows_support_id(httpx_mock: HTTPXMock, capsys):
130
+ cache_login(time.time() - 1)
131
+ httpx_mock.add_response(
132
+ method="POST",
133
+ url=TOKEN_ENDPOINT,
134
+ status_code=403,
135
+ headers={"content-type": "text/html"},
136
+ text="<html><head><title>403 Forbidden</title></head></html>",
137
+ )
138
+
139
+ with pytest.raises(typer.Exit):
140
+ require_credential()
141
+ request = httpx_mock.get_request(url=TOKEN_ENDPOINT)
142
+ assert request is not None
143
+ support_id = request.headers[SUPPORT_ID_HEADER]
144
+ assert f"Contact Orchestra support with ID: {support_id}" in capsys.readouterr().out
145
+ assert load_credentials() is not None
146
+
147
+
128
148
  def test_rejected_refresh_uses_token_another_process_just_rotated_in(httpx_mock: HTTPXMock):
129
149
  cache_login(time.time() - 1)
130
150
 
@@ -227,3 +247,43 @@ def test_other_errors_get_no_account_hint(response, capsys):
227
247
  api_module.fail_with_response("Fetch pipelines", response)
228
248
 
229
249
  assert ACCOUNT_HINT not in capsys.readouterr().out
250
+
251
+
252
+ def test_requests_carry_a_support_id(httpx_mock: HTTPXMock, monkeypatch):
253
+ monkeypatch.setenv("ORCHESTRA_API_KEY", "account-key")
254
+ httpx_mock.add_response(url="https://app.getorchestra.io/api/engine/public/pipelines")
255
+
256
+ api_client().get("https://app.getorchestra.io/api/engine/public/pipelines")
257
+
258
+ request = httpx_mock.get_request()
259
+ assert request is not None
260
+ assert re.fullmatch(r"[0-9a-f]{12}", request.headers[SUPPORT_ID_HEADER])
261
+
262
+
263
+ def test_waf_block_shows_support_id_instead_of_its_page(capsys):
264
+ response = httpx.Response(
265
+ 403,
266
+ headers={"content-type": "text/html"},
267
+ text="<html><head><title>403 Forbidden</title></head></html>",
268
+ request=httpx.Request(
269
+ "GET",
270
+ "https://app.getorchestra.io",
271
+ headers={SUPPORT_ID_HEADER: "0123456789ab"},
272
+ ),
273
+ )
274
+
275
+ api_module.fail_with_response("Fetch pipelines", response)
276
+
277
+ out = capsys.readouterr().out
278
+ assert "Your request was blocked. Contact Orchestra support with ID: 0123456789ab" in out
279
+ assert "<html>" not in out
280
+
281
+
282
+ def test_json_403_is_not_treated_as_waf_block(capsys):
283
+ response = httpx.Response(403, json={"detail": "Not allowed"})
284
+
285
+ api_module.fail_with_response("Fetch pipelines", response)
286
+
287
+ out = capsys.readouterr().out
288
+ assert "Not allowed" in out
289
+ assert "blocked" not in out
@@ -8,6 +8,7 @@ transport errors, or hand-roll JSON-vs-text error rendering themselves.
8
8
 
9
9
  import json
10
10
  import os
11
+ import secrets
11
12
  import time
12
13
  from collections.abc import Callable
13
14
  from typing import Any
@@ -22,6 +23,8 @@ from .styling import indent_message, red, yellow
22
23
  # Refresh this long before expiry so a token cannot lapse mid-request.
23
24
  _REFRESH_MARGIN_SECONDS = 60
24
25
 
26
+ SUPPORT_ID_HEADER = "X-Orchestra-Support-Id"
27
+
25
28
 
26
29
  def account_id_option() -> Any:
27
30
  """Return the ``--account-id`` option, whose value a command passes to ``api_client()``."""
@@ -33,6 +36,11 @@ def account_id_option() -> Any:
33
36
  )
34
37
 
35
38
 
39
+ def support_id_headers() -> dict[str, str]:
40
+ """Tag a request with a fresh ID that support can look up in the WAF logs."""
41
+ return {SUPPORT_ID_HEADER: secrets.token_hex(6)}
42
+
43
+
36
44
  def require_credential() -> str:
37
45
  """Return the bearer credential for API calls, or exit with code 1.
38
46
 
@@ -77,6 +85,7 @@ def _refresh(credentials: dict) -> dict | None:
77
85
  "refresh_token": credentials["refresh_token"],
78
86
  "client_id": credentials["client_id"],
79
87
  },
88
+ headers=support_id_headers(),
80
89
  timeout=30,
81
90
  )
82
91
  except httpx.HTTPError as e:
@@ -85,7 +94,7 @@ def _refresh(credentials: dict) -> dict | None:
85
94
  return credentials
86
95
  typer.echo(red(f"HTTP request failed: {e}"))
87
96
  raise typer.Exit(code=1)
88
- if 400 <= response.status_code < 500:
97
+ if 400 <= response.status_code < 500 and not is_waf_block(response):
89
98
  # Another CLI process may have refreshed first, rotating the refresh token
90
99
  # this one sent; its saved result is then still good. Otherwise the login
91
100
  # itself is unusable (invalid_grant, or a registration that has since expired)
@@ -128,6 +137,7 @@ class _OrchestraAuth(httpx.Auth):
128
137
  self._scoped = scoped
129
138
 
130
139
  def auth_flow(self, request: httpx.Request):
140
+ request.headers.update(support_id_headers())
131
141
  request.headers["Authorization"] = f"Bearer {require_credential()}"
132
142
  # After require_credential(), which drops a dead login, so a key never gets its default.
133
143
  if self._scoped:
@@ -172,8 +182,22 @@ def echo_response_error_body(response: httpx.Response) -> None:
172
182
  return
173
183
  except Exception:
174
184
  pass
175
- if response.text:
176
- typer.echo(yellow(indent_message(response.text)))
185
+ text = response_error_text(response)
186
+ if text:
187
+ typer.echo(yellow(indent_message(text)))
188
+
189
+
190
+ def is_waf_block(response: httpx.Response) -> bool:
191
+ """Orchestra's own 403s are JSON, so an HTML 403 is the WAF's block page."""
192
+ return response.status_code == 403 and "text/html" in response.headers.get("content-type", "")
193
+
194
+
195
+ def response_error_text(response: httpx.Response) -> str:
196
+ """Return the body, or the support ID to quote if the WAF blocked the request."""
197
+ if is_waf_block(response):
198
+ support_id = response.request.headers.get(SUPPORT_ID_HEADER)
199
+ return f"Your request was blocked. Contact Orchestra support with ID: {support_id}"
200
+ return response.text
177
201
 
178
202
 
179
203
  def fail_with_response(action: str, response: httpx.Response) -> typer.Exit:
@@ -13,6 +13,7 @@ import httpx
13
13
  import typer
14
14
  import yaml
15
15
 
16
+ from .api import response_error_text, support_id_headers
16
17
  from .constants import get_api_url
17
18
  from .styling import indent_message, red, yellow
18
19
 
@@ -114,7 +115,12 @@ def load_yaml(file: Path) -> tuple[dict | None, str | None]:
114
115
  def validate_yaml_with_api(data: dict) -> tuple[bool, str | None]:
115
116
  """POST a YAML payload to the schema endpoint and return ``(ok, err_message)``."""
116
117
  try:
117
- response = httpx.post(get_api_url("pipelines/schema"), json=data, timeout=15)
118
+ response = httpx.post(
119
+ get_api_url("pipelines/schema"),
120
+ json=data,
121
+ headers=support_id_headers(),
122
+ timeout=15,
123
+ )
118
124
  except Exception as e:
119
125
  return False, f"HTTP request failed: {e}"
120
126
 
@@ -124,7 +130,7 @@ def validate_yaml_with_api(data: dict) -> tuple[bool, str | None]:
124
130
  errors = response.json()
125
131
  return False, json.dumps(errors, indent=2)
126
132
  except Exception:
127
- return False, response.text
133
+ return False, response_error_text(response)
128
134
 
129
135
 
130
136
  def load_validated_pipeline_data(path: Path) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestra-cli
3
- Version: 0.5.2
3
+ Version: 0.5.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "orchestra-cli"
3
- version = "0.5.2"
3
+ version = "0.5.3"
4
4
  description = "Orchestra CLI. Perform operations with Orchestra locally."
5
5
  readme = "README.md"
6
6
  authors = [
File without changes
File without changes