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.
Files changed (64) hide show
  1. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/PKG-INFO +45 -4
  2. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/README.md +44 -3
  3. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/__init__.py +1 -1
  4. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/commands.py +54 -9
  5. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/blob/reference.py +10 -1
  6. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/cli/app.py +73 -2
  7. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/codes.py +5 -0
  8. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/auth.py +2 -3
  9. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/doctor.py +3 -3
  10. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/eject.py +5 -1
  11. blobhub_cli-0.7.0/src/blobhub_cli/commands/events.py +326 -0
  12. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/execute.py +6 -8
  13. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/scheduler.py +28 -13
  14. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/workflow.py +113 -17
  15. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/settings.py +16 -7
  16. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/console.py +11 -3
  17. blobhub_cli-0.7.0/src/blobhub_cli/definition/listener.py +325 -0
  18. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/model.py +2 -1
  19. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/limits.py +4 -0
  20. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/scheduler.py +55 -11
  21. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/references.py +20 -8
  22. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/definitions.py +19 -3
  23. blobhub_cli-0.7.0/src/blobhub_cli/workflow/event_filter.py +140 -0
  24. blobhub_cli-0.7.0/src/blobhub_cli/workflow/events.py +97 -0
  25. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/executions.py +18 -22
  26. blobhub_cli-0.7.0/src/blobhub_cli/workflow/follow.py +100 -0
  27. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/workflow/reference.py +21 -10
  28. blobhub_cli-0.7.0/src/blobhub_cli/workflow/session_reference.py +89 -0
  29. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/.gitignore +0 -0
  30. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/LICENSE +0 -0
  31. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/pyproject.toml +0 -0
  32. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/__main__.py +0 -0
  33. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/__init__.py +0 -0
  34. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/client.py +0 -0
  35. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/api/errors.py +0 -0
  36. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/blob/__init__.py +0 -0
  37. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/cli/__init__.py +0 -0
  38. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/__init__.py +0 -0
  39. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/blob.py +0 -0
  40. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/commands/completion.py +0 -0
  41. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/__init__.py +0 -0
  42. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/allowlist.py +0 -0
  43. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/emit.py +0 -0
  44. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/graph.py +0 -0
  45. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/compiler/sandbox.py +0 -0
  46. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/__init__.py +0 -0
  47. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/credentials.py +0 -0
  48. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/config/sources.py +0 -0
  49. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/__init__.py +0 -0
  50. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/drift.py +0 -0
  51. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/eject.py +0 -0
  52. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/definition/hashing.py +0 -0
  53. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/exit.py +0 -0
  54. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/formats/__init__.py +0 -0
  55. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/formats/io.py +0 -0
  56. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/ids.py +0 -0
  57. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/__init__.py +0 -0
  58. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/base.py +0 -0
  59. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/resolver.py +0 -0
  60. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/manifest/workflow.py +0 -0
  61. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/__init__.py +0 -0
  62. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/scheduler/schedules.py +0 -0
  63. {blobhub_cli-0.6.0 → blobhub_cli-0.7.0}/src/blobhub_cli/statestore.py +0 -0
  64. {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.6.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 every mutating command
182
- prints the exact intended calls without performing them; `--json` emits a single machine-readable object instead
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 every mutating command
129
- prints the exact intended calls without performing them; `--json` emits a single machine-readable object instead
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`.
@@ -5,4 +5,4 @@ Contract: this module is the sole source of the package version — hatchling re
5
5
  Invariant: no other file in this package defines `__version__`.
6
6
  """
7
7
 
8
- __version__ = "0.6.0"
8
+ __version__ = "0.7.0"
@@ -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
- created_since: str | None = None,
94
- ascending: bool = True,
108
+ filter: dict | None = None,
109
+ after: str | None = None,
110
+ ascending: bool | None = None,
95
111
  limit: int | None = None,
96
- ) -> list[dict]:
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
- args: dict[str, object] = {"execution_id": execution_id, "ascending": ascending}
100
- if created_since is not None:
101
- args["created_since"] = created_since
102
- if limit is not None:
103
- args["limit"] = limit
104
- return self._client.query(revision_id, "list_execution_events", **args)["execution_events"]
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 = f"blob {ref.label()} is absent or not accessible with this key"
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(add_completion=False, help="Manage stored profiles.")
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 or os.environ.get("BLOBHUB_PROFILE") or "default"
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 or default
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 or os.environ.get("BLOBHUB_PROFILE") or default
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 or os.environ.get("BLOBHUB_PROFILE") or default
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