blobhub-cli 0.5.0__tar.gz → 0.7.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.
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/PKG-INFO +45 -4
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/README.md +44 -3
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/__init__.py +1 -1
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/client.py +28 -8
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/commands.py +48 -16
- blobhub_cli-0.7.0/src/blobhub_cli/api/errors.py +90 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/blob/reference.py +10 -1
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/cli/app.py +78 -7
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/codes.py +6 -1
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/auth.py +2 -3
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/blob.py +14 -25
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/doctor.py +3 -3
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/eject.py +5 -1
- blobhub_cli-0.7.0/src/blobhub_cli/commands/events.py +326 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/execute.py +30 -18
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/scheduler.py +70 -55
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/workflow.py +115 -22
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/allowlist.py +23 -18
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/sandbox.py +3 -3
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/settings.py +16 -7
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/console.py +13 -3
- blobhub_cli-0.7.0/src/blobhub_cli/definition/listener.py +325 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/model.py +2 -1
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/exit.py +4 -2
- blobhub_cli-0.7.0/src/blobhub_cli/limits.py +51 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/scheduler.py +55 -11
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/references.py +29 -19
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/definitions.py +23 -7
- blobhub_cli-0.7.0/src/blobhub_cli/workflow/event_filter.py +140 -0
- blobhub_cli-0.7.0/src/blobhub_cli/workflow/events.py +97 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/executions.py +37 -25
- blobhub_cli-0.7.0/src/blobhub_cli/workflow/follow.py +100 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/reference.py +21 -10
- blobhub_cli-0.7.0/src/blobhub_cli/workflow/session_reference.py +89 -0
- blobhub_cli-0.5.0/src/blobhub_cli/api/errors.py +0 -51
- blobhub_cli-0.5.0/src/blobhub_cli/limits.py +0 -32
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/.gitignore +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/LICENSE +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/pyproject.toml +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/__main__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/blob/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/cli/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/completion.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/emit.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/graph.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/credentials.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/sources.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/drift.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/eject.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/hashing.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/formats/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/formats/io.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/ids.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/base.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/resolver.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/workflow.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/__init__.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/schedules.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/statestore.py +0 -0
- {blobhub_cli-0.5.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blobhub-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: BlobHub command line interface — work with blobs, revisions, and their contents from the terminal
|
|
5
5
|
Project-URL: Homepage, https://blobhub.io/
|
|
6
6
|
Project-URL: Documentation, https://docs.blobhub.io/
|
|
@@ -106,6 +106,9 @@ For CI, skip the credentials file entirely — set `BLOBHUB_API_KEY` (and option
|
|
|
106
106
|
`BLOBHUB_PROFILE`); the environment always wins over a stored profile, and a key from the environment alone
|
|
107
107
|
works with no credentials file present at all.
|
|
108
108
|
|
|
109
|
+
The root `--profile` selects a stored profile for any command (`blobhub --profile staging workflow events …`),
|
|
110
|
+
as `BLOBHUB_PROFILE` does. An exported `BLOBHUB_API_KEY` or `BLOBHUB_API_URL` still wins over the profile it names.
|
|
111
|
+
|
|
109
112
|
`login` itself can also run non-interactively and still store a profile, which the environment-only approach
|
|
110
113
|
above does not:
|
|
111
114
|
|
|
@@ -178,9 +181,10 @@ blobhub workflow deploy -f manifest.yaml
|
|
|
178
181
|
`deploy` builds every bound component locally (inlining local imports from `pkg/`, stripping sandbox-pre-bound
|
|
179
182
|
and allowlisted imports, rejecting anything else), uploads only the definitions whose serialized document
|
|
180
183
|
actually changed, and runs the platform's `check_definition` against each workflow definition it uploaded.
|
|
181
|
-
Re-running `deploy` with nothing changed uploads nothing and says so. `--dry-run` on
|
|
182
|
-
prints the exact intended calls without performing them; `--json` emits a single
|
|
183
|
-
of the rendered output.
|
|
184
|
+
Re-running `deploy` with nothing changed uploads nothing and says so. `--dry-run` (on `workflow pull|push|build|deploy|
|
|
185
|
+
prune` and `scheduler deploy`) prints the exact intended calls without performing them; `--json` emits a single
|
|
186
|
+
machine-readable object instead of the rendered output. The exception is `workflow events --follow --json`, which
|
|
187
|
+
writes JSON Lines, one `{"event", "schema_version"}` object per event and no final object.
|
|
184
188
|
|
|
185
189
|
Other commands in the `workflow` group:
|
|
186
190
|
|
|
@@ -196,6 +200,43 @@ it with `REMOTE_EDIT` (or `UNMANAGED_CODE` for hand-typed code the CLI never bui
|
|
|
196
200
|
or reconcile by hand first. See `docs/reference/error-codes.md` for the full catalog of error and advisory codes,
|
|
197
201
|
what each one means, and how to resolve it.
|
|
198
202
|
|
|
203
|
+
### Listeners and session events
|
|
204
|
+
|
|
205
|
+
A **listener** is a definition with `category: listener` that acts on a session's event stream: it filters events,
|
|
206
|
+
batches them, and invokes a workflow, a thread item or an object write. It lives in the manifest beside the
|
|
207
|
+
workflows and deploys, diffs, pulls and checks like any other definition. The source file holds the whole body
|
|
208
|
+
(`type: listener_definition`, `filter`, `batch`, `concurrency`, `invocation_target_type`, `invocation_target`,
|
|
209
|
+
`receipt`); a listener entry takes no `components`.
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
definitions:
|
|
213
|
+
- alias: orchestrate
|
|
214
|
+
category: listener # or inferred from `type: listener_definition` in the file
|
|
215
|
+
source: listeners/orchestrate.yaml
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A session is named as `<session>` (with `-f manifest.yaml` naming its blob and revision),
|
|
219
|
+
`<blob-id>/<revision>/<session>` or `<org>/<blob>/<revision>/<session>`; a qualified reference takes no `-f`.
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
blobhub workflow events my-session -f manifest.yaml # newest 20 events
|
|
223
|
+
blobhub workflow events my-session -f manifest.yaml --types object.written --aliases 'worker/**' --follow
|
|
224
|
+
blobhub workflow events my-session -f manifest.yaml --listener orchestrate --after 2026-11-09T17:00:00Z
|
|
225
|
+
blobhub workflow events my-session -f manifest.yaml --cursor <token> # continue the page a listing printed
|
|
226
|
+
blobhub workflow listeners my-session -f manifest.yaml [--listener orchestrate]
|
|
227
|
+
blobhub workflow listener disable my-session orchestrate -f manifest.yaml # enable | disable, for this session
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`--types`, `--aliases` and `--executions` repeat, and take globs where the platform does. A listing ends with
|
|
231
|
+
`more: --cursor <token>` when there is another page; the token carries its own filter and direction, so pass it alone.
|
|
232
|
+
`--follow` prints new events until interrupted without missing a late commit. `workflow execute --watch` follows the
|
|
233
|
+
run's log the same way and ends once the platform reports the execution finished.
|
|
234
|
+
|
|
235
|
+
With `--json`, the new commands print these keys: `workflow events` `{session, events, cursor}`; `workflow listeners`
|
|
236
|
+
`{session_id, listeners, connections}`, or with `--listener` `{session_id, listener, executions, last_execution_id}`;
|
|
237
|
+
`workflow listener enable|disable` `{session_id, listener}`. `workflow diff` adds `body_changed` and `validation_error`
|
|
238
|
+
to a listener's report, and `scheduler ls` adds `invocation_target_type` and `target_alias` to each schedule.
|
|
239
|
+
|
|
199
240
|
## License
|
|
200
241
|
|
|
201
242
|
MIT. See `LICENSE`.
|
|
@@ -53,6 +53,9 @@ For CI, skip the credentials file entirely — set `BLOBHUB_API_KEY` (and option
|
|
|
53
53
|
`BLOBHUB_PROFILE`); the environment always wins over a stored profile, and a key from the environment alone
|
|
54
54
|
works with no credentials file present at all.
|
|
55
55
|
|
|
56
|
+
The root `--profile` selects a stored profile for any command (`blobhub --profile staging workflow events …`),
|
|
57
|
+
as `BLOBHUB_PROFILE` does. An exported `BLOBHUB_API_KEY` or `BLOBHUB_API_URL` still wins over the profile it names.
|
|
58
|
+
|
|
56
59
|
`login` itself can also run non-interactively and still store a profile, which the environment-only approach
|
|
57
60
|
above does not:
|
|
58
61
|
|
|
@@ -125,9 +128,10 @@ blobhub workflow deploy -f manifest.yaml
|
|
|
125
128
|
`deploy` builds every bound component locally (inlining local imports from `pkg/`, stripping sandbox-pre-bound
|
|
126
129
|
and allowlisted imports, rejecting anything else), uploads only the definitions whose serialized document
|
|
127
130
|
actually changed, and runs the platform's `check_definition` against each workflow definition it uploaded.
|
|
128
|
-
Re-running `deploy` with nothing changed uploads nothing and says so. `--dry-run` on
|
|
129
|
-
prints the exact intended calls without performing them; `--json` emits a single
|
|
130
|
-
of the rendered output.
|
|
131
|
+
Re-running `deploy` with nothing changed uploads nothing and says so. `--dry-run` (on `workflow pull|push|build|deploy|
|
|
132
|
+
prune` and `scheduler deploy`) prints the exact intended calls without performing them; `--json` emits a single
|
|
133
|
+
machine-readable object instead of the rendered output. The exception is `workflow events --follow --json`, which
|
|
134
|
+
writes JSON Lines, one `{"event", "schema_version"}` object per event and no final object.
|
|
131
135
|
|
|
132
136
|
Other commands in the `workflow` group:
|
|
133
137
|
|
|
@@ -143,6 +147,43 @@ it with `REMOTE_EDIT` (or `UNMANAGED_CODE` for hand-typed code the CLI never bui
|
|
|
143
147
|
or reconcile by hand first. See `docs/reference/error-codes.md` for the full catalog of error and advisory codes,
|
|
144
148
|
what each one means, and how to resolve it.
|
|
145
149
|
|
|
150
|
+
### Listeners and session events
|
|
151
|
+
|
|
152
|
+
A **listener** is a definition with `category: listener` that acts on a session's event stream: it filters events,
|
|
153
|
+
batches them, and invokes a workflow, a thread item or an object write. It lives in the manifest beside the
|
|
154
|
+
workflows and deploys, diffs, pulls and checks like any other definition. The source file holds the whole body
|
|
155
|
+
(`type: listener_definition`, `filter`, `batch`, `concurrency`, `invocation_target_type`, `invocation_target`,
|
|
156
|
+
`receipt`); a listener entry takes no `components`.
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
definitions:
|
|
160
|
+
- alias: orchestrate
|
|
161
|
+
category: listener # or inferred from `type: listener_definition` in the file
|
|
162
|
+
source: listeners/orchestrate.yaml
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A session is named as `<session>` (with `-f manifest.yaml` naming its blob and revision),
|
|
166
|
+
`<blob-id>/<revision>/<session>` or `<org>/<blob>/<revision>/<session>`; a qualified reference takes no `-f`.
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
blobhub workflow events my-session -f manifest.yaml # newest 20 events
|
|
170
|
+
blobhub workflow events my-session -f manifest.yaml --types object.written --aliases 'worker/**' --follow
|
|
171
|
+
blobhub workflow events my-session -f manifest.yaml --listener orchestrate --after 2026-11-09T17:00:00Z
|
|
172
|
+
blobhub workflow events my-session -f manifest.yaml --cursor <token> # continue the page a listing printed
|
|
173
|
+
blobhub workflow listeners my-session -f manifest.yaml [--listener orchestrate]
|
|
174
|
+
blobhub workflow listener disable my-session orchestrate -f manifest.yaml # enable | disable, for this session
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`--types`, `--aliases` and `--executions` repeat, and take globs where the platform does. A listing ends with
|
|
178
|
+
`more: --cursor <token>` when there is another page; the token carries its own filter and direction, so pass it alone.
|
|
179
|
+
`--follow` prints new events until interrupted without missing a late commit. `workflow execute --watch` follows the
|
|
180
|
+
run's log the same way and ends once the platform reports the execution finished.
|
|
181
|
+
|
|
182
|
+
With `--json`, the new commands print these keys: `workflow events` `{session, events, cursor}`; `workflow listeners`
|
|
183
|
+
`{session_id, listeners, connections}`, or with `--listener` `{session_id, listener, executions, last_execution_id}`;
|
|
184
|
+
`workflow listener enable|disable` `{session_id, listener}`. `workflow diff` adds `body_changed` and `validation_error`
|
|
185
|
+
to a listener's report, and `scheduler ls` adds `invocation_target_type` and `target_alias` to each schedule.
|
|
186
|
+
|
|
146
187
|
## License
|
|
147
188
|
|
|
148
189
|
MIT. See `LICENSE`.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Contract: `get`, `query`, `command`, and `raw_query` route every response through `_handle`,
|
|
4
4
|
which maps HTTP status and the body's `status` field onto the `api.errors` hierarchy;
|
|
5
5
|
`raw_query` alone skips the status gate, and `probe` skips both the gate and the retry loop.
|
|
6
|
+
`get`, `query` and `raw_query` retry every transient failure; `command` retries only
|
|
7
|
+
`errors.NEVER_RAN`, because any other failure can follow a write the platform already committed.
|
|
6
8
|
Neither `query`, `command`, nor `raw_query` names which component serves a data-channel call --
|
|
7
9
|
the target revision's blob type decides that server-side, so there is nothing for a caller to
|
|
8
10
|
supply here at all, even for a scheduler `deploy` that legitimately writes schedules against one
|
|
@@ -20,19 +22,29 @@ import time
|
|
|
20
22
|
import httpx
|
|
21
23
|
|
|
22
24
|
from blobhub_cli.api.errors import (
|
|
25
|
+
NEVER_RAN,
|
|
26
|
+
TRANSIENT,
|
|
23
27
|
ApiAuthError,
|
|
24
28
|
ApiCommandError,
|
|
29
|
+
ApiConnectError,
|
|
30
|
+
ApiLimitExceeded,
|
|
25
31
|
ApiNetworkError,
|
|
26
32
|
ApiRateLimited,
|
|
27
33
|
ApiTransientError,
|
|
28
34
|
)
|
|
29
35
|
|
|
30
|
-
_TRANSIENT = (ApiRateLimited, ApiTransientError, ApiNetworkError)
|
|
31
36
|
_ATTEMPTS = 3
|
|
32
37
|
_BASE_DELAY = 0.5
|
|
33
38
|
_MAX_DELAY = 8.0
|
|
34
39
|
|
|
35
40
|
|
|
41
|
+
def _network_error(exc: httpx.TransportError) -> ApiNetworkError:
|
|
42
|
+
"""The transport failure as an `ApiNetworkError`, or an `ApiConnectError` when nothing was sent."""
|
|
43
|
+
error_type = ApiConnectError if isinstance(exc, (httpx.ConnectError, httpx.ConnectTimeout)) else ApiNetworkError
|
|
44
|
+
# str() of an httpx transport error is often empty (ReadError("")); the class name is then the only cause.
|
|
45
|
+
return error_type(str(exc) or type(exc).__name__)
|
|
46
|
+
|
|
47
|
+
|
|
36
48
|
class Client:
|
|
37
49
|
def __init__(
|
|
38
50
|
self,
|
|
@@ -62,7 +74,7 @@ class Client:
|
|
|
62
74
|
# parameters instead of ignoring them, so `?limit=` or `?cursor=None` is a 400.
|
|
63
75
|
if params is not None:
|
|
64
76
|
params = {key: value for key, value in params.items() if value is not None}
|
|
65
|
-
return self._send("GET", path, gate_status=True, params=params or None)
|
|
77
|
+
return self._send("GET", path, gate_status=True, idempotent=True, params=params or None)
|
|
66
78
|
|
|
67
79
|
def probe(self, path: str = "/users/me") -> tuple[int, float]:
|
|
68
80
|
"""One unretried request, returning (status_code, elapsed_ms) without gating the body.
|
|
@@ -75,7 +87,7 @@ class Client:
|
|
|
75
87
|
try:
|
|
76
88
|
resp = self._http.request("GET", path)
|
|
77
89
|
except httpx.TransportError as exc:
|
|
78
|
-
raise
|
|
90
|
+
raise _network_error(exc) from exc
|
|
79
91
|
return resp.status_code, (time.monotonic() - start) * 1000
|
|
80
92
|
|
|
81
93
|
def query(self, revision_id: str, command: str, **args: object) -> dict:
|
|
@@ -92,20 +104,26 @@ class Client:
|
|
|
92
104
|
) -> dict:
|
|
93
105
|
path = f"/revisions/{revision_id}/data/{channel}"
|
|
94
106
|
body = {"command": command, **args}
|
|
95
|
-
return self._send("POST", path, gate_status=gate_status, json=body)
|
|
107
|
+
return self._send("POST", path, gate_status=gate_status, idempotent=(channel == "query"), json=body)
|
|
96
108
|
|
|
97
|
-
def _send(
|
|
109
|
+
def _send(
|
|
110
|
+
self, method: str, path: str, *, gate_status: bool, idempotent: bool, **kwargs: object
|
|
111
|
+
) -> dict:
|
|
112
|
+
# A write retries only what proves it never ran (docs/specs/2026-10-06-safe-retries.md).
|
|
113
|
+
retryable = TRANSIENT if idempotent else NEVER_RAN
|
|
98
114
|
last_error: Exception
|
|
99
115
|
for attempt in range(_ATTEMPTS):
|
|
100
116
|
try:
|
|
101
117
|
resp = self._http.request(method, path, **kwargs)
|
|
102
118
|
except httpx.TransportError as exc:
|
|
103
|
-
last_error =
|
|
119
|
+
last_error = _network_error(exc)
|
|
104
120
|
else:
|
|
105
121
|
try:
|
|
106
122
|
return self._handle(resp, gate_status=gate_status)
|
|
107
|
-
except
|
|
123
|
+
except TRANSIENT as exc:
|
|
108
124
|
last_error = exc
|
|
125
|
+
if not isinstance(last_error, retryable):
|
|
126
|
+
break
|
|
109
127
|
if attempt < _ATTEMPTS - 1:
|
|
110
128
|
self._sleep(random.random() * min(_MAX_DELAY, _BASE_DELAY * 2**attempt))
|
|
111
129
|
raise last_error
|
|
@@ -122,5 +140,7 @@ class Client:
|
|
|
122
140
|
except ValueError as exc:
|
|
123
141
|
raise ApiTransientError(f"non-JSON response: HTTP {resp.status_code}") from exc
|
|
124
142
|
if gate_status and data.get("status") != "success":
|
|
125
|
-
|
|
143
|
+
if data.get("error") == "limit_exceeded":
|
|
144
|
+
raise ApiLimitExceeded(data.get("message", ""), data.get("limit"))
|
|
145
|
+
raise ApiCommandError(data.get("error", "unknown"), data.get("message", ""), data.get("limit"))
|
|
126
146
|
return data
|
|
@@ -41,8 +41,8 @@ class Api:
|
|
|
41
41
|
f"/blobs/{org}/{blob}/revisions", {"limit": limit, "cursor": cursor})
|
|
42
42
|
return body["revisions"], body["cursor"]
|
|
43
43
|
|
|
44
|
-
def get_blob_limits(self, org: str, blob: str) -> dict:
|
|
45
|
-
return self._client.get(f"/blobs/{org}/{blob}/limits")["
|
|
44
|
+
def get_blob_limits(self, org: str, blob: str) -> list[dict]:
|
|
45
|
+
return self._client.get(f"/blobs/{org}/{blob}/limits")["groups"]
|
|
46
46
|
|
|
47
47
|
def list_definitions(self, revision_id: str, category: str) -> list[dict]:
|
|
48
48
|
return self._client.query(revision_id, "list_definitions", category=category)["definitions"]
|
|
@@ -85,32 +85,64 @@ class Api:
|
|
|
85
85
|
def get_execution(self, revision_id: str, execution_id: str) -> dict:
|
|
86
86
|
return self._client.query(revision_id, "get_execution", execution_id=execution_id)["execution"]
|
|
87
87
|
|
|
88
|
+
def list_session_events(
|
|
89
|
+
self,
|
|
90
|
+
revision_id: str,
|
|
91
|
+
session: str,
|
|
92
|
+
*,
|
|
93
|
+
filter: dict | None = None,
|
|
94
|
+
after: str | None = None,
|
|
95
|
+
ascending: bool | None = None,
|
|
96
|
+
limit: int | None = None,
|
|
97
|
+
cursor: str | None = None,
|
|
98
|
+
) -> tuple[list[dict], str | None]:
|
|
99
|
+
args = self._page_args(filter, after, ascending, limit, cursor)
|
|
100
|
+
body = self._client.query(revision_id, "list_session_events", session=session, **args)
|
|
101
|
+
return body["events"], body["cursor"]
|
|
102
|
+
|
|
88
103
|
def list_execution_events(
|
|
89
104
|
self,
|
|
90
105
|
revision_id: str,
|
|
91
106
|
execution_id: str,
|
|
92
107
|
*,
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
filter: dict | None = None,
|
|
109
|
+
after: str | None = None,
|
|
110
|
+
ascending: bool | None = None,
|
|
95
111
|
limit: int | None = None,
|
|
96
|
-
|
|
112
|
+
cursor: str | None = None,
|
|
113
|
+
) -> tuple[list[dict], str | None]:
|
|
114
|
+
args = self._page_args(filter, after, ascending, limit, cursor)
|
|
115
|
+
body = self._client.query(revision_id, "list_execution_events", execution_id=execution_id, **args)
|
|
116
|
+
return body["events"], body["cursor"]
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _page_args(
|
|
120
|
+
filter: dict | None, after: str | None, ascending: bool | None, limit: int | None, cursor: str | None
|
|
121
|
+
) -> dict[str, object]:
|
|
97
122
|
# Keys are omitted rather than sent as null: every command schema is
|
|
98
123
|
# additionalProperties: false, so one stray key is a 400, not an ignored field.
|
|
99
|
-
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
given = {"filter": filter or None, "after": after, "ascending": ascending, "limit": limit, "cursor": cursor}
|
|
125
|
+
return {key: value for key, value in given.items() if value is not None}
|
|
126
|
+
|
|
127
|
+
def list_session_listeners(self, revision_id: str, session: str) -> list[dict]:
|
|
128
|
+
return self._client.query(revision_id, "list_session_listeners", session=session)["listeners"]
|
|
129
|
+
|
|
130
|
+
def set_session_listener(self, revision_id: str, session: str, listener: str, enabled: bool) -> dict:
|
|
131
|
+
return self._client.command(
|
|
132
|
+
revision_id, "set_session_listener", session=session, listener=listener, enabled=enabled
|
|
133
|
+
)["listener"]
|
|
105
134
|
|
|
106
135
|
def list_executions(
|
|
107
|
-
self,
|
|
136
|
+
self,
|
|
137
|
+
revision_id: str,
|
|
138
|
+
session_id: str,
|
|
139
|
+
*,
|
|
140
|
+
listener: str | None = None,
|
|
141
|
+
start_execution_id: str | None = None,
|
|
108
142
|
) -> tuple[list[dict], str | None]:
|
|
109
|
-
# start_execution_id is omitted rather than sent as null, same reason list_schedules omits
|
|
110
|
-
# start_schedule_id: LIST_EXECUTIONS_SCHEMA is additionalProperties: false. One page and the
|
|
111
|
-
# next cursor -- this method deliberately does not loop; the loop belongs to the caller that
|
|
112
|
-
# knows why it's paging (deploy's saturation count stops early on purpose).
|
|
113
143
|
args: dict[str, object] = {"session_id": session_id}
|
|
144
|
+
if listener is not None:
|
|
145
|
+
args["listener"] = listener
|
|
114
146
|
if start_execution_id is not None:
|
|
115
147
|
args["start_execution_id"] = start_execution_id
|
|
116
148
|
body = self._client.query(revision_id, "list_executions", **args)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Error hierarchy raised by `client.Client`.
|
|
2
|
+
|
|
3
|
+
Contract: every failure raised is one of these classes, each carrying a `code` class attribute
|
|
4
|
+
from `blobhub_cli.codes`; `ApiCommandError` additionally carries `.error`, the server's
|
|
5
|
+
machine-readable failure string, kept separate from the human-readable `.message`, and `.limit`,
|
|
6
|
+
the served limit object any refusal may carry -- `ApiLimitExceeded` is the `limit_exceeded` one.
|
|
7
|
+
`outcome_unknown` says which failures leave a write's outcome unknown (the SDK-modernization
|
|
8
|
+
epic's D5, `docs/specs/2026-10-06-safe-retries.md`).
|
|
9
|
+
Invariant: no instance of any of these classes ever carries the API key — the client that
|
|
10
|
+
raises them holds the only copy, and it is never included in a message here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
from blobhub_cli import codes
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApiError(Exception):
|
|
19
|
+
"""Base class for every error `Client` raises."""
|
|
20
|
+
|
|
21
|
+
code = "API_ERROR"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApiAuthError(ApiError):
|
|
25
|
+
"""The API key was rejected or the resource is forbidden (HTTP 401/403)."""
|
|
26
|
+
|
|
27
|
+
code = codes.AUTH_INVALID
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ApiRateLimited(ApiError):
|
|
31
|
+
"""API Gateway throttled the request (HTTP 429) before anything ran. Retried for every request."""
|
|
32
|
+
|
|
33
|
+
code = codes.API_RATE_LIMITED
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ApiTransientError(ApiError):
|
|
37
|
+
"""A server-side or malformed-response failure (HTTP 5xx, non-JSON body). Retried for reads only."""
|
|
38
|
+
|
|
39
|
+
code = codes.API_TRANSIENT_ERROR
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ApiNetworkError(ApiError):
|
|
43
|
+
"""A transport failure: no usable response arrived. Retried for reads only."""
|
|
44
|
+
|
|
45
|
+
code = codes.API_NETWORK_ERROR
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ApiConnectError(ApiNetworkError):
|
|
49
|
+
"""The connection was never established, so the request was never sent. Retried for every request."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ApiCommandError(ApiError):
|
|
53
|
+
"""A successful HTTP response whose body reports `status` other than `success`."""
|
|
54
|
+
|
|
55
|
+
code = codes.API_COMMAND_FAILED
|
|
56
|
+
|
|
57
|
+
def __init__(self, error: str, message: str = "", limit: dict | None = None) -> None:
|
|
58
|
+
message = with_limit(message, limit)
|
|
59
|
+
super().__init__(message or error)
|
|
60
|
+
self.error = error
|
|
61
|
+
self.message = message or error
|
|
62
|
+
self.limit = limit
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ApiLimitExceeded(ApiCommandError):
|
|
66
|
+
"""A `limit_exceeded` refusal: a quota or a configurable ceiling has been reached."""
|
|
67
|
+
|
|
68
|
+
code = codes.LIMIT_EXCEEDED
|
|
69
|
+
|
|
70
|
+
def __init__(self, message: str = "", limit: dict | None = None) -> None:
|
|
71
|
+
super().__init__("limit_exceeded", message, limit)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def with_limit(message: str, limit: dict | None) -> str:
|
|
75
|
+
"""`message` with the served limit appended, as `(limit <id>: value <v>[, held <h>])`."""
|
|
76
|
+
if not limit:
|
|
77
|
+
return message
|
|
78
|
+
held = f", held {json.dumps(limit['held'])}" if "held" in limit else ""
|
|
79
|
+
named = f"limit {limit.get('id')}: value {json.dumps(limit.get('value'))}{held}"
|
|
80
|
+
return f"{message} ({named})" if message else named
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
TRANSIENT = (ApiRateLimited, ApiTransientError, ApiNetworkError)
|
|
84
|
+
# A write may retry only these: a 429 is refused before any handler runs, and a failed connect sent nothing.
|
|
85
|
+
NEVER_RAN = (ApiRateLimited, ApiConnectError)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def outcome_unknown(error: BaseException) -> bool:
|
|
89
|
+
"""Whether a failed request may still have been carried out by the platform."""
|
|
90
|
+
return isinstance(error, TRANSIENT) and not isinstance(error, NEVER_RAN)
|
|
@@ -40,12 +40,21 @@ def not_accessible(ref: BlobRef) -> CliExit:
|
|
|
40
40
|
can add: an org-scoped key must address its org by UUID and gets exactly this 403 against an
|
|
41
41
|
alias -- which is why it belongs to the message rather than to one of the two raise sites.
|
|
42
42
|
"""
|
|
43
|
-
message =
|
|
43
|
+
message = _refused(ref.label())
|
|
44
44
|
if not ids.looks_like_uuid(ref.org):
|
|
45
45
|
message += f"; if this API key is org-scoped, it must address the org by UUID, not the alias {ref.org!r}"
|
|
46
46
|
return CliExit(codes.BLOB_NOT_ACCESSIBLE, message, "blobhub whoami")
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def id_not_accessible(blob_id: str) -> CliExit:
|
|
50
|
+
"""BLOB_NOT_ACCESSIBLE for a blob named by id alone, so no org clause applies."""
|
|
51
|
+
return CliExit(codes.BLOB_NOT_ACCESSIBLE, _refused(blob_id), "blobhub whoami")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _refused(label: str) -> str:
|
|
55
|
+
return f"blob {label} is absent or not accessible with this key"
|
|
56
|
+
|
|
57
|
+
|
|
49
58
|
def parse(text: str, default_org: str | None, *, command: str = DEFAULT_COMMAND) -> BlobRef:
|
|
50
59
|
# `command` is the sub-command the caller is running ("blob revisions", "blob limits", ...),
|
|
51
60
|
# threaded in only so that every hint below is a command the user can actually paste. It is no
|
|
@@ -12,7 +12,7 @@ import typer
|
|
|
12
12
|
from typer.completion import completion_init
|
|
13
13
|
|
|
14
14
|
from blobhub_cli import __version__, codes, console
|
|
15
|
-
from blobhub_cli.api.errors import ApiError
|
|
15
|
+
from blobhub_cli.api.errors import ApiCommandError, ApiError
|
|
16
16
|
from blobhub_cli.commands.completion import Shell as CompletionShell
|
|
17
17
|
from blobhub_cli.config import settings
|
|
18
18
|
from blobhub_cli.exit import CliExit
|
|
@@ -42,9 +42,10 @@ def build_app() -> typer.Typer:
|
|
|
42
42
|
verbose: bool = typer.Option(False, "-v", "--verbose", help="Print extra diagnostic detail."),
|
|
43
43
|
quiet: bool = typer.Option(False, "-q", "--quiet", help="Suppress advisory warnings."),
|
|
44
44
|
org: str = typer.Option(None, "--org", help="Org a manifest's bare 'blob:' name belongs to."),
|
|
45
|
+
profile: str = typer.Option(None, "--profile", help="Profile to use for this command."),
|
|
45
46
|
) -> None:
|
|
46
47
|
console.configure(json_output=json_output, verbose=verbose, quiet=quiet)
|
|
47
|
-
settings.configure(org=org)
|
|
48
|
+
settings.configure(org=org, profile=profile)
|
|
48
49
|
if version:
|
|
49
50
|
typer.echo(__version__)
|
|
50
51
|
raise typer.Exit(0)
|
|
@@ -134,7 +135,10 @@ def build_app() -> typer.Typer:
|
|
|
134
135
|
|
|
135
136
|
completion_cmd.emit(shell)
|
|
136
137
|
|
|
137
|
-
profile_app = typer.Typer(
|
|
138
|
+
profile_app = typer.Typer(
|
|
139
|
+
add_completion=False,
|
|
140
|
+
help="Manage stored profiles. Takes names as arguments; ignores the root --profile.",
|
|
141
|
+
)
|
|
138
142
|
|
|
139
143
|
@profile_app.command("ls")
|
|
140
144
|
def profile_ls() -> None:
|
|
@@ -175,7 +179,7 @@ def build_app() -> typer.Typer:
|
|
|
175
179
|
blob: str = typer.Argument(..., help="Blob as '<org>/<name>', or a bare name with --org."),
|
|
176
180
|
refresh: bool = typer.Option(False, "--refresh", help="Also refresh the compiler's local allowlist cache."),
|
|
177
181
|
) -> None:
|
|
178
|
-
"""Show a blob's limits
|
|
182
|
+
"""Show a blob's limits by group: id, holding, source and value."""
|
|
179
183
|
from blobhub_cli.commands import blob as blob_cmd
|
|
180
184
|
|
|
181
185
|
blob_cmd.limits(blob, refresh)
|
|
@@ -234,13 +238,12 @@ def build_app() -> typer.Typer:
|
|
|
234
238
|
manifest: Path = typer.Option(..., "-f", "--file", help="Path to the workflow manifest."), # noqa: B008
|
|
235
239
|
definition: list[str] = typer.Option([], "--definition", help="Limit to this alias, repeatable."), # noqa: B008
|
|
236
240
|
force: bool = typer.Option(False, "--force", help="Overwrite code edited outside the CLI."),
|
|
237
|
-
strict_imports: bool = typer.Option(False, "--strict-imports", help="Treat an unverifiable import as fatal."),
|
|
238
241
|
dry_run: bool = typer.Option(False, "--dry-run", help="Report the intended calls and perform none."),
|
|
239
242
|
) -> None:
|
|
240
243
|
"""Build, upload only what changed, and check the workflow definitions that were uploaded."""
|
|
241
244
|
from blobhub_cli.commands import workflow
|
|
242
245
|
|
|
243
|
-
workflow.deploy(manifest, tuple(definition), force,
|
|
246
|
+
workflow.deploy(manifest, tuple(definition), force, dry_run)
|
|
244
247
|
|
|
245
248
|
@workflow_app.command("eject")
|
|
246
249
|
def workflow_eject(
|
|
@@ -271,6 +274,73 @@ def build_app() -> typer.Typer:
|
|
|
271
274
|
|
|
272
275
|
execute_cmd.execute(manifest, definition, session, watch, open_browser)
|
|
273
276
|
|
|
277
|
+
@workflow_app.command("events")
|
|
278
|
+
def workflow_events(
|
|
279
|
+
session: str = typer.Argument(
|
|
280
|
+
..., help="A session, or <blob-id>/<revision>/<session> or <org>/<blob>/<revision>/<session>."
|
|
281
|
+
),
|
|
282
|
+
manifest: Path = typer.Option( # noqa: B008
|
|
283
|
+
None, "-f", "--file", help="Manifest naming the blob and revision of a bare session."
|
|
284
|
+
),
|
|
285
|
+
types: list[str] = typer.Option([], "--types", help="Event types, globs allowed; repeatable."), # noqa: B008
|
|
286
|
+
aliases: list[str] = typer.Option([], "--aliases", help="Object aliases; repeatable."), # noqa: B008
|
|
287
|
+
executions: list[str] = typer.Option([], "--executions", help="Execution ids; repeatable."), # noqa: B008
|
|
288
|
+
listener: str = typer.Option(None, "--listener", help="Only events a listener caused: its alias or id."),
|
|
289
|
+
after: str = typer.Option(None, "--after", help="List events after this id, boundary or ISO 8601 time."),
|
|
290
|
+
cursor: str = typer.Option(None, "--cursor", help="Continue a listing from the token it printed."),
|
|
291
|
+
limit: int = typer.Option(20, "--limit", min=1, max=1000, help="Events per page."),
|
|
292
|
+
follow: bool = typer.Option(False, "--follow", help="Print new events as they arrive until interrupted."),
|
|
293
|
+
) -> None:
|
|
294
|
+
"""List a session's events, newest first unless --after is given, or follow them."""
|
|
295
|
+
from blobhub_cli.commands import events as events_cmd
|
|
296
|
+
|
|
297
|
+
events_cmd.events(
|
|
298
|
+
session, manifest, types=types, aliases=aliases, executions=executions, listener=listener,
|
|
299
|
+
after=after, cursor=cursor, limit=limit, follow=follow,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@workflow_app.command("listeners")
|
|
303
|
+
def workflow_listeners(
|
|
304
|
+
session: str = typer.Argument(
|
|
305
|
+
..., help="A session, or <blob-id>/<revision>/<session> or <org>/<blob>/<revision>/<session>."
|
|
306
|
+
),
|
|
307
|
+
manifest: Path = typer.Option( # noqa: B008
|
|
308
|
+
None, "-f", "--file", help="Manifest naming the blob and revision of a bare session."
|
|
309
|
+
),
|
|
310
|
+
listener: str = typer.Option(None, "--listener", help="Show one declared listener: its alias or id."),
|
|
311
|
+
) -> None:
|
|
312
|
+
"""List a session's listeners with their switch, slots and outcomes, or show one in detail."""
|
|
313
|
+
from blobhub_cli.commands import events as events_cmd
|
|
314
|
+
|
|
315
|
+
events_cmd.listeners(session, manifest, listener=listener)
|
|
316
|
+
|
|
317
|
+
listener_app = typer.Typer(add_completion=False, help="Switch a declared listener for one session.")
|
|
318
|
+
|
|
319
|
+
def _listener_switch(session: str, listener: str, manifest: Path | None, enabled: bool) -> None:
|
|
320
|
+
from blobhub_cli.commands import events as events_cmd
|
|
321
|
+
|
|
322
|
+
events_cmd.switch(session, manifest, listener, enabled)
|
|
323
|
+
|
|
324
|
+
@listener_app.command("enable")
|
|
325
|
+
def listener_enable(
|
|
326
|
+
session: str = typer.Argument(..., help="A session reference, as for `workflow events`."),
|
|
327
|
+
listener: str = typer.Argument(..., help="A declared listener's alias or id."),
|
|
328
|
+
manifest: Path = typer.Option(None, "-f", "--file", help="Manifest naming a bare session's blob."), # noqa: B008
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Enable a listener for this session, overriding its definition when it is off there."""
|
|
331
|
+
_listener_switch(session, listener, manifest, True)
|
|
332
|
+
|
|
333
|
+
@listener_app.command("disable")
|
|
334
|
+
def listener_disable(
|
|
335
|
+
session: str = typer.Argument(..., help="A session reference, as for `workflow events`."),
|
|
336
|
+
listener: str = typer.Argument(..., help="A declared listener's alias or id."),
|
|
337
|
+
manifest: Path = typer.Option(None, "-f", "--file", help="Manifest naming a bare session's blob."), # noqa: B008
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Disable a listener for this session, overriding its definition when it is on there."""
|
|
340
|
+
_listener_switch(session, listener, manifest, False)
|
|
341
|
+
|
|
342
|
+
workflow_app.add_typer(listener_app, name="listener")
|
|
343
|
+
|
|
274
344
|
@workflow_app.command("check")
|
|
275
345
|
def workflow_check(
|
|
276
346
|
manifest: Path = typer.Option(..., "-f", "--file", help="Path to the workflow manifest."), # noqa: B008
|
|
@@ -381,7 +451,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
381
451
|
# so wrap it in a CliExit rather than let a raw traceback reach the user. Only AUTH_INVALID
|
|
382
452
|
# has a hint that's genuinely a runnable command; inventing one for the rest would be prose.
|
|
383
453
|
hint = "blobhub login" if exc.code == codes.AUTH_INVALID else ""
|
|
384
|
-
|
|
454
|
+
limit = exc.limit if isinstance(exc, ApiCommandError) else None
|
|
455
|
+
console.error(CliExit(exc.code, str(exc), hint, limit=limit))
|
|
385
456
|
return 1
|
|
386
457
|
except Exception as exc: # usage errors carry exit_code; anything else is a real bug
|
|
387
458
|
# Typer >=0.24 vendors its own click fork under `typer._click` rather than
|
|
@@ -31,7 +31,6 @@ MANIFEST_VERSION_UNSUPPORTED = "MANIFEST_VERSION_UNSUPPORTED"
|
|
|
31
31
|
BLOB_REFERENCE_INVALID = "BLOB_REFERENCE_INVALID"
|
|
32
32
|
BLOB_NOT_ACCESSIBLE = "BLOB_NOT_ACCESSIBLE"
|
|
33
33
|
BLOB_NOT_WORKFLOW = "BLOB_NOT_WORKFLOW"
|
|
34
|
-
LIMITS_NOT_ACCESSIBLE = "LIMITS_NOT_ACCESSIBLE"
|
|
35
34
|
REVISION_NOT_ACCESSIBLE = "REVISION_NOT_ACCESSIBLE"
|
|
36
35
|
REVISION_NOT_WRITABLE = "REVISION_NOT_WRITABLE"
|
|
37
36
|
|
|
@@ -40,11 +39,13 @@ DEFINITION_NOT_ACCESSIBLE = "DEFINITION_NOT_ACCESSIBLE"
|
|
|
40
39
|
DEFINITION_ALIAS_INVALID = "DEFINITION_ALIAS_INVALID"
|
|
41
40
|
DEFINITION_CATEGORY_UNKNOWN = "DEFINITION_CATEGORY_UNKNOWN"
|
|
42
41
|
ALIAS_IN_USE = "ALIAS_IN_USE"
|
|
42
|
+
LISTENER_INVALID = "LISTENER_INVALID"
|
|
43
43
|
LIMIT_EXCEEDED = "LIMIT_EXCEEDED"
|
|
44
44
|
PRUNE_NOT_CONFIRMED = "PRUNE_NOT_CONFIRMED"
|
|
45
45
|
|
|
46
46
|
# Workflow references
|
|
47
47
|
WORKFLOW_REFERENCE_INVALID = "WORKFLOW_REFERENCE_INVALID"
|
|
48
|
+
SESSION_REFERENCE_INVALID = "SESSION_REFERENCE_INVALID"
|
|
48
49
|
|
|
49
50
|
# Format
|
|
50
51
|
FORMAT_UNSUPPORTED = "FORMAT_UNSUPPORTED"
|
|
@@ -82,6 +83,10 @@ SESSION_NOT_ACCESSIBLE = "SESSION_NOT_ACCESSIBLE"
|
|
|
82
83
|
SESSION_NOT_OPEN = "SESSION_NOT_OPEN"
|
|
83
84
|
DEFINITION_NOT_EXECUTABLE = "DEFINITION_NOT_EXECUTABLE"
|
|
84
85
|
EXECUTION_FAILED = "EXECUTION_FAILED"
|
|
86
|
+
EXECUTION_OUTCOME_UNKNOWN = "EXECUTION_OUTCOME_UNKNOWN"
|
|
87
|
+
EVENT_FILTER_INVALID = "EVENT_FILTER_INVALID"
|
|
88
|
+
EVENT_CURSOR_INVALID = "EVENT_CURSOR_INVALID"
|
|
89
|
+
LISTENER_NOT_FOUND = "LISTENER_NOT_FOUND"
|
|
85
90
|
|
|
86
91
|
# Schedules
|
|
87
92
|
SCHEDULE_ALIAS_REQUIRED = "SCHEDULE_ALIAS_REQUIRED"
|
|
@@ -7,7 +7,6 @@ hint, or the returned payload — only the profile name, URL, and realtime URL a
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import getpass
|
|
10
|
-
import os
|
|
11
10
|
import sys
|
|
12
11
|
from collections.abc import Callable
|
|
13
12
|
from typing import NamedTuple
|
|
@@ -86,7 +85,7 @@ def login(
|
|
|
86
85
|
stdin_read: Callable[[], str] | None = None,
|
|
87
86
|
api_factory: Callable[[Settings], Api] = _default_api_factory,
|
|
88
87
|
) -> dict:
|
|
89
|
-
name = profile
|
|
88
|
+
name = settings.profile_name(profile, None) or "default"
|
|
90
89
|
|
|
91
90
|
requests = [
|
|
92
91
|
sources.Request("api-key", supplied.api_key, supplied.api_key_stdin, supplied.api_key_env),
|
|
@@ -179,7 +178,7 @@ def logout(profile: str | None, all_profiles: bool) -> dict:
|
|
|
179
178
|
removed = sorted(profiles)
|
|
180
179
|
credentials.save({}, None)
|
|
181
180
|
else:
|
|
182
|
-
name = profile
|
|
181
|
+
name = settings.profile_name(profile, default)
|
|
183
182
|
if name is None or name not in profiles:
|
|
184
183
|
raise CliExit(codes.PROFILE_NOT_FOUND, f"no profile named {name!r}", "blobhub profile ls")
|
|
185
184
|
del profiles[name]
|