dirigent-cli 0.9.0__py3-none-any.whl
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.
- dirigent_cli/__init__.py +5 -0
- dirigent_cli/aliases.py +41 -0
- dirigent_cli/commands.py +2525 -0
- dirigent_cli/context.py +136 -0
- dirigent_cli/formatters.py +158 -0
- dirigent_cli/graph.py +109 -0
- dirigent_cli/health.py +294 -0
- dirigent_cli/local.py +790 -0
- dirigent_cli/main.py +1169 -0
- dirigent_cli/output.py +543 -0
- dirigent_cli/params.py +389 -0
- dirigent_cli/profiles.py +221 -0
- dirigent_cli/project.py +643 -0
- dirigent_cli/py.typed +0 -0
- dirigent_cli/reaper.py +115 -0
- dirigent_cli/scaffold.py +63 -0
- dirigent_cli/schemas.py +85 -0
- dirigent_cli/sources.py +76 -0
- dirigent_cli/stream.py +180 -0
- dirigent_cli/summaries.py +420 -0
- dirigent_cli/templates/pack/README.md.tmpl +23 -0
- dirigent_cli/templates/pack/__init__.py.tmpl +24 -0
- dirigent_cli/templates/pack/operator.py.tmpl +34 -0
- dirigent_cli/templates/pack/pyproject.toml.tmpl +21 -0
- dirigent_cli/templates/pack/test_plugin.py.tmpl +21 -0
- dirigent_cli/timing.py +322 -0
- dirigent_cli/triggers.py +631 -0
- dirigent_cli-0.9.0.dist-info/METADATA +24 -0
- dirigent_cli-0.9.0.dist-info/RECORD +32 -0
- dirigent_cli-0.9.0.dist-info/WHEEL +4 -0
- dirigent_cli-0.9.0.dist-info/entry_points.txt +4 -0
- dirigent_cli-0.9.0.dist-info/licenses/LICENSE +18 -0
dirigent_cli/triggers.py
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
"""The trigger and alerting command groups: schedules, webhooks, and alert rules."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Any, cast
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from dirigent_cli.commands import paged, parse_log_levels, parse_params, parse_priority
|
|
9
|
+
from dirigent_cli.context import Session, client_for, state_of
|
|
10
|
+
from dirigent_cli.output import (
|
|
11
|
+
console,
|
|
12
|
+
emit_fact,
|
|
13
|
+
emit_one,
|
|
14
|
+
emit_records,
|
|
15
|
+
moment,
|
|
16
|
+
refuse,
|
|
17
|
+
render_bool,
|
|
18
|
+
styled,
|
|
19
|
+
table,
|
|
20
|
+
)
|
|
21
|
+
from dirigent_client import AlertEvent, AlertScope, WebhookTokenOut
|
|
22
|
+
|
|
23
|
+
schedule_app = typer.Typer(
|
|
24
|
+
name="schedule", help="A pipeline's clocks: cron, interval, or one-time.", no_args_is_help=True
|
|
25
|
+
)
|
|
26
|
+
webhook_app = typer.Typer(name="webhook", help="Inbound webhooks and their delivery history.", no_args_is_help=True)
|
|
27
|
+
trigger_document_app = typer.Typer(
|
|
28
|
+
name="trigger-document",
|
|
29
|
+
help="Documents that declare clocks for a pipeline defined elsewhere.",
|
|
30
|
+
no_args_is_help=True,
|
|
31
|
+
)
|
|
32
|
+
alerts_app = typer.Typer(name="alerts", help="Alert rules, and testing a channel.", no_args_is_help=True)
|
|
33
|
+
alerts_rules_app = typer.Typer(name="rules", help="The rules that bind an event to a channel.", no_args_is_help=True)
|
|
34
|
+
alerts_app.add_typer(alerts_rules_app)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check_clock(cron: str | None, interval: str | None, at: str | None) -> None:
|
|
38
|
+
"""Refuse zero or two clocks before anything is sent."""
|
|
39
|
+
declared = [flag for flag, value in (("--cron", cron), ("--interval", interval), ("--at", at)) if value]
|
|
40
|
+
if len(declared) != 1:
|
|
41
|
+
named = ", ".join(declared) or "none"
|
|
42
|
+
_fail(f"a schedule takes exactly one of --cron, --interval, or --at ({named} given)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fail(message: str) -> None:
|
|
46
|
+
"""Write a refusal the CLI decided on its own as a record, and exit non-zero."""
|
|
47
|
+
refuse(message)
|
|
48
|
+
raise typer.Exit(code=1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _params_for(dg: Session, pipeline: str, values: list[str] | None, files: list[str] | None) -> dict[str, Any]:
|
|
52
|
+
"""Build a schedule's parameter overrides against the pipeline's own schema."""
|
|
53
|
+
detail = dg.call(dg.pipelines.get(pipeline))
|
|
54
|
+
document = detail.document or {}
|
|
55
|
+
schema = cast("dict[str, Any]", document.get("params") or {})
|
|
56
|
+
return parse_params(values, schema=schema, files=[Path(name) for name in files or []])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@schedule_app.command("create")
|
|
60
|
+
def schedule_create(
|
|
61
|
+
ctx: typer.Context,
|
|
62
|
+
pipeline: Annotated[str, typer.Argument(help="The pipeline this schedule belongs to.")],
|
|
63
|
+
code: Annotated[str, typer.Argument(help="What to call it; unique within the pipeline.")],
|
|
64
|
+
name: Annotated[str | None, typer.Option("--name", help="A human title for this schedule.")] = None,
|
|
65
|
+
description: Annotated[str | None, typer.Option("--description", help="What this schedule is for.")] = None,
|
|
66
|
+
cron: Annotated[str | None, typer.Option("--cron", help="A cron expression, in the schedule's timezone.")] = None,
|
|
67
|
+
interval: Annotated[str | None, typer.Option("--interval", help="A humane duration, such as 1h or 30m.")] = None,
|
|
68
|
+
at: Annotated[str | None, typer.Option("--at", help="An ISO instant, for a schedule that fires once.")] = None,
|
|
69
|
+
timezone: Annotated[str, typer.Option("--tz", help="The IANA zone the clock is read in.")] = "UTC",
|
|
70
|
+
param: Annotated[
|
|
71
|
+
list[str] | None,
|
|
72
|
+
typer.Option("-p", "--param", help="key=value parameter override, repeatable."),
|
|
73
|
+
] = None,
|
|
74
|
+
param_file: Annotated[
|
|
75
|
+
list[str] | None,
|
|
76
|
+
typer.Option("-P", "--params-file", help="A file of parameter overrides, in YAML or JSON."),
|
|
77
|
+
] = None,
|
|
78
|
+
log_level: Annotated[
|
|
79
|
+
list[str] | None,
|
|
80
|
+
typer.Option(
|
|
81
|
+
"--log-level",
|
|
82
|
+
help="A level every fired run keeps (debug) or PATTERN=LEVEL for one block family; "
|
|
83
|
+
"repeatable. Omitted keeps info and up.",
|
|
84
|
+
),
|
|
85
|
+
] = None,
|
|
86
|
+
priority: Annotated[
|
|
87
|
+
str | None,
|
|
88
|
+
typer.Option(
|
|
89
|
+
"--priority",
|
|
90
|
+
help="How far ahead of other runs a fired run is claimed: low, normal, or high. "
|
|
91
|
+
"Omitted takes the pipeline's own.",
|
|
92
|
+
),
|
|
93
|
+
] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Declare a schedule on a pipeline, with its own timezone and parameter overrides."""
|
|
96
|
+
_check_clock(cron, interval, at)
|
|
97
|
+
with client_for(state_of(ctx)) as dg:
|
|
98
|
+
params = _params_for(dg, pipeline, param, param_file)
|
|
99
|
+
created = dg.call(
|
|
100
|
+
dg.schedules.create(
|
|
101
|
+
pipeline,
|
|
102
|
+
code,
|
|
103
|
+
name=name,
|
|
104
|
+
description=description,
|
|
105
|
+
cron=cron,
|
|
106
|
+
interval=interval,
|
|
107
|
+
at=_instant(at),
|
|
108
|
+
timezone=timezone,
|
|
109
|
+
params=params,
|
|
110
|
+
log_levels=parse_log_levels(log_level),
|
|
111
|
+
priority=parse_priority(priority),
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
emit_fact(
|
|
115
|
+
"schedule.created",
|
|
116
|
+
message="created",
|
|
117
|
+
code=created.code,
|
|
118
|
+
pipeline=pipeline,
|
|
119
|
+
name=created.name,
|
|
120
|
+
clock=str(created.cron or created.interval or created.at),
|
|
121
|
+
timezone=created.timezone,
|
|
122
|
+
next_fire_at=created.next_fire_at,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _instant(value: str | None) -> Any:
|
|
127
|
+
"""Read the ``--at`` flag as an ISO instant."""
|
|
128
|
+
from datetime import datetime
|
|
129
|
+
|
|
130
|
+
if value is None:
|
|
131
|
+
return None
|
|
132
|
+
try:
|
|
133
|
+
return datetime.fromisoformat(value)
|
|
134
|
+
except ValueError:
|
|
135
|
+
_fail(f"--at takes an ISO instant, and {value!r} is not one")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@schedule_app.command("list")
|
|
139
|
+
def schedule_list(
|
|
140
|
+
ctx: typer.Context,
|
|
141
|
+
pipeline: Annotated[str, typer.Argument(help="The pipeline whose schedules to list.")],
|
|
142
|
+
) -> None:
|
|
143
|
+
"""List a pipeline's schedules, with when each one next fires."""
|
|
144
|
+
with client_for(state_of(ctx)) as dg:
|
|
145
|
+
rows = list(paged(lambda after, size: dg.call(dg.schedules.list(pipeline, after=after, limit=size)), None))
|
|
146
|
+
if state_of(ctx).json_output:
|
|
147
|
+
return emit_records("schedule", rows)
|
|
148
|
+
table(
|
|
149
|
+
f"schedules of {pipeline}",
|
|
150
|
+
["code", "name", "clock", "timezone", "paused", "next firing", "last fired"],
|
|
151
|
+
[
|
|
152
|
+
[
|
|
153
|
+
row.code,
|
|
154
|
+
row.name or "-",
|
|
155
|
+
str(row.cron or row.interval or row.at),
|
|
156
|
+
row.timezone,
|
|
157
|
+
render_bool(row.paused),
|
|
158
|
+
moment(row.next_fire_at),
|
|
159
|
+
moment(row.last_fired_at),
|
|
160
|
+
]
|
|
161
|
+
for row in rows
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@schedule_app.command("pause")
|
|
167
|
+
def schedule_pause(
|
|
168
|
+
ctx: typer.Context,
|
|
169
|
+
pipeline: Annotated[str, typer.Argument()],
|
|
170
|
+
code: Annotated[str, typer.Argument()],
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Stop a schedule firing, keeping it and its history."""
|
|
173
|
+
with client_for(state_of(ctx)) as dg:
|
|
174
|
+
dg.call(dg.schedules.pause(pipeline, code))
|
|
175
|
+
emit_fact("schedule.paused", message="paused", code=code, pipeline=pipeline)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@schedule_app.command("resume")
|
|
179
|
+
def schedule_resume(
|
|
180
|
+
ctx: typer.Context,
|
|
181
|
+
pipeline: Annotated[str, typer.Argument()],
|
|
182
|
+
code: Annotated[str, typer.Argument()],
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Start a schedule firing again, from the next slot rather than the ones it missed."""
|
|
185
|
+
with client_for(state_of(ctx)) as dg:
|
|
186
|
+
row = dg.call(dg.schedules.resume(pipeline, code))
|
|
187
|
+
emit_fact(
|
|
188
|
+
"schedule.resumed",
|
|
189
|
+
message="resumed",
|
|
190
|
+
code=code,
|
|
191
|
+
pipeline=pipeline,
|
|
192
|
+
next_fire_at=row.next_fire_at,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@schedule_app.command("firings")
|
|
197
|
+
def schedule_firings(
|
|
198
|
+
ctx: typer.Context,
|
|
199
|
+
pipeline: Annotated[str, typer.Argument()],
|
|
200
|
+
code: Annotated[str, typer.Argument()],
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Show what a schedule has actually done, including the firings it skipped."""
|
|
203
|
+
with client_for(state_of(ctx)) as dg:
|
|
204
|
+
rows = list(
|
|
205
|
+
paged(lambda after, size: dg.call(dg.schedules.firings(pipeline, code, after=after, limit=size)), None)
|
|
206
|
+
)
|
|
207
|
+
if state_of(ctx).json_output:
|
|
208
|
+
return emit_records("firing", rows)
|
|
209
|
+
table(
|
|
210
|
+
f"firings of {code}",
|
|
211
|
+
["due", "fired", "outcome", "misfired", "run", "detail"],
|
|
212
|
+
[
|
|
213
|
+
[
|
|
214
|
+
moment(row.scheduled_for),
|
|
215
|
+
moment(row.created_at),
|
|
216
|
+
styled(row.outcome.value),
|
|
217
|
+
render_bool(row.misfired),
|
|
218
|
+
str(row.run_id or "-")[:8],
|
|
219
|
+
row.detail or "-",
|
|
220
|
+
]
|
|
221
|
+
for row in rows
|
|
222
|
+
],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@schedule_app.command("delete")
|
|
227
|
+
def schedule_delete(
|
|
228
|
+
ctx: typer.Context,
|
|
229
|
+
pipeline: Annotated[str, typer.Argument()],
|
|
230
|
+
code: Annotated[str, typer.Argument()],
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Remove a schedule and its firing history."""
|
|
233
|
+
with client_for(state_of(ctx)) as dg:
|
|
234
|
+
dg.call(dg.schedules.delete(pipeline, code))
|
|
235
|
+
emit_fact("schedule.deleted", message="deleted", code=code, pipeline=pipeline)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _print_token(minted: WebhookTokenOut, *, base_url: str) -> None:
|
|
239
|
+
"""Print a minted token once, with the URL already assembled.
|
|
240
|
+
|
|
241
|
+
The instance stores only the token's hash; there is no second chance to read it.
|
|
242
|
+
"""
|
|
243
|
+
console.print(f"\n POST [bold]{base_url.rstrip('/')}{minted.url_path}[/]")
|
|
244
|
+
console.print(f" Token [bold]{minted.token}[/]")
|
|
245
|
+
console.print("\n[yellow]This token is shown once.[/] The instance stores only its hash.\n")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@webhook_app.command("create")
|
|
249
|
+
def webhook_create(
|
|
250
|
+
ctx: typer.Context,
|
|
251
|
+
pipeline: Annotated[str, typer.Argument(help="The pipeline this webhook starts.")],
|
|
252
|
+
code: Annotated[str, typer.Argument(help="What to call it; unique within the pipeline.")],
|
|
253
|
+
name: Annotated[str | None, typer.Option("--name", help="A human title for this webhook.")] = None,
|
|
254
|
+
description: Annotated[str | None, typer.Option("--description", help="What this webhook is for.")] = None,
|
|
255
|
+
map_value: Annotated[
|
|
256
|
+
list[str] | None,
|
|
257
|
+
typer.Option("--map", help="param=$.path.into.payload, repeatable."),
|
|
258
|
+
] = None,
|
|
259
|
+
hmac_secret: Annotated[
|
|
260
|
+
str | None,
|
|
261
|
+
typer.Option("--hmac-secret", help="Require an X-Dirigent-Signature over the raw body."),
|
|
262
|
+
] = None,
|
|
263
|
+
rate_limit: Annotated[int, typer.Option("--rate-limit", help="Deliveries a minute this token may make.")] = 60,
|
|
264
|
+
priority: Annotated[
|
|
265
|
+
str | None,
|
|
266
|
+
typer.Option(
|
|
267
|
+
"--priority",
|
|
268
|
+
help="How far ahead of other runs an accepted delivery's run is claimed: low, normal, or high. "
|
|
269
|
+
"Omitted takes the pipeline's own.",
|
|
270
|
+
),
|
|
271
|
+
] = None,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Declare a webhook and print its token once, with the URL to POST to."""
|
|
274
|
+
mapping: dict[str, str] = {}
|
|
275
|
+
for entry in map_value or []:
|
|
276
|
+
if "=" not in entry:
|
|
277
|
+
_fail(f"--map takes param=$.path.into.payload, and {entry!r} has no '='")
|
|
278
|
+
key, path = entry.split("=", 1)
|
|
279
|
+
mapping[key.strip()] = path.strip()
|
|
280
|
+
with client_for(state_of(ctx)) as dg:
|
|
281
|
+
minted = dg.call(
|
|
282
|
+
dg.webhooks.create(
|
|
283
|
+
pipeline,
|
|
284
|
+
code,
|
|
285
|
+
name=name,
|
|
286
|
+
description=description,
|
|
287
|
+
params_from_payload=mapping,
|
|
288
|
+
hmac_secret=hmac_secret,
|
|
289
|
+
rate_limit_per_minute=rate_limit,
|
|
290
|
+
priority=parse_priority(priority),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
base = dg.url
|
|
294
|
+
if state_of(ctx).json_output:
|
|
295
|
+
return emit_fact(
|
|
296
|
+
"webhook.created",
|
|
297
|
+
message="created",
|
|
298
|
+
code=minted.code,
|
|
299
|
+
pipeline=pipeline,
|
|
300
|
+
url=f"{base.rstrip('/')}{minted.url_path}",
|
|
301
|
+
token=minted.token,
|
|
302
|
+
)
|
|
303
|
+
console.print(f"[green]created[/] webhook [bold]{minted.code}[/] on {pipeline}")
|
|
304
|
+
_print_token(minted, base_url=base)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@webhook_app.command("list")
|
|
308
|
+
def webhook_list(
|
|
309
|
+
ctx: typer.Context,
|
|
310
|
+
pipeline: Annotated[str, typer.Argument(help="The pipeline whose webhooks to list.")],
|
|
311
|
+
) -> None:
|
|
312
|
+
"""List a pipeline's webhooks, with their mapping but never their tokens."""
|
|
313
|
+
with client_for(state_of(ctx)) as dg:
|
|
314
|
+
rows = list(paged(lambda after, size: dg.call(dg.webhooks.list(pipeline, after=after, limit=size)), None))
|
|
315
|
+
if state_of(ctx).json_output:
|
|
316
|
+
return emit_records("webhook", rows)
|
|
317
|
+
table(
|
|
318
|
+
f"webhooks of {pipeline}",
|
|
319
|
+
["code", "name", "token", "signed", "active", "limit/min", "maps", "last delivery"],
|
|
320
|
+
[
|
|
321
|
+
[
|
|
322
|
+
row.code,
|
|
323
|
+
row.name or "-",
|
|
324
|
+
f"{row.token_prefix}...",
|
|
325
|
+
render_bool(row.signed),
|
|
326
|
+
render_bool(row.active),
|
|
327
|
+
str(row.rate_limit_per_minute),
|
|
328
|
+
", ".join(sorted(row.params_from_payload)) or "-",
|
|
329
|
+
moment(row.last_delivery_at),
|
|
330
|
+
]
|
|
331
|
+
for row in rows
|
|
332
|
+
],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@webhook_app.command("rotate-token")
|
|
337
|
+
def webhook_rotate(
|
|
338
|
+
ctx: typer.Context,
|
|
339
|
+
pipeline: Annotated[str, typer.Argument()],
|
|
340
|
+
code: Annotated[str, typer.Argument()],
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Mint a new token and forget the old one; every caller has to be updated."""
|
|
343
|
+
with client_for(state_of(ctx)) as dg:
|
|
344
|
+
minted = dg.call(dg.webhooks.rotate_token(pipeline, code))
|
|
345
|
+
base = dg.url
|
|
346
|
+
if state_of(ctx).json_output:
|
|
347
|
+
return emit_fact(
|
|
348
|
+
"webhook.token_rotated",
|
|
349
|
+
message="rotated",
|
|
350
|
+
code=code,
|
|
351
|
+
pipeline=pipeline,
|
|
352
|
+
url=f"{base.rstrip('/')}{minted.url_path}",
|
|
353
|
+
token=minted.token,
|
|
354
|
+
)
|
|
355
|
+
console.print(f"[green]rotated[/] the token of webhook [bold]{code}[/]; the previous one no longer works")
|
|
356
|
+
_print_token(minted, base_url=base)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@webhook_app.command("deliveries")
|
|
360
|
+
def webhook_deliveries(
|
|
361
|
+
ctx: typer.Context,
|
|
362
|
+
pipeline: Annotated[str, typer.Argument()],
|
|
363
|
+
code: Annotated[str, typer.Argument()],
|
|
364
|
+
) -> None:
|
|
365
|
+
"""Show what has arrived at a webhook, refusals included."""
|
|
366
|
+
with client_for(state_of(ctx)) as dg:
|
|
367
|
+
rows = list(
|
|
368
|
+
paged(lambda after, size: dg.call(dg.webhooks.deliveries(pipeline, code, after=after, limit=size)), None)
|
|
369
|
+
)
|
|
370
|
+
if state_of(ctx).json_output:
|
|
371
|
+
return emit_records("delivery", rows)
|
|
372
|
+
table(
|
|
373
|
+
f"deliveries to {code}",
|
|
374
|
+
["received", "outcome", "run", "from", "detail"],
|
|
375
|
+
[
|
|
376
|
+
[
|
|
377
|
+
moment(row.created_at),
|
|
378
|
+
styled(row.outcome.value),
|
|
379
|
+
str(row.run_id or "-")[:8],
|
|
380
|
+
row.source or "-",
|
|
381
|
+
row.reason or "-",
|
|
382
|
+
]
|
|
383
|
+
for row in rows
|
|
384
|
+
],
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@webhook_app.command("delete")
|
|
389
|
+
def webhook_delete(
|
|
390
|
+
ctx: typer.Context,
|
|
391
|
+
pipeline: Annotated[str, typer.Argument()],
|
|
392
|
+
code: Annotated[str, typer.Argument()],
|
|
393
|
+
) -> None:
|
|
394
|
+
"""Remove a webhook, its token, and its delivery history."""
|
|
395
|
+
with client_for(state_of(ctx)) as dg:
|
|
396
|
+
dg.call(dg.webhooks.delete(pipeline, code))
|
|
397
|
+
emit_fact("webhook.deleted", message="deleted", code=code, pipeline=pipeline)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@alerts_rules_app.command("list")
|
|
401
|
+
def alerts_rules_list(
|
|
402
|
+
ctx: typer.Context,
|
|
403
|
+
) -> None:
|
|
404
|
+
"""List the alert rules this instance holds."""
|
|
405
|
+
with client_for(state_of(ctx)) as dg:
|
|
406
|
+
rows = list(paged(lambda after, size: dg.call(dg.alerts.rules(after=after, limit=size)), None))
|
|
407
|
+
if state_of(ctx).json_output:
|
|
408
|
+
return emit_records("alert_rule", rows)
|
|
409
|
+
table(
|
|
410
|
+
"alert rules",
|
|
411
|
+
["code", "name", "event", "scope", "notifier", "throttle", "active", "last sent"],
|
|
412
|
+
[
|
|
413
|
+
[
|
|
414
|
+
row.code,
|
|
415
|
+
row.name or "-",
|
|
416
|
+
row.event.value,
|
|
417
|
+
row.pipeline or row.scope.value,
|
|
418
|
+
row.notifier,
|
|
419
|
+
row.throttle,
|
|
420
|
+
render_bool(row.active),
|
|
421
|
+
moment(row.last_sent_at),
|
|
422
|
+
]
|
|
423
|
+
for row in rows
|
|
424
|
+
],
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@alerts_rules_app.command("create")
|
|
429
|
+
def alerts_rules_create(
|
|
430
|
+
ctx: typer.Context,
|
|
431
|
+
code: Annotated[str, typer.Argument(help="What to call the rule.")],
|
|
432
|
+
event: Annotated[
|
|
433
|
+
str,
|
|
434
|
+
typer.Option(
|
|
435
|
+
"--event",
|
|
436
|
+
help="The event that fires it: run_failed, run_completed_with_errors, run_succeeded, or run_stuck.",
|
|
437
|
+
),
|
|
438
|
+
],
|
|
439
|
+
notifier: Annotated[str, typer.Option("--notifier", help="The channel to deliver through, such as log.")],
|
|
440
|
+
name: Annotated[str | None, typer.Option("--name", help="A human title for this rule.")] = None,
|
|
441
|
+
description: Annotated[str | None, typer.Option("--description", help="What this rule is for.")] = None,
|
|
442
|
+
pipeline: Annotated[str | None, typer.Option("--pipeline", help="Watch one pipeline instead of all.")] = None,
|
|
443
|
+
connection: Annotated[str | None, typer.Option("--connection", help="The credential the channel uses.")] = None,
|
|
444
|
+
template: Annotated[str | None, typer.Option("--template", help="Subject template, reading ${run.*}.")] = None,
|
|
445
|
+
throttle: Annotated[str, typer.Option("--throttle", help="At most one message per window, e.g. 15m.")] = "0s",
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Declare an alert rule binding an event at a scope to a channel."""
|
|
448
|
+
if event not in set(AlertEvent):
|
|
449
|
+
_fail(f"{event!r} is not an alert event ({', '.join(sorted(AlertEvent))})")
|
|
450
|
+
with client_for(state_of(ctx)) as dg:
|
|
451
|
+
created = dg.call(
|
|
452
|
+
dg.alerts.create_rule(
|
|
453
|
+
code,
|
|
454
|
+
name=name,
|
|
455
|
+
description=description,
|
|
456
|
+
event=AlertEvent(event),
|
|
457
|
+
notifier=notifier,
|
|
458
|
+
scope=AlertScope.PIPELINE if pipeline else AlertScope.GLOBAL,
|
|
459
|
+
pipeline=pipeline,
|
|
460
|
+
connection=connection,
|
|
461
|
+
template=template,
|
|
462
|
+
throttle=throttle,
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
emit_fact(
|
|
466
|
+
"alert_rule.created",
|
|
467
|
+
message="created",
|
|
468
|
+
code=created.code,
|
|
469
|
+
name=created.name,
|
|
470
|
+
event=created.event.value,
|
|
471
|
+
scope=created.pipeline or created.scope.value,
|
|
472
|
+
notifier=created.notifier,
|
|
473
|
+
connection=created.connection,
|
|
474
|
+
throttle=created.throttle,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@alerts_rules_app.command("delete")
|
|
479
|
+
def alerts_rules_delete(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> None:
|
|
480
|
+
"""Remove an alert rule; the notifications it already raised are kept."""
|
|
481
|
+
with client_for(state_of(ctx)) as dg:
|
|
482
|
+
dg.call(dg.alerts.delete_rule(code))
|
|
483
|
+
emit_fact("alert_rule.deleted", message="deleted", code=code, notifications="kept")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@alerts_rules_app.command("pause")
|
|
487
|
+
def alerts_rules_pause(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> None:
|
|
488
|
+
"""Hold a rule's deliveries; the rule itself is left as it was declared."""
|
|
489
|
+
_set_rule_paused(ctx, code, paused=True)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@alerts_rules_app.command("resume")
|
|
493
|
+
def alerts_rules_resume(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> None:
|
|
494
|
+
"""Let a held rule deliver again."""
|
|
495
|
+
_set_rule_paused(ctx, code, paused=False)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _set_rule_paused(ctx: typer.Context, code: str, *, paused: bool) -> None:
|
|
499
|
+
"""Write a rule's paused flag and say what it is now."""
|
|
500
|
+
with client_for(state_of(ctx)) as dg:
|
|
501
|
+
rule = dg.call(dg.alerts.set_rule_paused(code, paused=paused))
|
|
502
|
+
emit_fact(
|
|
503
|
+
"alert_rule.paused" if rule.paused else "alert_rule.resumed",
|
|
504
|
+
message="paused" if rule.paused else "resumed",
|
|
505
|
+
code=rule.code,
|
|
506
|
+
event=rule.event.value,
|
|
507
|
+
notifier=rule.notifier,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@alerts_app.command("test")
|
|
512
|
+
def alerts_test(
|
|
513
|
+
ctx: typer.Context,
|
|
514
|
+
notifier: Annotated[str, typer.Argument(help="The channel to send through, such as log or webhook.")],
|
|
515
|
+
connection: Annotated[
|
|
516
|
+
str | None,
|
|
517
|
+
typer.Option("--connection", help="The credential record the channel delivers through."),
|
|
518
|
+
] = None,
|
|
519
|
+
subject: Annotated[str, typer.Option("--subject", help="What the test message says.")] = "dirigent test alert",
|
|
520
|
+
) -> None:
|
|
521
|
+
"""Send a test message through a channel, on the same queue a real alert takes."""
|
|
522
|
+
with client_for(state_of(ctx)) as dg:
|
|
523
|
+
queued = dg.call(dg.alerts.test(notifier=notifier, connection=connection, subject=subject))
|
|
524
|
+
emit_fact(
|
|
525
|
+
"notification.queued",
|
|
526
|
+
message="queued",
|
|
527
|
+
notification_id=str(queued.notification_id),
|
|
528
|
+
notifier=queued.notifier,
|
|
529
|
+
connection=connection,
|
|
530
|
+
subject=subject,
|
|
531
|
+
detail=queued.detail,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@alerts_app.command("queue")
|
|
536
|
+
def alerts_queue(
|
|
537
|
+
ctx: typer.Context,
|
|
538
|
+
) -> None:
|
|
539
|
+
"""Show queued and delivered alerts."""
|
|
540
|
+
with client_for(state_of(ctx)) as dg:
|
|
541
|
+
rows = list(paged(lambda after, size: dg.call(dg.alerts.notifications(after=after, limit=size)), None))
|
|
542
|
+
if state_of(ctx).json_output:
|
|
543
|
+
return emit_records("notification", rows)
|
|
544
|
+
table(
|
|
545
|
+
"notifications",
|
|
546
|
+
["subject", "notifier", "status", "tries", "sent", "error"],
|
|
547
|
+
[
|
|
548
|
+
[
|
|
549
|
+
row.subject,
|
|
550
|
+
row.notifier,
|
|
551
|
+
styled(row.status.value),
|
|
552
|
+
str(row.attempt),
|
|
553
|
+
moment(row.sent_at),
|
|
554
|
+
(row.error or "-")[:60],
|
|
555
|
+
]
|
|
556
|
+
for row in rows
|
|
557
|
+
],
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@trigger_document_app.command("list")
|
|
562
|
+
def trigger_document_list(ctx: typer.Context) -> None:
|
|
563
|
+
"""List the triggers documents this instance holds, with the pipeline each one fires."""
|
|
564
|
+
with client_for(state_of(ctx)) as dg:
|
|
565
|
+
rows = list(paged(lambda after, size: dg.call(dg.trigger_documents.list(after=after, limit=size)), None))
|
|
566
|
+
if state_of(ctx).json_output:
|
|
567
|
+
return emit_records("trigger_document", rows)
|
|
568
|
+
table(
|
|
569
|
+
"trigger documents",
|
|
570
|
+
["code", "name", "pipeline", "source", "applied by", "applied"],
|
|
571
|
+
[
|
|
572
|
+
[
|
|
573
|
+
row.code,
|
|
574
|
+
row.name or "-",
|
|
575
|
+
row.pipeline,
|
|
576
|
+
row.provenance_source.value,
|
|
577
|
+
row.applied_by or "-",
|
|
578
|
+
moment(row.updated_at),
|
|
579
|
+
]
|
|
580
|
+
for row in rows
|
|
581
|
+
],
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@trigger_document_app.command("show")
|
|
586
|
+
def trigger_document_show(
|
|
587
|
+
ctx: typer.Context,
|
|
588
|
+
code: Annotated[str, typer.Argument(help="The triggers document to read.")],
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Show one triggers document and the schedules and webhooks it owns."""
|
|
591
|
+
with client_for(state_of(ctx)) as dg:
|
|
592
|
+
detail = dg.call(dg.trigger_documents.get(code))
|
|
593
|
+
if state_of(ctx).json_output:
|
|
594
|
+
return emit_one("trigger_document", detail)
|
|
595
|
+
console.print(f"[bold]{detail.name or detail.code}[/] [dim]{detail.code}[/]")
|
|
596
|
+
console.print(f" pipeline [bold]{detail.pipeline}[/]")
|
|
597
|
+
console.print(f" digest [dim]{detail.digest}[/]")
|
|
598
|
+
console.print(f" schedules {', '.join(detail.schedules) or '-'}")
|
|
599
|
+
console.print(f" webhooks {', '.join(detail.webhooks) or '-'}")
|
|
600
|
+
if detail.description:
|
|
601
|
+
console.print(f"\n{detail.description}")
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@trigger_document_app.command("delete")
|
|
605
|
+
def trigger_document_delete(
|
|
606
|
+
ctx: typer.Context,
|
|
607
|
+
code: Annotated[str, typer.Argument(help="The triggers document to remove.")],
|
|
608
|
+
) -> None:
|
|
609
|
+
"""Remove a triggers document and every schedule and webhook it declared."""
|
|
610
|
+
with client_for(state_of(ctx)) as dg:
|
|
611
|
+
dg.call(dg.trigger_documents.delete(code))
|
|
612
|
+
emit_fact("trigger_document.deleted", message="deleted", code=code, schedules="deleted", webhooks="deleted")
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@alerts_app.command("retry")
|
|
616
|
+
def alerts_retry(
|
|
617
|
+
ctx: typer.Context,
|
|
618
|
+
notification: Annotated[str, typer.Argument(metavar="NOTIFICATION", help="The notification's id.")],
|
|
619
|
+
) -> None:
|
|
620
|
+
"""Put one notification back on the queue, due now."""
|
|
621
|
+
with client_for(state_of(ctx)) as dg:
|
|
622
|
+
row = dg.call(dg.alerts.retry(notification))
|
|
623
|
+
emit_fact(
|
|
624
|
+
"notification.retried",
|
|
625
|
+
message="queued",
|
|
626
|
+
notification_id=str(row.id),
|
|
627
|
+
notifier=row.notifier,
|
|
628
|
+
subject=row.subject,
|
|
629
|
+
attempt=row.attempt,
|
|
630
|
+
available_at=row.available_at,
|
|
631
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dirigent-cli
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: The dirigent command line interface (dirigent / dg).
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: dirigent-blocks
|
|
8
|
+
Requires-Dist: dirigent-client
|
|
9
|
+
Requires-Dist: dirigent-common
|
|
10
|
+
Requires-Dist: dirigent-core
|
|
11
|
+
Requires-Dist: dirigent-plugin
|
|
12
|
+
Requires-Dist: dirigent-server
|
|
13
|
+
Requires-Dist: httpx2>=2.12.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
15
|
+
Requires-Dist: rich>=15.0.0
|
|
16
|
+
Requires-Dist: tomli-w>=1.2.0
|
|
17
|
+
Requires-Dist: typer>=0.27.2
|
|
18
|
+
Requires-Dist: uvicorn>=0.52.4
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# dirigent-cli
|
|
23
|
+
|
|
24
|
+
The dirigent command line interface (dirigent / dg).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
dirigent_cli/__init__.py,sha256=XiMCe3t4MmWf7o1e3lZFiAwWPV7-yHWxDbUkaHD6TJw,143
|
|
2
|
+
dirigent_cli/aliases.py,sha256=dNvLlFQFXsf_a-8jfBEHhOk7o61ybueAH-ddHDKlLMU,1691
|
|
3
|
+
dirigent_cli/commands.py,sha256=8x1L1A4ugylKKM2iRC3F4s3olcfo4nd7WQ9Po8GznHk,97845
|
|
4
|
+
dirigent_cli/context.py,sha256=W1Uq7-wW8Yiblx6_ZoR6prA0aqEziaTUs7m51nmG12g,4973
|
|
5
|
+
dirigent_cli/formatters.py,sha256=jpHjF4CBxRXYr330jeSzG7Hav-5ILs6CvXLHVu5eE5g,6181
|
|
6
|
+
dirigent_cli/graph.py,sha256=c4lbbBnOtMZDqtR2HTz--vktUyiCmffRV8GUe94SMAQ,4161
|
|
7
|
+
dirigent_cli/health.py,sha256=KTvVmdz_aLGS84zkgCSGuBamsc7I3nY4-oWCh9nBoJw,13495
|
|
8
|
+
dirigent_cli/local.py,sha256=y3Djgv5pQsh9PiRvV_Xu5z6fVUeyR9cPq7YZ6fe-E34,32096
|
|
9
|
+
dirigent_cli/main.py,sha256=Sy6N6dq4wJullhB_wPmeHEPFH_7TcU25gandhten0Fc,45498
|
|
10
|
+
dirigent_cli/output.py,sha256=OLxw4GN_EKe66bJCA3Rn8g9JAQJZ3lvd7-AlbzBm3tI,20202
|
|
11
|
+
dirigent_cli/params.py,sha256=XxszRHK3xXiBB0A-NUYpsKgdar9OUnOIQkcDzT1jvBk,15310
|
|
12
|
+
dirigent_cli/profiles.py,sha256=viTS-nRCxsRH_KaOC3Amec0eZTNHpm6cr-75xcLwwGM,8319
|
|
13
|
+
dirigent_cli/project.py,sha256=db8Lt2q-hGe6sY7WYdAkYu2b4gQQVcVE34FyigGdXn0,23081
|
|
14
|
+
dirigent_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
dirigent_cli/reaper.py,sha256=IHW7fT2wmvpBe50pv4st9U2W0BETNo6S7edrCdOgp8E,4294
|
|
16
|
+
dirigent_cli/scaffold.py,sha256=z_hLUD0gaSRTNgLTBSNKvavejEAJKWjOjCx06LIkhx8,2353
|
|
17
|
+
dirigent_cli/schemas.py,sha256=k3VLPKY84Rn7cR7VAKUfeqlp-xic8FueAwjoiPQL-uk,2648
|
|
18
|
+
dirigent_cli/sources.py,sha256=wbOOamQ-9kipOyXq0UHJE2XQEhaRsvHr0_uATb9Sc38,2481
|
|
19
|
+
dirigent_cli/stream.py,sha256=aonQzc2REzHnDtm0aNB025nc5N8jUNPUK01YeoK0BHk,6455
|
|
20
|
+
dirigent_cli/summaries.py,sha256=vLcG5XAlmkPrZzv4O4LOIYGZf9ZjPjDXKJpEhtEZuIg,16326
|
|
21
|
+
dirigent_cli/templates/pack/README.md.tmpl,sha256=no-cnv5lXkbWO-HvjeGUc3Nm_Fwsjgj5iCqUZn6Y9Nc,668
|
|
22
|
+
dirigent_cli/templates/pack/__init__.py.tmpl,sha256=WGjM6oQyn7Y99oENs7sexb5XcJxr7YjySpatDZfX8lc,566
|
|
23
|
+
dirigent_cli/templates/pack/operator.py.tmpl,sha256=VxqwqvT2Par_oEFjstZNc8EWkD_wIC3M8qEt9symD_E,1035
|
|
24
|
+
dirigent_cli/templates/pack/pyproject.toml.tmpl,sha256=17qC1vShBi0-OnGTdD8S_2SLH1xG5RNNdrbe7kdVvKA,528
|
|
25
|
+
dirigent_cli/templates/pack/test_plugin.py.tmpl,sha256=fRVpXAdLPiwdaMmFmV9yC9EgU6qrOUbD447fUysW_8I,893
|
|
26
|
+
dirigent_cli/timing.py,sha256=sU_vk2sSx1WMei6rFtIvvdUB8UP1cwIPkHoX6KiOraU,13591
|
|
27
|
+
dirigent_cli/triggers.py,sha256=7VvkNaasTD9nXAInsreXDUb2PhIr8cHIZOret40t9WQ,23660
|
|
28
|
+
dirigent_cli-0.9.0.dist-info/licenses/LICENSE,sha256=LKBm7Cx-WBc1zca4DjGxq99VEpAiWGnZDxIKmntn1hQ,910
|
|
29
|
+
dirigent_cli-0.9.0.dist-info/WHEEL,sha256=mru_b36sH6joUMnwf7IlFCun3RoDjrNg9RcfBmEcqsE,81
|
|
30
|
+
dirigent_cli-0.9.0.dist-info/entry_points.txt,sha256=mnlAulZPEjH3OIqTvGHJBI0AgcnnSeWYi6mkLe-8qk4,79
|
|
31
|
+
dirigent_cli-0.9.0.dist-info/METADATA,sha256=ZhRCzEjWyXIGUPksxhKAwgoCWWuRi0w2FtGMk7QpQ9M,678
|
|
32
|
+
dirigent_cli-0.9.0.dist-info/RECORD,,
|