blobhub-cli 0.6.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.6.0 → blobhub_cli-0.7.0}/PKG-INFO +45 -4
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/README.md +44 -3
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/__init__.py +1 -1
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/commands.py +54 -9
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/blob/reference.py +10 -1
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/cli/app.py +73 -2
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/codes.py +5 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/auth.py +2 -3
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/doctor.py +3 -3
- {blobhub_cli-0.6.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.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/execute.py +6 -8
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/scheduler.py +28 -13
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/workflow.py +113 -17
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/settings.py +16 -7
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/console.py +11 -3
- blobhub_cli-0.7.0/src/blobhub_cli/definition/listener.py +325 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/model.py +2 -1
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/limits.py +4 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/scheduler.py +55 -11
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/references.py +20 -8
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/definitions.py +19 -3
- 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.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/executions.py +18 -22
- blobhub_cli-0.7.0/src/blobhub_cli/workflow/follow.py +100 -0
- {blobhub_cli-0.6.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.6.0 → blobhub_cli-0.7.0}/.gitignore +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/LICENSE +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/pyproject.toml +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/__main__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/client.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/errors.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/blob/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/cli/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/blob.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/completion.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/allowlist.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/emit.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/graph.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/sandbox.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/credentials.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/sources.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/drift.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/eject.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/hashing.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/exit.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/formats/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/formats/io.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/ids.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/base.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/resolver.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/workflow.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/__init__.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/schedules.py +0 -0
- {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/statestore.py +0 -0
- {blobhub_cli-0.6.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`.
|
|
@@ -85,23 +85,68 @@ 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"]
|
|
134
|
+
|
|
135
|
+
def list_executions(
|
|
136
|
+
self,
|
|
137
|
+
revision_id: str,
|
|
138
|
+
session_id: str,
|
|
139
|
+
*,
|
|
140
|
+
listener: str | None = None,
|
|
141
|
+
start_execution_id: str | None = None,
|
|
142
|
+
) -> tuple[list[dict], str | None]:
|
|
143
|
+
args: dict[str, object] = {"session_id": session_id}
|
|
144
|
+
if listener is not None:
|
|
145
|
+
args["listener"] = listener
|
|
146
|
+
if start_execution_id is not None:
|
|
147
|
+
args["start_execution_id"] = start_execution_id
|
|
148
|
+
body = self._client.query(revision_id, "list_executions", **args)
|
|
149
|
+
return body["executions"], body["last_execution_id"]
|
|
105
150
|
|
|
106
151
|
def create_schedule(self, revision_id: str, **fields: object) -> dict:
|
|
107
152
|
return self._client.command(revision_id, "create_schedule", **fields)["schedule"]
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -270,6 +274,73 @@ def build_app() -> typer.Typer:
|
|
|
270
274
|
|
|
271
275
|
execute_cmd.execute(manifest, definition, session, watch, open_browser)
|
|
272
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
|
+
|
|
273
344
|
@workflow_app.command("check")
|
|
274
345
|
def workflow_check(
|
|
275
346
|
manifest: Path = typer.Option(..., "-f", "--file", help="Path to the workflow manifest."), # noqa: B008
|
|
@@ -39,11 +39,13 @@ DEFINITION_NOT_ACCESSIBLE = "DEFINITION_NOT_ACCESSIBLE"
|
|
|
39
39
|
DEFINITION_ALIAS_INVALID = "DEFINITION_ALIAS_INVALID"
|
|
40
40
|
DEFINITION_CATEGORY_UNKNOWN = "DEFINITION_CATEGORY_UNKNOWN"
|
|
41
41
|
ALIAS_IN_USE = "ALIAS_IN_USE"
|
|
42
|
+
LISTENER_INVALID = "LISTENER_INVALID"
|
|
42
43
|
LIMIT_EXCEEDED = "LIMIT_EXCEEDED"
|
|
43
44
|
PRUNE_NOT_CONFIRMED = "PRUNE_NOT_CONFIRMED"
|
|
44
45
|
|
|
45
46
|
# Workflow references
|
|
46
47
|
WORKFLOW_REFERENCE_INVALID = "WORKFLOW_REFERENCE_INVALID"
|
|
48
|
+
SESSION_REFERENCE_INVALID = "SESSION_REFERENCE_INVALID"
|
|
47
49
|
|
|
48
50
|
# Format
|
|
49
51
|
FORMAT_UNSUPPORTED = "FORMAT_UNSUPPORTED"
|
|
@@ -82,6 +84,9 @@ SESSION_NOT_OPEN = "SESSION_NOT_OPEN"
|
|
|
82
84
|
DEFINITION_NOT_EXECUTABLE = "DEFINITION_NOT_EXECUTABLE"
|
|
83
85
|
EXECUTION_FAILED = "EXECUTION_FAILED"
|
|
84
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]
|
|
@@ -22,7 +22,7 @@ from blobhub_cli.api.commands import Api
|
|
|
22
22
|
from blobhub_cli.api.errors import ApiAuthError, ApiError
|
|
23
23
|
from blobhub_cli.compiler import allowlist
|
|
24
24
|
from blobhub_cli.config import credentials
|
|
25
|
-
from blobhub_cli.config.settings import DEFAULT_API_URL, DEFAULT_REALTIME_URL, Settings
|
|
25
|
+
from blobhub_cli.config.settings import DEFAULT_API_URL, DEFAULT_REALTIME_URL, Settings, profile_name
|
|
26
26
|
from blobhub_cli.exit import CliExit
|
|
27
27
|
|
|
28
28
|
OK, FAIL, WARN, SKIP = "ok", "fail", "warn", "skip"
|
|
@@ -81,7 +81,7 @@ def _resolve_url(flag_url: str | None, flag_profile: str | None) -> str:
|
|
|
81
81
|
profiles, default = credentials.load()
|
|
82
82
|
except CliExit:
|
|
83
83
|
return DEFAULT_API_URL
|
|
84
|
-
name = flag_profile
|
|
84
|
+
name = profile_name(flag_profile, default)
|
|
85
85
|
profile = profiles.get(name) if name else None
|
|
86
86
|
return profile.url if profile else DEFAULT_API_URL
|
|
87
87
|
|
|
@@ -97,7 +97,7 @@ def _resolve_key(flag_key: str | None, flag_profile: str | None) -> tuple[str |
|
|
|
97
97
|
catch, in its most load-bearing row.
|
|
98
98
|
"""
|
|
99
99
|
profiles, default = credentials.load() # may raise CliExit; the caller converts it to a row
|
|
100
|
-
name = flag_profile
|
|
100
|
+
name = profile_name(flag_profile, default)
|
|
101
101
|
if name and name not in profiles:
|
|
102
102
|
return None, f"{codes.PROFILE_NOT_FOUND}: no profile named {name!r}"
|
|
103
103
|
if flag_key:
|
|
@@ -76,8 +76,12 @@ def eject(
|
|
|
76
76
|
order: list[tuple[str, Path]] = []
|
|
77
77
|
seen_keys: set[tuple[str, Path]] = set()
|
|
78
78
|
suggestions: list[dict] = []
|
|
79
|
+
skipped: list[dict] = []
|
|
79
80
|
|
|
80
81
|
for entry in set_.entries:
|
|
82
|
+
if entry.category == "listener":
|
|
83
|
+
skipped.append({"category": entry.category, "alias": entry.alias})
|
|
84
|
+
continue
|
|
81
85
|
remote = set_.remote.get((entry.category, entry.alias))
|
|
82
86
|
if remote is None:
|
|
83
87
|
continue # nothing on the revision yet for this alias -- nothing to eject
|
|
@@ -148,6 +152,6 @@ def eject(
|
|
|
148
152
|
"alias": alias, "root": str(root), "files": [str(path) for path, _ in targets],
|
|
149
153
|
})
|
|
150
154
|
|
|
151
|
-
payload = {"definitions": definitions_payload, "bound": not suggestions}
|
|
155
|
+
payload = {"definitions": definitions_payload, "bound": not suggestions, "skipped": skipped}
|
|
152
156
|
console.result(payload, lambda p: _render_eject(p, suggestions))
|
|
153
157
|
return payload
|