dirigent-cli 0.22.0__tar.gz → 0.23.1__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 (37) hide show
  1. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/PKG-INFO +9 -9
  2. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/pyproject.toml +9 -9
  3. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/pyproject.toml.orig +9 -9
  4. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/commands.py +96 -14
  5. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/messages.py +5 -0
  6. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/output.py +5 -0
  7. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/seeding.py +13 -3
  8. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/summaries.py +29 -0
  9. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/triggers.py +2 -2
  10. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/LICENSE +0 -0
  11. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/README.md +0 -0
  12. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/__init__.py +0 -0
  13. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/aliases.py +0 -0
  14. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/context.py +0 -0
  15. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/explain.py +0 -0
  16. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/formatters.py +0 -0
  17. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/graph.py +0 -0
  18. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/health.py +0 -0
  19. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/init_form.py +0 -0
  20. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/local.py +0 -0
  21. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/main.py +0 -0
  22. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/params.py +0 -0
  23. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/profiles.py +0 -0
  24. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/project.py +0 -0
  25. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/py.typed +0 -0
  26. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/reaper.py +0 -0
  27. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/scaffold.py +0 -0
  28. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/schemas.py +0 -0
  29. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/sources.py +0 -0
  30. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/starters.py +0 -0
  31. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/stream.py +0 -0
  32. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
  33. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
  34. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
  35. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
  36. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
  37. {dirigent_cli-0.22.0 → dirigent_cli-0.23.1}/src/dirigent_cli/timing.py +0 -0
@@ -1,19 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigent-cli
3
- Version: 0.22.0
3
+ Version: 0.23.1
4
4
  Summary: The dirigent command line interface (dirigent / dg).
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3.13
9
- Requires-Dist: dirigent-block-execute==0.22.0
10
- Requires-Dist: dirigent-blocks==0.22.0
11
- Requires-Dist: dirigent-client==0.22.0
12
- Requires-Dist: dirigent-common==0.22.0
13
- Requires-Dist: dirigent-core==0.22.0
14
- Requires-Dist: dirigent-examples==0.22.0
15
- Requires-Dist: dirigent-plugin==0.22.0
16
- Requires-Dist: dirigent-server==0.22.0
9
+ Requires-Dist: dirigent-block-execute==0.23.1
10
+ Requires-Dist: dirigent-blocks==0.23.1
11
+ Requires-Dist: dirigent-client==0.23.1
12
+ Requires-Dist: dirigent-common==0.23.1
13
+ Requires-Dist: dirigent-core==0.23.1
14
+ Requires-Dist: dirigent-examples==0.23.1
15
+ Requires-Dist: dirigent-plugin==0.23.1
16
+ Requires-Dist: dirigent-server==0.23.1
17
17
  Requires-Dist: httpx2>=2.12.0
18
18
  Requires-Dist: python-dotenv>=1.1.0
19
19
  Requires-Dist: pyyaml>=6.0.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.22.0"
3
+ version = "0.23.1"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,14 +11,14 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-execute==0.22.0",
15
- "dirigent-blocks==0.22.0",
16
- "dirigent-client==0.22.0",
17
- "dirigent-common==0.22.0",
18
- "dirigent-core==0.22.0",
19
- "dirigent-examples==0.22.0",
20
- "dirigent-plugin==0.22.0",
21
- "dirigent-server==0.22.0",
14
+ "dirigent-block-execute==0.23.1",
15
+ "dirigent-blocks==0.23.1",
16
+ "dirigent-client==0.23.1",
17
+ "dirigent-common==0.23.1",
18
+ "dirigent-core==0.23.1",
19
+ "dirigent-examples==0.23.1",
20
+ "dirigent-plugin==0.23.1",
21
+ "dirigent-server==0.23.1",
22
22
  "httpx2>=2.12.0",
23
23
  "python-dotenv>=1.1.0",
24
24
  "pyyaml>=6.0.3",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.22.0"
3
+ version = "0.23.1"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,14 +11,14 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-execute==0.22.0",
15
- "dirigent-blocks==0.22.0",
16
- "dirigent-client==0.22.0",
17
- "dirigent-common==0.22.0",
18
- "dirigent-core==0.22.0",
19
- "dirigent-examples==0.22.0",
20
- "dirigent-plugin==0.22.0",
21
- "dirigent-server==0.22.0",
14
+ "dirigent-block-execute==0.23.1",
15
+ "dirigent-blocks==0.23.1",
16
+ "dirigent-client==0.23.1",
17
+ "dirigent-common==0.23.1",
18
+ "dirigent-core==0.23.1",
19
+ "dirigent-examples==0.23.1",
20
+ "dirigent-plugin==0.23.1",
21
+ "dirigent-server==0.23.1",
22
22
  "httpx2>=2.12.0",
23
23
  "python-dotenv>=1.1.0",
24
24
  "pyyaml>=6.0.3",
@@ -74,6 +74,7 @@ from dirigent_cli.messages import (
74
74
  RUN_SKIPPED,
75
75
  SCAFFOLD_REFUSED,
76
76
  SCHEMA_NOT_AN_OBJECT,
77
+ SCHEMA_NOTHING_TO_CHANGE,
77
78
  SCHEMA_UNREADABLE,
78
79
  SET_DG_TOKEN,
79
80
  SOURCE_REFUSED,
@@ -87,6 +88,7 @@ from dirigent_cli.messages import (
87
88
  from dirigent_cli.messages import WINDOW_GRAMMAR as WINDOW_GRAMMAR_MESSAGE
88
89
  from dirigent_cli.output import (
89
90
  CONNECTION_HEALTH,
91
+ RUNS_IN_FLIGHT,
90
92
  Detail,
91
93
  age,
92
94
  connection_health,
@@ -135,6 +137,7 @@ from dirigent_cli.sources import (
135
137
  from dirigent_cli.stream import Sink, track_steps, use_scratch_prefix
136
138
  from dirigent_cli.timing import RunProfile, attempt_timing, by_step, profile, step_timing
137
139
  from dirigent_client import (
140
+ CLEAR,
138
141
  ApplyResult,
139
142
  AttemptEvent,
140
143
  AttemptOut,
@@ -142,6 +145,7 @@ from dirigent_client import (
142
145
  BackfillAccepted,
143
146
  BlockKind,
144
147
  Catalog,
148
+ Clear,
145
149
  DocumentKind,
146
150
  ExampleDetail,
147
151
  ExampleOut,
@@ -159,6 +163,7 @@ from dirigent_client import (
159
163
  RunReport,
160
164
  RunStatus,
161
165
  ValidationIssue,
166
+ declared_label,
162
167
  )
163
168
  from dirigent_common import Issue, JsonMap, Message
164
169
  from dirigent_core import migrations
@@ -823,7 +828,7 @@ def pipeline_list(
823
828
  return emit_records("pipeline", rows)
824
829
  table(
825
830
  "pipelines",
826
- ["code", "name", "tags", "version", "active", "runs in flight", "updated"],
831
+ ["code", "name", "tags", "version", "active", RUNS_IN_FLIGHT, "updated"],
827
832
  [
828
833
  [
829
834
  row.code,
@@ -859,7 +864,7 @@ def pipeline_show(
859
864
  "importance": row.importance.value,
860
865
  "active": "yes" if row.active else "no",
861
866
  "current version": row.current_version,
862
- "runs in flight": row.active_runs,
867
+ RUNS_IN_FLIGHT: row.active_runs,
863
868
  "created": moment(row.created_at),
864
869
  },
865
870
  )
@@ -2465,8 +2470,11 @@ def schema_list(ctx: typer.Context) -> None:
2465
2470
  return emit_records("schema", rows)
2466
2471
  table(
2467
2472
  "schemas",
2468
- ["code", "name", "description"],
2469
- [[row.code, row.name or "-", row.description or "-"] for row in rows],
2473
+ ["code", "name", "description", "used by"],
2474
+ [
2475
+ [row.code, row.name or "-", row.description or "-", str(len(row.used_by)) if row.used_by else "-"]
2476
+ for row in rows
2477
+ ],
2470
2478
  )
2471
2479
 
2472
2480
 
@@ -2483,6 +2491,19 @@ def schema_create(
2483
2491
  ] = None,
2484
2492
  ) -> None:
2485
2493
  """Store a locally authored JSON Schema, taking its identity from its own keywords."""
2494
+ schema, document = _read_schema(reference)
2495
+ resolved = code
2496
+ if resolved is None and not (isinstance(schema.get("$id"), str) and code_from_id(cast("str", schema["$id"]))):
2497
+ resolved = code_from_id(Path(document.ref).name) if document.ref != "(stdin)" else None
2498
+ with client_for(state_of(ctx)) as dg:
2499
+ created = dg.call(dg.schemas.create(schema, code=resolved, name=name, description=description))
2500
+ if state_of(ctx).json_output:
2501
+ return emit_fact("schema.created", message="created", code=created.code, name=created.name)
2502
+ console.print(f"[green]stored[/] schema [bold]{created.code}[/]")
2503
+
2504
+
2505
+ def _read_schema(reference: str) -> tuple[JsonMap, Document]:
2506
+ """Read a locally authored JSON Schema from a file, a URL, or standard input."""
2486
2507
  try:
2487
2508
  document = read_document(reference)
2488
2509
  except SourceError as error:
@@ -2493,15 +2514,69 @@ def schema_create(
2493
2514
  fail(SCHEMA_UNREADABLE, label=document.label, detail=str(error))
2494
2515
  if not isinstance(body, dict):
2495
2516
  fail(SCHEMA_NOT_AN_OBJECT, label=document.label, kind=type(body).__name__)
2496
- schema = cast("JsonMap", body)
2497
- resolved = code
2498
- if resolved is None and not (isinstance(schema.get("$id"), str) and code_from_id(cast("str", schema["$id"]))):
2499
- resolved = code_from_id(Path(document.ref).name) if document.ref != "(stdin)" else None
2517
+ return cast("JsonMap", body), document
2518
+
2519
+
2520
+ def _label_given(text: str | None) -> str | Clear | None:
2521
+ """Read a label option: not given leaves what is stored, and given empty clears it.
2522
+
2523
+ An empty title is the absence of one rather than a title, and no screen can draw it, so
2524
+ the empty string is never what gets stored.
2525
+ """
2526
+ if text is None:
2527
+ return None
2528
+ return text.strip() or CLEAR
2529
+
2530
+
2531
+ @schema_app.command("update")
2532
+ def schema_update(
2533
+ ctx: typer.Context,
2534
+ code: Annotated[str, typer.Argument(help="The schema to change; a code is fixed once minted.")],
2535
+ reference: Annotated[
2536
+ str | None,
2537
+ typer.Argument(help="A JSON Schema file, or - for standard input, to replace the stored body with."),
2538
+ ] = None,
2539
+ name: Annotated[
2540
+ str | None,
2541
+ typer.Option(help="A human title; taken from the replacing schema's title when omitted, cleared when empty."),
2542
+ ] = None,
2543
+ description: Annotated[
2544
+ str | None,
2545
+ typer.Option(help="What the schema is for; taken from the replacing schema's description, cleared when empty."),
2546
+ ] = None,
2547
+ ) -> None:
2548
+ """Correct a stored schema: replace the shape, relabel it, or both.
2549
+
2550
+ What this was not given is left as it stands, and an option given empty clears what is
2551
+ stored. Replacing the body takes the new schema's own ``title`` and ``description`` where
2552
+ the options name neither, exactly as storing one does, so the labels belong to the shape
2553
+ the instance now holds rather than to the one it no longer does.
2554
+
2555
+ An edit is never refused for being depended on: a code is fixed once minted, so refusing
2556
+ it would freeze an in-use shape permanently. The answer names the pipelines that validate
2557
+ against it, and the decision was made for them.
2558
+ """
2559
+ schema = _read_schema(reference)[0] if reference is not None else None
2560
+ label = _label_given(name)
2561
+ about = _label_given(description)
2562
+ if schema is not None:
2563
+ label = declared_label(schema, "title") if label is None else label
2564
+ about = declared_label(schema, "description") if about is None else about
2565
+ changed = [
2566
+ field for field, given in (("name", label), ("description", about), ("body", schema)) if given is not None
2567
+ ]
2568
+ if not changed:
2569
+ fail(SCHEMA_NOTHING_TO_CHANGE, code=code)
2500
2570
  with client_for(state_of(ctx)) as dg:
2501
- created = dg.call(dg.schemas.create(schema, code=resolved, name=name, description=description))
2502
- if state_of(ctx).json_output:
2503
- return emit_fact("schema.created", message="created", code=created.code, name=created.name)
2504
- console.print(f"[green]stored[/] schema [bold]{created.code}[/]")
2571
+ edited = dg.call(dg.schemas.update(code, body=schema, name=label, description=about))
2572
+ emit_fact(
2573
+ "schema.updated",
2574
+ message="updated",
2575
+ code=edited.code,
2576
+ name=edited.name,
2577
+ changed=changed,
2578
+ used_by=edited.used_by,
2579
+ )
2505
2580
 
2506
2581
 
2507
2582
  @schema_app.command("show")
@@ -2511,13 +2586,20 @@ def schema_show(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> N
2511
2586
  row = dg.call(dg.schemas.get(code))
2512
2587
  if state_of(ctx).json_output:
2513
2588
  return emit_one("schema", row)
2514
- fields(f"schema {code}", {"name": row.name or "-", "description": row.description or "-"})
2589
+ fields(
2590
+ f"schema {code}",
2591
+ {
2592
+ "name": row.name or "-",
2593
+ "description": row.description or "-",
2594
+ "used by": ", ".join(row.used_by) or "-",
2595
+ },
2596
+ )
2515
2597
  console.print_json(data=row.body)
2516
2598
 
2517
2599
 
2518
2600
  @schema_app.command("delete")
2519
2601
  def schema_delete(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> None:
2520
- """Remove a schema."""
2602
+ """Remove a schema, which the instance refuses while a stored pipeline names it."""
2521
2603
  with client_for(state_of(ctx)) as dg:
2522
2604
  dg.call(dg.schemas.delete(code))
2523
2605
  emit_fact("schema.deleted", message="deleted", code=code)
@@ -189,6 +189,11 @@ SCHEMA_NOT_AN_OBJECT = CLI.define(
189
189
  "{label} is not a JSON Schema: a schema is an object, and this is {kind}",
190
190
  )
191
191
 
192
+ SCHEMA_NOTHING_TO_CHANGE = CLI.define(
193
+ "schema_nothing_to_change",
194
+ "nothing was given to change about {code}; pass a schema file, --name or --description",
195
+ )
196
+
192
197
  # What a profile refuses at validation. Pydantic owns the code a validator's refusal reaches
193
198
  # the wire under, so this is rendered into the ``ValueError`` it wraps.
194
199
 
@@ -478,6 +478,11 @@ def prioritised(priority: object) -> str:
478
478
  #: same words out of its own catalogue, and `tests/test_shared_words.py` holds the two together.
479
479
  IMPORTANCE_FLOOR = "{importance} and above"
480
480
 
481
+ #: What a pipeline's unsettled runs are called, wherever a rendering names the count the server
482
+ #: computes from `ACTIVE_RUN_STATUSES`. The web UI heads its own feed of them with the same noun
483
+ #: out of its catalogue, and `tests/test_shared_words.py` fails when the two stop agreeing.
484
+ RUNS_IN_FLIGHT = "runs in flight"
485
+
481
486
 
482
487
  def watching(scope: object, importance: object) -> str:
483
488
  """What an alert rule watches, in one cell: its scope, and the floor it fires at.
@@ -11,7 +11,7 @@ from pathlib import Path
11
11
  from typing import Any, cast
12
12
 
13
13
  from dirigent_cli.local import ConnectionSpec
14
- from dirigent_client import Dirigent, DirigentError, PlanAction, ProvenanceSource
14
+ from dirigent_client import Dirigent, DirigentError, PlanAction, ProvenanceSource, declared_label
15
15
  from dirigent_common import JsonMap
16
16
  from dirigent_core.documents import CARRIED, SUFFIXES, is_document, readable, safe_load
17
17
  from dirigent_core.protocol import Record, make
@@ -99,11 +99,21 @@ def _schemas(raw: JsonMap) -> dict[str, JsonMap]:
99
99
 
100
100
 
101
101
  async def _schema(client: Dirigent, code: str, body: JsonMap) -> None:
102
- """Store one named schema, replacing the body of one this instance already holds."""
102
+ """Store one named schema, replacing the shape and the labels of one already held.
103
+
104
+ Storing reads the schema's own ``title`` and ``description`` and editing reads neither, so
105
+ the replacement sends what the document's schema declares about itself; otherwise the same
106
+ corpus would label a schema one way on a fresh instance and another way on a seeded one.
107
+ """
103
108
  try:
104
109
  await client.schemas.create(body, code=code)
105
110
  except DirigentError:
106
- await client.schemas.update(code, body=body)
111
+ await client.schemas.update(
112
+ code,
113
+ body=body,
114
+ name=declared_label(body, "title"),
115
+ description=declared_label(body, "description"),
116
+ )
107
117
 
108
118
 
109
119
  async def _connections(client: Dirigent, declared: Sequence[ConnectionSpec], origin: str) -> AsyncIterator[Record]:
@@ -52,6 +52,7 @@ BULKY: Final = frozenset(
52
52
  "token",
53
53
  "preflight",
54
54
  "requires",
55
+ "used_by",
55
56
  }
56
57
  )
57
58
 
@@ -342,6 +343,33 @@ def _created(record: Record) -> RenderableType | None:
342
343
  return Group(*parts)
343
344
 
344
345
 
346
+ #: What a changed shape reaches. The web UI warns in these words before a save, out of its own
347
+ #: catalogue, and `tests/test_shared_words.py` fails when the two stop agreeing. Written without
348
+ #: terminal punctuation, because the rendering puts the pipelines after it.
349
+ SCHEMA_REACHES = "Every pipeline naming this code checks against this shape from its next run"
350
+
351
+ #: What it does not reach. Said after the pipelines rather than with them: the colon above governs
352
+ #: that list, and a run in flight is not on it. Paired with the web UI's own words by the
353
+ #: same test, and punctuated, because nothing follows it.
354
+ IN_FLIGHT_KEEPS_ITS_SHAPE = "A run in flight keeps the shape it started with."
355
+
356
+
357
+ def _schema_edited(record: Record) -> RenderableType | None:
358
+ """Render whose pipelines the shape that was just changed was changed for.
359
+
360
+ An edit is never refused for being depended on, so the pipelines checking against the
361
+ code are the ones the edit was a decision about, and they are named rather than counted.
362
+ Where no pipeline names the code, no run checks against it either, so neither line is drawn.
363
+ """
364
+ named = _texts(record, "used_by")
365
+ if not named:
366
+ return None
367
+ parts: list[RenderableType] = [f"\n{SCHEMA_REACHES}:"]
368
+ parts.extend(f" [yellow]-[/] {escape(one)}" for one in named)
369
+ parts.append(f"\n{IN_FLIGHT_KEEPS_ITS_SHAPE}")
370
+ return Group(*parts)
371
+
372
+
345
373
  def _config(record: Record) -> RenderableType | None:
346
374
  """Render the effective configuration as a setting-per-row table."""
347
375
  settings = record.get("settings")
@@ -524,4 +552,5 @@ RENDERERS: Final[Mapping[str, Callable[[Record], RenderableType | None]]] = {
524
552
  "project.scaffolded": _scaffolded,
525
553
  "instance.initialised": _initialised,
526
554
  "pipeline.created": _created,
555
+ "schema.updated": _schema_edited,
527
556
  }
@@ -28,7 +28,7 @@ from dirigent_cli.output import (
28
28
  throttle_note,
29
29
  watching,
30
30
  )
31
- from dirigent_client import LOG_NOTIFIER, AlertEvent, AlertScope, WebhookTokenOut
31
+ from dirigent_client import LOG_NOTIFIER, TEST_SUBJECT, AlertEvent, AlertScope, WebhookTokenOut
32
32
  from dirigent_common import Message
33
33
 
34
34
  schedule_app = typer.Typer(
@@ -609,7 +609,7 @@ def alerts_test(
609
609
  str | None,
610
610
  typer.Option("--connection", help="The connection to deliver through; none delivers to the process log."),
611
611
  ] = None,
612
- subject: Annotated[str, typer.Option("--subject", help="What the test message says.")] = "dirigent test alert",
612
+ subject: Annotated[str, typer.Option("--subject", help="What the test message says.")] = TEST_SUBJECT,
613
613
  ) -> None:
614
614
  """Send a test message to one target, on the same queue a real alert takes."""
615
615
  with client_for(state_of(ctx)) as dg:
File without changes
File without changes