dirigent-cli 0.21.0__tar.gz → 0.23.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.
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/PKG-INFO +9 -9
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/pyproject.toml +9 -9
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/pyproject.toml.orig +9 -9
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/commands.py +99 -15
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/context.py +8 -1
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/explain.py +31 -11
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/main.py +8 -3
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/messages.py +15 -1
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/output.py +44 -9
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/profiles.py +27 -10
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/schemas.py +4 -1
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/seeding.py +13 -3
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/sources.py +4 -1
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/summaries.py +29 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/triggers.py +2 -2
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/LICENSE +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/README.md +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/__init__.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/aliases.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/formatters.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/graph.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/health.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/init_form.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/local.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/params.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/project.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/py.typed +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/reaper.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/scaffold.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/starters.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/stream.py +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
- {dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/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.
|
|
3
|
+
Version: 0.23.0
|
|
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.
|
|
10
|
-
Requires-Dist: dirigent-blocks==0.
|
|
11
|
-
Requires-Dist: dirigent-client==0.
|
|
12
|
-
Requires-Dist: dirigent-common==0.
|
|
13
|
-
Requires-Dist: dirigent-core==0.
|
|
14
|
-
Requires-Dist: dirigent-examples==0.
|
|
15
|
-
Requires-Dist: dirigent-plugin==0.
|
|
16
|
-
Requires-Dist: dirigent-server==0.
|
|
9
|
+
Requires-Dist: dirigent-block-execute==0.23.0
|
|
10
|
+
Requires-Dist: dirigent-blocks==0.23.0
|
|
11
|
+
Requires-Dist: dirigent-client==0.23.0
|
|
12
|
+
Requires-Dist: dirigent-common==0.23.0
|
|
13
|
+
Requires-Dist: dirigent-core==0.23.0
|
|
14
|
+
Requires-Dist: dirigent-examples==0.23.0
|
|
15
|
+
Requires-Dist: dirigent-plugin==0.23.0
|
|
16
|
+
Requires-Dist: dirigent-server==0.23.0
|
|
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.
|
|
3
|
+
version = "0.23.0"
|
|
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.
|
|
15
|
-
"dirigent-blocks==0.
|
|
16
|
-
"dirigent-client==0.
|
|
17
|
-
"dirigent-common==0.
|
|
18
|
-
"dirigent-core==0.
|
|
19
|
-
"dirigent-examples==0.
|
|
20
|
-
"dirigent-plugin==0.
|
|
21
|
-
"dirigent-server==0.
|
|
14
|
+
"dirigent-block-execute==0.23.0",
|
|
15
|
+
"dirigent-blocks==0.23.0",
|
|
16
|
+
"dirigent-client==0.23.0",
|
|
17
|
+
"dirigent-common==0.23.0",
|
|
18
|
+
"dirigent-core==0.23.0",
|
|
19
|
+
"dirigent-examples==0.23.0",
|
|
20
|
+
"dirigent-plugin==0.23.0",
|
|
21
|
+
"dirigent-server==0.23.0",
|
|
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.
|
|
3
|
+
version = "0.23.0"
|
|
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.
|
|
15
|
-
"dirigent-blocks==0.
|
|
16
|
-
"dirigent-client==0.
|
|
17
|
-
"dirigent-common==0.
|
|
18
|
-
"dirigent-core==0.
|
|
19
|
-
"dirigent-examples==0.
|
|
20
|
-
"dirigent-plugin==0.
|
|
21
|
-
"dirigent-server==0.
|
|
14
|
+
"dirigent-block-execute==0.23.0",
|
|
15
|
+
"dirigent-blocks==0.23.0",
|
|
16
|
+
"dirigent-client==0.23.0",
|
|
17
|
+
"dirigent-common==0.23.0",
|
|
18
|
+
"dirigent-core==0.23.0",
|
|
19
|
+
"dirigent-examples==0.23.0",
|
|
20
|
+
"dirigent-plugin==0.23.0",
|
|
21
|
+
"dirigent-server==0.23.0",
|
|
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,
|
|
@@ -86,8 +87,11 @@ from dirigent_cli.messages import (
|
|
|
86
87
|
)
|
|
87
88
|
from dirigent_cli.messages import WINDOW_GRAMMAR as WINDOW_GRAMMAR_MESSAGE
|
|
88
89
|
from dirigent_cli.output import (
|
|
90
|
+
CONNECTION_HEALTH,
|
|
91
|
+
RUNS_IN_FLIGHT,
|
|
89
92
|
Detail,
|
|
90
93
|
age,
|
|
94
|
+
connection_health,
|
|
91
95
|
console,
|
|
92
96
|
elapsed,
|
|
93
97
|
emit_event,
|
|
@@ -133,6 +137,7 @@ from dirigent_cli.sources import (
|
|
|
133
137
|
from dirigent_cli.stream import Sink, track_steps, use_scratch_prefix
|
|
134
138
|
from dirigent_cli.timing import RunProfile, attempt_timing, by_step, profile, step_timing
|
|
135
139
|
from dirigent_client import (
|
|
140
|
+
CLEAR,
|
|
136
141
|
ApplyResult,
|
|
137
142
|
AttemptEvent,
|
|
138
143
|
AttemptOut,
|
|
@@ -140,6 +145,7 @@ from dirigent_client import (
|
|
|
140
145
|
BackfillAccepted,
|
|
141
146
|
BlockKind,
|
|
142
147
|
Catalog,
|
|
148
|
+
Clear,
|
|
143
149
|
DocumentKind,
|
|
144
150
|
ExampleDetail,
|
|
145
151
|
ExampleOut,
|
|
@@ -157,6 +163,7 @@ from dirigent_client import (
|
|
|
157
163
|
RunReport,
|
|
158
164
|
RunStatus,
|
|
159
165
|
ValidationIssue,
|
|
166
|
+
declared_label,
|
|
160
167
|
)
|
|
161
168
|
from dirigent_common import Issue, JsonMap, Message
|
|
162
169
|
from dirigent_core import migrations
|
|
@@ -821,7 +828,7 @@ def pipeline_list(
|
|
|
821
828
|
return emit_records("pipeline", rows)
|
|
822
829
|
table(
|
|
823
830
|
"pipelines",
|
|
824
|
-
["code", "name", "tags", "version", "active",
|
|
831
|
+
["code", "name", "tags", "version", "active", RUNS_IN_FLIGHT, "updated"],
|
|
825
832
|
[
|
|
826
833
|
[
|
|
827
834
|
row.code,
|
|
@@ -857,7 +864,7 @@ def pipeline_show(
|
|
|
857
864
|
"importance": row.importance.value,
|
|
858
865
|
"active": "yes" if row.active else "no",
|
|
859
866
|
"current version": row.current_version,
|
|
860
|
-
|
|
867
|
+
RUNS_IN_FLIGHT: row.active_runs,
|
|
861
868
|
"created": moment(row.created_at),
|
|
862
869
|
},
|
|
863
870
|
)
|
|
@@ -2435,7 +2442,7 @@ def connection_check(ctx: typer.Context, code: Annotated[str, typer.Argument()])
|
|
|
2435
2442
|
report = dg.call(dg.connections.check(code))
|
|
2436
2443
|
emit_fact(
|
|
2437
2444
|
"connection.checked",
|
|
2438
|
-
message=
|
|
2445
|
+
message=CONNECTION_HEALTH[connection_health(report.healthy)],
|
|
2439
2446
|
code=code,
|
|
2440
2447
|
healthy=report.healthy,
|
|
2441
2448
|
detail=report.detail,
|
|
@@ -2463,8 +2470,11 @@ def schema_list(ctx: typer.Context) -> None:
|
|
|
2463
2470
|
return emit_records("schema", rows)
|
|
2464
2471
|
table(
|
|
2465
2472
|
"schemas",
|
|
2466
|
-
["code", "name", "description"],
|
|
2467
|
-
[
|
|
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
|
+
],
|
|
2468
2478
|
)
|
|
2469
2479
|
|
|
2470
2480
|
|
|
@@ -2481,6 +2491,19 @@ def schema_create(
|
|
|
2481
2491
|
] = None,
|
|
2482
2492
|
) -> None:
|
|
2483
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."""
|
|
2484
2507
|
try:
|
|
2485
2508
|
document = read_document(reference)
|
|
2486
2509
|
except SourceError as error:
|
|
@@ -2491,15 +2514,69 @@ def schema_create(
|
|
|
2491
2514
|
fail(SCHEMA_UNREADABLE, label=document.label, detail=str(error))
|
|
2492
2515
|
if not isinstance(body, dict):
|
|
2493
2516
|
fail(SCHEMA_NOT_AN_OBJECT, label=document.label, kind=type(body).__name__)
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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)
|
|
2498
2570
|
with client_for(state_of(ctx)) as dg:
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
+
)
|
|
2503
2580
|
|
|
2504
2581
|
|
|
2505
2582
|
@schema_app.command("show")
|
|
@@ -2509,13 +2586,20 @@ def schema_show(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> N
|
|
|
2509
2586
|
row = dg.call(dg.schemas.get(code))
|
|
2510
2587
|
if state_of(ctx).json_output:
|
|
2511
2588
|
return emit_one("schema", row)
|
|
2512
|
-
fields(
|
|
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
|
+
)
|
|
2513
2597
|
console.print_json(data=row.body)
|
|
2514
2598
|
|
|
2515
2599
|
|
|
2516
2600
|
@schema_app.command("delete")
|
|
2517
2601
|
def schema_delete(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> None:
|
|
2518
|
-
"""Remove a schema."""
|
|
2602
|
+
"""Remove a schema, which the instance refuses while a stored pipeline names it."""
|
|
2519
2603
|
with client_for(state_of(ctx)) as dg:
|
|
2520
2604
|
dg.call(dg.schemas.delete(code))
|
|
2521
2605
|
emit_fact("schema.deleted", message="deleted", code=code)
|
|
@@ -143,4 +143,11 @@ class Session(BlockingDirigent):
|
|
|
143
143
|
def client_for(state: CliState, *, needs_token: bool = True) -> Session:
|
|
144
144
|
"""Build the API client for a resolved endpoint."""
|
|
145
145
|
endpoint = state.endpoint(needs_token=needs_token)
|
|
146
|
-
return Session(
|
|
146
|
+
return Session(
|
|
147
|
+
url=endpoint.url,
|
|
148
|
+
token=endpoint.token,
|
|
149
|
+
api_prefix=endpoint.api_prefix,
|
|
150
|
+
timeout=endpoint.timeout.total_seconds(),
|
|
151
|
+
connect_timeout=endpoint.connect_timeout.total_seconds(),
|
|
152
|
+
retries=endpoint.retries,
|
|
153
|
+
)
|
|
@@ -7,9 +7,10 @@ under a deadline. What only the run can decide -- a parameter supplied at the co
|
|
|
7
7
|
a window's bounds -- is reported as unknown rather than guessed at, and a total that counted
|
|
8
8
|
one of those says so.
|
|
9
9
|
|
|
10
|
-
The cardinality rules are
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
The cardinality rules are the engine's: a literal list is its own length,
|
|
11
|
+
``${steps.<name>.items}`` adopts another fan-out's grid, a reference that resolves to a list is
|
|
12
|
+
that list, one that reads a step's output is as wide as that step makes it while the run
|
|
13
|
+
executes, and anything else is only the run's to answer.
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from collections.abc import Mapping, Sequence
|
|
@@ -31,6 +32,9 @@ UNKNOWN: Final = "unknown"
|
|
|
31
32
|
#: How a cardinality names the fan-out whose grid a step maps over instead of its own.
|
|
32
33
|
ADOPTS: Final = "adopts {step}"
|
|
33
34
|
|
|
35
|
+
#: How a cardinality names the step whose output a late grid is expanded from.
|
|
36
|
+
LATE: Final = "from {step}"
|
|
37
|
+
|
|
34
38
|
|
|
35
39
|
class DocumentShape(BaseModel):
|
|
36
40
|
"""What a whole document will cost: the steps, the totals over them, and the warnings."""
|
|
@@ -61,7 +65,7 @@ def explain(definition: PipelineDefinition, catalog: Catalog | None = None) -> D
|
|
|
61
65
|
order = definition.topological_order()
|
|
62
66
|
shapes: dict[str, StepShape] = {}
|
|
63
67
|
for name in order:
|
|
64
|
-
shapes[name] = _step_shape(name, definition.steps[name], params, shapes, catalog)
|
|
68
|
+
shapes[name] = _step_shape(name, definition.steps[name], params, shapes, catalog, definition.grid_source(name))
|
|
65
69
|
rows = [shapes[name] for name in order]
|
|
66
70
|
return DocumentShape(
|
|
67
71
|
attempts_max=sum(row.max_attempts * (1 if row.elements is None else row.elements) for row in rows),
|
|
@@ -90,9 +94,13 @@ def _step_shape(
|
|
|
90
94
|
params: JsonMap,
|
|
91
95
|
done: Mapping[str, StepShape],
|
|
92
96
|
catalog: Catalog | None,
|
|
97
|
+
source: str | None,
|
|
93
98
|
) -> StepShape:
|
|
94
|
-
"""Read one step as the work it will become, against the steps already read.
|
|
95
|
-
|
|
99
|
+
"""Read one step as the work it will become, against the steps already read.
|
|
100
|
+
|
|
101
|
+
``source`` is the step a late grid waits for, which the row names.
|
|
102
|
+
"""
|
|
103
|
+
cardinality, elements = _cardinality(step, params, done, source)
|
|
96
104
|
poll, deadline, from_block = _waits(step, _entry(step, catalog))
|
|
97
105
|
return StepShape(
|
|
98
106
|
name=name,
|
|
@@ -100,6 +108,7 @@ def _step_shape(
|
|
|
100
108
|
depends_on=list(step.depends_on),
|
|
101
109
|
cardinality=cardinality,
|
|
102
110
|
elements=elements,
|
|
111
|
+
grid_source=source,
|
|
103
112
|
reference=step.for_each if isinstance(step.for_each, str) else None,
|
|
104
113
|
items=step.items.value,
|
|
105
114
|
max_attempts=step.retry.max_attempts,
|
|
@@ -112,7 +121,9 @@ def _step_shape(
|
|
|
112
121
|
)
|
|
113
122
|
|
|
114
123
|
|
|
115
|
-
def _cardinality(
|
|
124
|
+
def _cardinality(
|
|
125
|
+
step: StepDefinition, params: JsonMap, done: Mapping[str, StepShape], source: str | None
|
|
126
|
+
) -> tuple[int | str, int | None]:
|
|
116
127
|
"""Say how many run items a step becomes, and the count behind that where there is one."""
|
|
117
128
|
expression = step.for_each
|
|
118
129
|
if expression is None:
|
|
@@ -123,10 +134,8 @@ def _cardinality(step: StepDefinition, params: JsonMap, done: Mapping[str, StepS
|
|
|
123
134
|
if adopted is not None:
|
|
124
135
|
upstream = done[adopted].elements if adopted in done else None
|
|
125
136
|
return ADOPTS.format(step=adopted), upstream
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
# run is created and no step has run by then.
|
|
129
|
-
return UNKNOWN, None
|
|
137
|
+
if source is not None:
|
|
138
|
+
return LATE.format(step=source), None
|
|
130
139
|
try:
|
|
131
140
|
resolved = resolve(expression, ReferenceScope(params=params))
|
|
132
141
|
except UnknownReference:
|
|
@@ -202,6 +211,17 @@ def _warnings(rows: Sequence[StepShape], catalog: Catalog | None) -> list[ShapeW
|
|
|
202
211
|
),
|
|
203
212
|
)
|
|
204
213
|
)
|
|
214
|
+
if row.grid_source is not None and row.cardinality == LATE.format(step=row.grid_source):
|
|
215
|
+
found.append(
|
|
216
|
+
ShapeWarning(
|
|
217
|
+
step=row.name,
|
|
218
|
+
cause="late-cardinality",
|
|
219
|
+
message=(
|
|
220
|
+
f"{row.name} fans out over {row.reference}, which {row.grid_source} produces while "
|
|
221
|
+
f"the run executes, so it is counted as one item here."
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
205
225
|
if row.retry_wait is not None and row.deadline is not None and row.retry_wait > row.deadline:
|
|
206
226
|
found.append(
|
|
207
227
|
ShapeWarning(
|
|
@@ -50,7 +50,7 @@ from dirigent_cli.messages import (
|
|
|
50
50
|
)
|
|
51
51
|
from dirigent_cli.output import configure, detail_mode, emit_fact, emit_problem, emit_rendered, refuse
|
|
52
52
|
from dirigent_client.enums import UserRole
|
|
53
|
-
from dirigent_common import Issue
|
|
53
|
+
from dirigent_common import Issue, raised_detail, validation_issues
|
|
54
54
|
from dirigent_core import migrations
|
|
55
55
|
from dirigent_core.config import STATE_DIR, Settings, get_settings, redacted_url, reset_settings_cache
|
|
56
56
|
from dirigent_core.logging import configure_logging, silence_stdout
|
|
@@ -402,7 +402,7 @@ async def _ensure_container(settings: Settings) -> "ContainerResult":
|
|
|
402
402
|
except DomainError as error:
|
|
403
403
|
commands.fail(STORE_UNCONFIGURED, root=settings.artifact_root, detail=str(error))
|
|
404
404
|
except Exception as error:
|
|
405
|
-
commands.fail(STORE_UNREACHABLE, root=settings.artifact_root, detail=
|
|
405
|
+
commands.fail(STORE_UNREACHABLE, root=settings.artifact_root, detail=raised_detail(error))
|
|
406
406
|
finally:
|
|
407
407
|
await engine.dispose()
|
|
408
408
|
|
|
@@ -443,7 +443,12 @@ def connection_ensure(
|
|
|
443
443
|
except ValidationError as error:
|
|
444
444
|
# include_input=False: the input here is a credential, and pydantic's default error
|
|
445
445
|
# payload echoes the value that failed.
|
|
446
|
-
commands.fail(
|
|
446
|
+
commands.fail(
|
|
447
|
+
CONNECTION_UNUSABLE,
|
|
448
|
+
code=code,
|
|
449
|
+
kind=kind_id,
|
|
450
|
+
problems=validation_issues(error.errors(include_input=False)),
|
|
451
|
+
)
|
|
447
452
|
key = settings.secret_key.get_secret_value() if settings.secret_key else None
|
|
448
453
|
try:
|
|
449
454
|
public, envelope, key_id = SecretBox(key).encrypt_config(model, validated)
|
|
@@ -25,7 +25,7 @@ RUN_DB_UPGRADE = CLI.define("run_db_upgrade", "run dg db upgrade")
|
|
|
25
25
|
|
|
26
26
|
UNKNOWN_CONNECTION_KIND = CLI.define("unknown_connection_kind", "no connection kind {kind} is installed ({known})")
|
|
27
27
|
|
|
28
|
-
CONNECTION_UNUSABLE = CLI.define("connection_unusable", "{code} is not a usable {kind} connection
|
|
28
|
+
CONNECTION_UNUSABLE = CLI.define("connection_unusable", "{code} is not a usable {kind} connection")
|
|
29
29
|
|
|
30
30
|
INVALID_AGE = CLI.define("invalid_age", "{detail}")
|
|
31
31
|
|
|
@@ -189,6 +189,20 @@ 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
|
+
|
|
197
|
+
# What a profile refuses at validation. Pydantic owns the code a validator's refusal reaches
|
|
198
|
+
# the wire under, so this is rendered into the ``ValueError`` it wraps.
|
|
199
|
+
|
|
200
|
+
PROFILE_NAMES_A_DATABASE = CLI.define(
|
|
201
|
+
"profile_names_a_database",
|
|
202
|
+
"profile {name} names a database URL. Profiles address a server over HTTP; "
|
|
203
|
+
"a database URL belongs in DIRIGENT_DATABASE_URL on the host that runs it",
|
|
204
|
+
)
|
|
205
|
+
|
|
192
206
|
NOT_AUTHENTICATED = CLI.define("not_authenticated", "no token for {url}")
|
|
193
207
|
|
|
194
208
|
SET_DG_TOKEN = CLI.define("set_dg_token", "set DG_TOKEN")
|
|
@@ -474,16 +474,29 @@ def prioritised(priority: object) -> str:
|
|
|
474
474
|
return ""
|
|
475
475
|
|
|
476
476
|
|
|
477
|
+
#: The floor an alert rule fires at, said beside its scope. The web UI writes this half in the
|
|
478
|
+
#: same words out of its own catalogue, and `tests/test_shared_words.py` holds the two together.
|
|
479
|
+
IMPORTANCE_FLOOR = "{importance} and above"
|
|
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
|
+
|
|
486
|
+
|
|
477
487
|
def watching(scope: object, importance: object) -> str:
|
|
478
488
|
"""What an alert rule watches, in one cell: its scope, and the floor it fires at.
|
|
479
489
|
|
|
480
490
|
Almost every rule names no importance, so a column of its own would mostly be empty.
|
|
481
491
|
"""
|
|
482
|
-
|
|
492
|
+
if not importance:
|
|
493
|
+
return str(scope)
|
|
494
|
+
return f"{scope}, {IMPORTANCE_FLOOR.format(importance=importance)}"
|
|
483
495
|
|
|
484
496
|
|
|
485
497
|
#: What each alert event is called in a rendering. The wire's word is what a rule is declared
|
|
486
|
-
#: with; these are the words a reader is shown
|
|
498
|
+
#: with; these are the words a reader is shown. The web UI draws the same four out of its own
|
|
499
|
+
#: catalogue, and `tests/test_shared_words.py` fails when the two tables stop agreeing.
|
|
487
500
|
ALERT_EVENTS: Mapping[str, str] = {
|
|
488
501
|
"run_failed": "Failed",
|
|
489
502
|
"run_completed_with_errors": "Completed with errors",
|
|
@@ -529,17 +542,39 @@ def render_bool(value: object) -> str:
|
|
|
529
542
|
return "[green]yes[/]" if value else "[dim]no[/]"
|
|
530
543
|
|
|
531
544
|
|
|
545
|
+
#: What a connection's last check is called, keyed by the state the web UI keys it by. A
|
|
546
|
+
#: credential's health is one vocabulary across the product, so these are the bundle's own four
|
|
547
|
+
#: words and `tests/test_shared_words.py` fails when the two tables stop agreeing.
|
|
548
|
+
CONNECTION_HEALTH: Mapping[str, str] = {
|
|
549
|
+
"unchecked": "never checked",
|
|
550
|
+
"unverified": "not verified",
|
|
551
|
+
"healthy": "healthy",
|
|
552
|
+
"failed": "failed",
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def connection_health(healthy: object) -> str:
|
|
557
|
+
"""Say which state a check that ran left a connection in.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
healthy: What the check decided, or None where the kind publishes no check.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
The key into `CONNECTION_HEALTH`.
|
|
564
|
+
"""
|
|
565
|
+
if healthy is None:
|
|
566
|
+
return "unverified"
|
|
567
|
+
return "healthy" if healthy else "failed"
|
|
568
|
+
|
|
569
|
+
|
|
532
570
|
def render_check(last_check_at: object, healthy: object) -> str:
|
|
533
571
|
"""Render what a connection's last check said, across the four states a row can be in.
|
|
534
572
|
|
|
535
|
-
A check answers
|
|
536
|
-
none of the three.
|
|
573
|
+
A check answers that the system is there, that it is not, or that the kind publishes no
|
|
574
|
+
check at all; a row nothing has ever asked is in none of the three.
|
|
537
575
|
"""
|
|
538
|
-
if
|
|
539
|
-
|
|
540
|
-
if healthy is None:
|
|
541
|
-
return "[dim]not verified[/]"
|
|
542
|
-
return "[green]yes[/]" if healthy else "[dim]no[/]"
|
|
576
|
+
state = connection_health(healthy) if last_check_at else "unchecked"
|
|
577
|
+
return f"[{STATUS_STYLES.get(state, 'dim')}]{CONNECTION_HEALTH[state]}[/]"
|
|
543
578
|
|
|
544
579
|
|
|
545
580
|
def moment(value: object) -> str:
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
"""Profiles: which server the CLI talks to,
|
|
1
|
+
"""Profiles: which server the CLI talks to, how it gets a token for it, and how patient it is.
|
|
2
2
|
|
|
3
|
-
A profile is client-side
|
|
4
|
-
|
|
3
|
+
A profile is client-side only and must never hold a database URL: a CLI that could reach the
|
|
4
|
+
database would bypass authentication, attribution, and validation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
import shutil
|
|
9
9
|
import subprocess
|
|
10
10
|
from collections.abc import Mapping
|
|
11
|
+
from datetime import timedelta
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, Final, cast
|
|
13
14
|
|
|
@@ -15,8 +16,9 @@ import yaml
|
|
|
15
16
|
from dotenv import dotenv_values
|
|
16
17
|
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
17
18
|
|
|
18
|
-
from
|
|
19
|
-
from
|
|
19
|
+
from dirigent_cli.messages import PROFILE_NAMES_A_DATABASE
|
|
20
|
+
from dirigent_client import API_PREFIX, DEFAULT_CONNECT_TIMEOUT, DEFAULT_RETRIES, DEFAULT_TIMEOUT
|
|
21
|
+
from dirigent_common import Duration, EntityName
|
|
20
22
|
|
|
21
23
|
PROJECT_PROFILES: Final = Path(".dirigent") / "profiles.yaml"
|
|
22
24
|
PROJECT_ENV_FILE: Final = ".env"
|
|
@@ -32,13 +34,16 @@ DATABASE_SCHEMES: Final = ("postgresql", "postgres", "sqlite", "mysql")
|
|
|
32
34
|
|
|
33
35
|
TOKEN_COMMAND_TIMEOUT: Final = 30.0
|
|
34
36
|
|
|
37
|
+
DEFAULT_REQUEST_TIMEOUT: Final = timedelta(seconds=DEFAULT_TIMEOUT)
|
|
38
|
+
DEFAULT_CONNECT_BUDGET: Final = timedelta(seconds=DEFAULT_CONNECT_TIMEOUT)
|
|
39
|
+
|
|
35
40
|
|
|
36
41
|
class ProfileError(Exception):
|
|
37
42
|
"""A profile could not be read, found, or turned into a usable token."""
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
class Profile(BaseModel):
|
|
41
|
-
"""One named server
|
|
46
|
+
"""One named server, how to obtain a token for it, and how long to wait on it."""
|
|
42
47
|
|
|
43
48
|
model_config = ConfigDict(frozen=True)
|
|
44
49
|
|
|
@@ -53,15 +58,21 @@ class Profile(BaseModel):
|
|
|
53
58
|
api_prefix: str = API_PREFIX
|
|
54
59
|
"""Where this instance serves its API, for one configured with a different prefix."""
|
|
55
60
|
|
|
61
|
+
timeout: Duration = DEFAULT_REQUEST_TIMEOUT
|
|
62
|
+
"""How long a request to this instance may take to answer."""
|
|
63
|
+
|
|
64
|
+
connect_timeout: Duration = DEFAULT_CONNECT_BUDGET
|
|
65
|
+
"""How long reaching this instance may take before it is called unreachable."""
|
|
66
|
+
|
|
67
|
+
retries: int = Field(default=DEFAULT_RETRIES, ge=0)
|
|
68
|
+
"""How many further attempts a lost or refused-with-a-5xx request gets."""
|
|
69
|
+
|
|
56
70
|
@model_validator(mode="after")
|
|
57
71
|
def _refuse_a_database_url(self) -> "Profile":
|
|
58
72
|
"""Refuse a profile pointing at a database rather than at an API."""
|
|
59
73
|
scheme = self.url.split("://", 1)[0].split("+", 1)[0].lower()
|
|
60
74
|
if scheme in DATABASE_SCHEMES:
|
|
61
|
-
raise ValueError(
|
|
62
|
-
f"profile {self.name!r} names a database URL. Profiles address a server over HTTP; "
|
|
63
|
-
f"a database URL belongs in DIRIGENT_DATABASE_URL on the host that runs it"
|
|
64
|
-
)
|
|
75
|
+
raise ValueError(PROFILE_NAMES_A_DATABASE.render(name=repr(self.name)))
|
|
65
76
|
return self
|
|
66
77
|
|
|
67
78
|
def resolve_token(self, environ: Mapping[str, str] | None = None) -> str | None:
|
|
@@ -193,6 +204,9 @@ class Endpoint(BaseModel):
|
|
|
193
204
|
profile: str | None = None
|
|
194
205
|
source: str = "default"
|
|
195
206
|
api_prefix: str = API_PREFIX
|
|
207
|
+
timeout: Duration = DEFAULT_REQUEST_TIMEOUT
|
|
208
|
+
connect_timeout: Duration = DEFAULT_CONNECT_BUDGET
|
|
209
|
+
retries: int = DEFAULT_RETRIES
|
|
196
210
|
|
|
197
211
|
|
|
198
212
|
def resolve_endpoint(
|
|
@@ -236,4 +250,7 @@ def resolve_endpoint(
|
|
|
236
250
|
profile=chosen.name if chosen else None,
|
|
237
251
|
source=source,
|
|
238
252
|
api_prefix=chosen.api_prefix if chosen else API_PREFIX,
|
|
253
|
+
timeout=chosen.timeout if chosen else DEFAULT_REQUEST_TIMEOUT,
|
|
254
|
+
connect_timeout=chosen.connect_timeout if chosen else DEFAULT_CONNECT_BUDGET,
|
|
255
|
+
retries=chosen.retries if chosen else DEFAULT_RETRIES,
|
|
239
256
|
)
|
|
@@ -65,11 +65,14 @@ class StepShape(BaseModel):
|
|
|
65
65
|
depends_on: list[str] = Field(default_factory=list[str])
|
|
66
66
|
|
|
67
67
|
cardinality: int | str = 1
|
|
68
|
-
"""How many run items this step becomes: a count, ``adopts <step>``, or ``unknown``."""
|
|
68
|
+
"""How many run items this step becomes: a count, ``adopts <step>``, ``from <step>``, or ``unknown``."""
|
|
69
69
|
|
|
70
70
|
elements: int | None = 1
|
|
71
71
|
"""The count behind the cardinality, which an adoption takes from the grid it adopts."""
|
|
72
72
|
|
|
73
|
+
grid_source: str | None = None
|
|
74
|
+
"""The step a late grid waits for before it expands, or null for a grid fixed at creation."""
|
|
75
|
+
|
|
73
76
|
reference: str | None = None
|
|
74
77
|
"""The ``for_each`` this step maps over, where it names one rather than listing it."""
|
|
75
78
|
|
|
@@ -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
|
|
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(
|
|
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]:
|
|
@@ -8,10 +8,13 @@ import httpx2
|
|
|
8
8
|
import yaml
|
|
9
9
|
from pydantic import BaseModel, ConfigDict
|
|
10
10
|
|
|
11
|
+
from dirigent_client import DEFAULT_CONNECT_TIMEOUT
|
|
11
12
|
from dirigent_client.enums import ProvenanceSource
|
|
12
13
|
|
|
13
14
|
STDIN: Final = "-"
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
#: The body may take thirty seconds to arrive, and reaching the host that serves it two.
|
|
17
|
+
FETCH_TIMEOUT: Final = httpx2.Timeout(30.0, connect=DEFAULT_CONNECT_TIMEOUT)
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class SourceError(Exception):
|
|
@@ -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.")] =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/operator.py.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.21.0 → dirigent_cli-0.23.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl
RENAMED
|
File without changes
|
|
File without changes
|