blobhub-cli 0.7.0__tar.gz → 0.8.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.7.0 → blobhub_cli-0.8.0}/PKG-INFO +1 -1
  2. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/__init__.py +1 -1
  3. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/events.py +17 -5
  4. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/definition/listener.py +26 -3
  5. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/.gitignore +0 -0
  6. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/LICENSE +0 -0
  7. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/README.md +0 -0
  8. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/pyproject.toml +0 -0
  9. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/__main__.py +0 -0
  10. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/api/__init__.py +0 -0
  11. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/api/client.py +0 -0
  12. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/api/commands.py +0 -0
  13. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/api/errors.py +0 -0
  14. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/blob/__init__.py +0 -0
  15. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/blob/reference.py +0 -0
  16. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/cli/__init__.py +0 -0
  17. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/cli/app.py +0 -0
  18. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/codes.py +0 -0
  19. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/__init__.py +0 -0
  20. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/auth.py +0 -0
  21. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/blob.py +0 -0
  22. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/completion.py +0 -0
  23. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/doctor.py +0 -0
  24. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/eject.py +0 -0
  25. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/execute.py +0 -0
  26. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/scheduler.py +0 -0
  27. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/commands/workflow.py +0 -0
  28. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/compiler/__init__.py +0 -0
  29. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/compiler/allowlist.py +0 -0
  30. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/compiler/emit.py +0 -0
  31. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/compiler/graph.py +0 -0
  32. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/compiler/sandbox.py +0 -0
  33. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/config/__init__.py +0 -0
  34. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/config/credentials.py +0 -0
  35. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/config/settings.py +0 -0
  36. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/config/sources.py +0 -0
  37. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/console.py +0 -0
  38. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/definition/__init__.py +0 -0
  39. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/definition/drift.py +0 -0
  40. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/definition/eject.py +0 -0
  41. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/definition/hashing.py +0 -0
  42. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/definition/model.py +0 -0
  43. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/exit.py +0 -0
  44. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/formats/__init__.py +0 -0
  45. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/formats/io.py +0 -0
  46. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/ids.py +0 -0
  47. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/limits.py +0 -0
  48. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/manifest/__init__.py +0 -0
  49. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/manifest/base.py +0 -0
  50. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/manifest/resolver.py +0 -0
  51. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/manifest/scheduler.py +0 -0
  52. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/manifest/workflow.py +0 -0
  53. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/scheduler/__init__.py +0 -0
  54. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/scheduler/references.py +0 -0
  55. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/scheduler/schedules.py +0 -0
  56. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/statestore.py +0 -0
  57. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/__init__.py +0 -0
  58. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/definitions.py +0 -0
  59. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/event_filter.py +0 -0
  60. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/events.py +0 -0
  61. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/executions.py +0 -0
  62. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/follow.py +0 -0
  63. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/reference.py +0 -0
  64. {blobhub_cli-0.7.0 → blobhub_cli-0.8.0}/src/blobhub_cli/workflow/session_reference.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: blobhub-cli
3
- Version: 0.7.0
3
+ Version: 0.8.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/
@@ -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.7.0"
8
+ __version__ = "0.8.0"
@@ -18,6 +18,7 @@ from blobhub_cli.api.commands import Api
18
18
  from blobhub_cli.api.errors import ApiAuthError, ApiCommandError
19
19
  from blobhub_cli.config import settings
20
20
  from blobhub_cli.config.settings import Settings
21
+ from blobhub_cli.definition.listener import DEFAULT_RETRY_DELAY_SECONDS
21
22
  from blobhub_cli.exit import CliExit
22
23
  from blobhub_cli.workflow import event_filter, follow, session_reference
23
24
  from blobhub_cli.workflow import events as event_lines
@@ -166,7 +167,7 @@ def events(
166
167
  return payload
167
168
 
168
169
 
169
- _ROW = "{:<{width}} {:<13} {:<9} {:<6} {:<8} {:<40} {}"
170
+ _ROW = "{:<{width}} {:<13} {:<9} {:<7} {:<6} {:<8} {:<40} {}"
170
171
  _MORE_RUNS = "no runs in the newest 1,000 executions; more exist"
171
172
 
172
173
 
@@ -194,15 +195,25 @@ def _mode(entry: dict) -> str:
194
195
  return entry["definition"].get("concurrency", {}).get("mode", "serial")
195
196
 
196
197
 
198
+ def _retry(entry: dict) -> str:
199
+ retry = entry["definition"].get("retry")
200
+ if not isinstance(retry, dict) or "attempts" not in retry:
201
+ return "-"
202
+ return f"{retry['attempts']}x{retry.get('delay_seconds', DEFAULT_RETRY_DELAY_SECONDS)}s"
203
+
204
+
197
205
  def _render_entry_line(entry: dict, width: int) -> str:
198
206
  pending = entry.get("pending", {}).get("count", 0)
199
- cells = (entry["alias"], _enabled(entry), _mode(entry), _slots(entry), pending, _last(entry), _counts(entry))
207
+ cells = (
208
+ entry["alias"], _enabled(entry), _mode(entry), _retry(entry), _slots(entry), pending, _last(entry),
209
+ _counts(entry),
210
+ )
200
211
  return _ROW.format(*cells, width=width)
201
212
 
202
213
 
203
214
  def _render_listeners(payload: dict) -> None:
204
215
  width = max([len("ALIAS"), *(len(entry["alias"]) for entry in payload["listeners"])])
205
- print(_ROW.format("ALIAS", "ENABLED", "MODE", "SLOTS", "PENDING", "LAST", "COUNTS", width=width))
216
+ print(_ROW.format("ALIAS", "ENABLED", "MODE", "RETRY", "SLOTS", "PENDING", "LAST", "COUNTS", width=width))
206
217
  for entry in payload["listeners"]:
207
218
  print(_render_entry_line(entry, width))
208
219
  if payload["connections"]:
@@ -220,12 +231,13 @@ def _render_listener_detail(payload: dict) -> None:
220
231
  print(f"{entry['alias']} {_enabled(entry)} slots {_slots(entry)} {_counts(entry)}")
221
232
  if entry.get("description"):
222
233
  print(f"description: {entry['description']}")
223
- for field in ("filter", "batch", "concurrency", "invocation_target_type", "invocation_target"):
234
+ for field in ("filter", "batch", "concurrency", "retry", "invocation_target_type", "invocation_target"):
224
235
  if field in body:
225
236
  print(f"{field}: {json.dumps(body[field])}")
226
237
  print("\nOutcomes")
227
238
  for outcome in entry["outcomes"] or []:
228
- print(f" {outcome['at']} {outcome['status']} count={outcome['count']} batch={outcome['batch_id']}")
239
+ retry = f" retry={outcome['retry']}" if "retry" in outcome else ""
240
+ print(f" {outcome['at']} {outcome['status']} count={outcome['count']} batch={outcome['batch_id']}{retry}")
229
241
  if not entry["outcomes"]:
230
242
  print(" none")
231
243
  print("\nRuns")
@@ -1,7 +1,8 @@
1
1
  """Offline validation, write-target shapes and comparison of a listener definition body.
2
2
 
3
- Contract: `error` returns the first violation of docs/specs/2026-11-10-workflow_listeners.md section 4.4, in the
4
- platform's wording (blobhub-infra `listeners/definition.py`), or None; `comparable` fills the documented defaults.
3
+ Contract: `error` returns the first violation of docs/specs/2026-11-10-workflow_listeners.md section 4.4 and of
4
+ docs/specs/2026-11-14-listener-retry.md, in the platform's wording (blobhub-infra `listeners/definition.py`), or
5
+ None; `comparable` fills the documented defaults.
5
6
  Invariant: templates are only checked for being strings -- their syntax and names are the platform's -- and nothing
6
7
  here reads the catalog except through the `groups` a caller hands in.
7
8
  """
@@ -15,7 +16,7 @@ TYPE = "listener_definition"
15
16
  VERSION = "1.0.0"
16
17
  BODY_KEYS = (
17
18
  "type", "version", "description", "enabled", "filter", "batch", "concurrency", "invocation_target_type",
18
- "invocation_target", "receipt",
19
+ "invocation_target", "receipt", "retry",
19
20
  )
20
21
  CONTEXTS = ("batch", "event", "listener", "item", "object")
21
22
  MARKERS = ("{{", "{%", "{#")
@@ -25,6 +26,7 @@ BATCH_KEYS = ("window_seconds", "max_events", "distinct_by")
25
26
  CONCURRENCY_KEYS = ("mode", "key", "key_segments", "max_parallel")
26
27
  CONCURRENCY_MODES = ("serial", "keyed", "parallel")
27
28
  RECEIPT_KEYS = ("alias", "type", "parent")
29
+ RETRY_KEYS = ("attempts", "delay_seconds")
28
30
  WORKFLOW_TARGET_KEYS = ("workflow", "input_data")
29
31
  THREAD_ITEM_KEYS = ("alias", "content", "metadata", "parent_id")
30
32
  OBJECT_KEYS = ("alias", "value")
@@ -33,6 +35,9 @@ REFERENCE_KEYS = ("org", "blob", "revision", "definition")
33
35
  REFERENCE_DEPENDENCIES = {"org": "blob", "revision": "blob", "blob": "revision"}
34
36
 
35
37
  DEFAULT_MAX_EVENTS = 100
38
+ MAX_RETRY_ATTEMPTS = 3
39
+ MAX_RETRY_DELAY_SECONDS = 900
40
+ DEFAULT_RETRY_DELAY_SECONDS = 30
36
41
  MAX_DISTINCT_BY = 8
37
42
  MAX_PATH_SEGMENTS = 8
38
43
  MAX_KEY_SEGMENTS = 16
@@ -60,6 +65,7 @@ def error(body: object, *, groups: list[dict] | None = None) -> str | None:
60
65
  or _concurrency(body.get("concurrency", {}), groups)
61
66
  or _target(body)
62
67
  or (_receipt(body["receipt"]) if "receipt" in body else None)
68
+ or (_retry(body["retry"], body["invocation_target_type"]) if "retry" in body else None)
63
69
  )
64
70
 
65
71
 
@@ -111,6 +117,9 @@ def comparable(local: dict, remote: dict) -> tuple[dict, dict]:
111
117
  if isinstance(batch, dict):
112
118
  batch.setdefault("window_seconds", 0)
113
119
  batch.setdefault("max_events", DEFAULT_MAX_EVENTS)
120
+ retry = body.get("retry")
121
+ if isinstance(retry, dict):
122
+ retry.setdefault("delay_seconds", DEFAULT_RETRY_DELAY_SECONDS)
114
123
  omitted = isinstance(local["concurrency"], dict) and "max_parallel" not in local["concurrency"]
115
124
  if omitted and isinstance(remote["concurrency"], dict):
116
125
  remote["concurrency"].pop("max_parallel", None)
@@ -323,3 +332,17 @@ def _receipt(receipt: object) -> str | None:
323
332
  if "parent" in receipt and receipt["parent"] != "item":
324
333
  return 'receipt.parent: must be "item"'
325
334
  return None
335
+
336
+
337
+ def _retry(retry: object, target_type: str) -> str | None:
338
+ if message := _keys("retry", retry, RETRY_KEYS):
339
+ return message
340
+ attempts = retry.get("attempts")
341
+ if not _is_int(attempts) or not 1 <= attempts <= MAX_RETRY_ATTEMPTS:
342
+ return f"retry.attempts: must be an integer from 1 to {MAX_RETRY_ATTEMPTS}"
343
+ delay = retry.get("delay_seconds", DEFAULT_RETRY_DELAY_SECONDS)
344
+ if not _is_int(delay) or not 0 <= delay <= MAX_RETRY_DELAY_SECONDS:
345
+ return f"retry.delay_seconds: must be an integer from 0 to {MAX_RETRY_DELAY_SECONDS}"
346
+ if target_type != "workflow":
347
+ return "retry: only a workflow action has a run to retry"
348
+ return None
File without changes
File without changes
File without changes
File without changes