dirigent-cli 0.23.4__tar.gz → 0.25.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.23.4 → dirigent_cli-0.25.0}/PKG-INFO +9 -9
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/pyproject.toml +9 -9
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/pyproject.toml.orig +9 -9
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/commands.py +162 -17
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/explain.py +16 -4
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/graph.py +11 -1
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/init_form.py +2 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/local.py +52 -6
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/main.py +11 -5
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/messages.py +19 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/project.py +86 -1
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/schemas.py +4 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/seeding.py +117 -62
- dirigent_cli-0.25.0/src/dirigent_cli/source_commands.py +203 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/summaries.py +19 -1
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/LICENSE +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/README.md +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/__init__.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/aliases.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/context.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/formatters.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/health.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/output.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/params.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/profiles.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/py.typed +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/reaper.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/scaffold.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/sources.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/starters.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/stream.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/timing.py +0 -0
- {dirigent_cli-0.23.4 → dirigent_cli-0.25.0}/src/dirigent_cli/triggers.py +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.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.25.0
|
|
10
|
+
Requires-Dist: dirigent-blocks==0.25.0
|
|
11
|
+
Requires-Dist: dirigent-client==0.25.0
|
|
12
|
+
Requires-Dist: dirigent-common==0.25.0
|
|
13
|
+
Requires-Dist: dirigent-core==0.25.0
|
|
14
|
+
Requires-Dist: dirigent-examples==0.25.0
|
|
15
|
+
Requires-Dist: dirigent-plugin==0.25.0
|
|
16
|
+
Requires-Dist: dirigent-server==0.25.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.25.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.25.0",
|
|
15
|
+
"dirigent-blocks==0.25.0",
|
|
16
|
+
"dirigent-client==0.25.0",
|
|
17
|
+
"dirigent-common==0.25.0",
|
|
18
|
+
"dirigent-core==0.25.0",
|
|
19
|
+
"dirigent-examples==0.25.0",
|
|
20
|
+
"dirigent-plugin==0.25.0",
|
|
21
|
+
"dirigent-server==0.25.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.25.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.25.0",
|
|
15
|
+
"dirigent-blocks==0.25.0",
|
|
16
|
+
"dirigent-client==0.25.0",
|
|
17
|
+
"dirigent-common==0.25.0",
|
|
18
|
+
"dirigent-core==0.25.0",
|
|
19
|
+
"dirigent-examples==0.25.0",
|
|
20
|
+
"dirigent-plugin==0.25.0",
|
|
21
|
+
"dirigent-server==0.25.0",
|
|
22
22
|
"httpx2>=2.12.0",
|
|
23
23
|
"python-dotenv>=1.1.0",
|
|
24
24
|
"pyyaml>=6.0.3",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""The commands that talk to a server: definitions, execution, catalog, connections, admin."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import sys
|
|
6
7
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
@@ -43,6 +44,8 @@ from dirigent_cli.messages import (
|
|
|
43
44
|
INIT_CANCELLED,
|
|
44
45
|
INIT_REFUSED,
|
|
45
46
|
INIT_WAY,
|
|
47
|
+
INPUT_FILE_UNREADABLE,
|
|
48
|
+
INPUT_REFUSED,
|
|
46
49
|
INSTANCE_ALREADY_HERE,
|
|
47
50
|
KEEP_IS_LOCAL_ONLY,
|
|
48
51
|
LOCAL_RUN_HAD_NO_OUTCOME,
|
|
@@ -56,6 +59,7 @@ from dirigent_cli.messages import (
|
|
|
56
59
|
NO_FAILURE_TO_RETRY,
|
|
57
60
|
NO_FIRST_PASSWORD,
|
|
58
61
|
NO_REPORT_DOCUMENT,
|
|
62
|
+
NO_SUCH_BLOCK,
|
|
59
63
|
NO_SUCH_EXAMPLE,
|
|
60
64
|
NOT_A_BLOCK_KIND,
|
|
61
65
|
NOT_A_PRIORITY,
|
|
@@ -120,6 +124,7 @@ from dirigent_cli.project import (
|
|
|
120
124
|
DEFAULT_PIPELINES_DIR,
|
|
121
125
|
InitChoices,
|
|
122
126
|
ProjectError,
|
|
127
|
+
agent_notes,
|
|
123
128
|
check_choices,
|
|
124
129
|
find_project,
|
|
125
130
|
scaffold,
|
|
@@ -309,6 +314,18 @@ def _print_plan(result: ApplyResult, origin: str) -> bool:
|
|
|
309
314
|
return True
|
|
310
315
|
|
|
311
316
|
|
|
317
|
+
def _print_source_tokens(result: ApplyResult, *, base_url: str) -> None:
|
|
318
|
+
"""Print each token an apply minted for a source, once, with where it is presented.
|
|
319
|
+
|
|
320
|
+
The instance stores only a token's hash; there is no second chance to read it.
|
|
321
|
+
"""
|
|
322
|
+
for minted in result.sources.tokens:
|
|
323
|
+
console.print(f"\n source [bold]{minted.code}[/] {base_url.rstrip('/')}{minted.url_path}")
|
|
324
|
+
console.print(f" Authorization: Bearer [bold]{minted.token}[/]")
|
|
325
|
+
if result.sources.tokens:
|
|
326
|
+
console.print("\n[yellow]Each token is shown once.[/] The instance stores only its hash.\n")
|
|
327
|
+
|
|
328
|
+
|
|
312
329
|
def _print_graph(steps: Sequence[GraphStep], title: str = "steps") -> None:
|
|
313
330
|
"""Print a pipeline's shape: roots at the margin, each step under what it waits for."""
|
|
314
331
|
lines = render_graph(steps)
|
|
@@ -322,7 +339,7 @@ def _print_graph(steps: Sequence[GraphStep], title: str = "steps") -> None:
|
|
|
322
339
|
def graph_steps(definition: PipelineDefinition) -> list[GraphStep]:
|
|
323
340
|
"""Read a definition's steps in the shape the tree is drawn from."""
|
|
324
341
|
return [
|
|
325
|
-
GraphStep(name=name, block=step.
|
|
342
|
+
GraphStep(name=name, block=step.block_id, depends_on=tuple(step.depends_on), rule=step.rule.value)
|
|
326
343
|
for name, step in definition.steps.items()
|
|
327
344
|
]
|
|
328
345
|
|
|
@@ -402,6 +419,8 @@ def apply_command(
|
|
|
402
419
|
raise typer.Exit(code=1)
|
|
403
420
|
return
|
|
404
421
|
accepted = all(_print_plan(result, origin) for result, origin in results)
|
|
422
|
+
for result, _ in results:
|
|
423
|
+
_print_source_tokens(result, base_url=dg.url)
|
|
405
424
|
if pruned is not None:
|
|
406
425
|
for code in pruned.pruned:
|
|
407
426
|
console.print(f"{styled('deactivated'):<10} {code} [dim](absent from the project)[/]")
|
|
@@ -484,10 +503,14 @@ def validate_command(
|
|
|
484
503
|
bool, typer.Option("--explain", help="Also say what the work will cost: how wide, how many tries, how long.")
|
|
485
504
|
] = False,
|
|
486
505
|
) -> None:
|
|
487
|
-
"""Check a document
|
|
506
|
+
"""Check a document against the blocks installed beside dg, or against a server's with --server.
|
|
488
507
|
|
|
489
|
-
Offline,
|
|
490
|
-
|
|
508
|
+
Offline, every step's block and config is checked against the blocks installed beside dg,
|
|
509
|
+
and a connection or pipeline a document names is not, since only an instance knows what it
|
|
510
|
+
holds; a triggers document is checked at the schema level, its clocks and its codes.
|
|
511
|
+
`--server` checks against the instance's own catalog instead, and adds the connections and
|
|
512
|
+
pipelines it holds, the pipeline a triggers document names, and the parameters its
|
|
513
|
+
schedules pin.
|
|
491
514
|
|
|
492
515
|
`--explain` adds the shape of the work ahead for every pipeline document that is valid:
|
|
493
516
|
the fan-out widths, the retry budgets and the waits. With `--server` too, a sensor that
|
|
@@ -509,11 +532,18 @@ def validate_command(
|
|
|
509
532
|
problems=[issue.model_dump(mode="json") for issue in error.problems],
|
|
510
533
|
)
|
|
511
534
|
failures += 1
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
535
|
+
from dirigent_common import format_checker_with
|
|
536
|
+
from dirigent_core.plugins import load_plugin_host
|
|
537
|
+
|
|
538
|
+
host = load_plugin_host()
|
|
539
|
+
catalog = host.catalog()
|
|
540
|
+
blocks: Mapping[str, Any] | None = host.blocks
|
|
541
|
+
checker = format_checker_with(host.formats)
|
|
542
|
+
connections: list[str] | None = None
|
|
543
|
+
pipelines: list[str] | None = None
|
|
515
544
|
targets: dict[str, TriggerTarget] = {}
|
|
516
545
|
if server:
|
|
546
|
+
blocks = None
|
|
517
547
|
with client_for(state) as dg:
|
|
518
548
|
catalog = dg.call(dg.blocks.catalog())
|
|
519
549
|
connections = [
|
|
@@ -527,10 +557,11 @@ def validate_command(
|
|
|
527
557
|
for definition, label in parsed:
|
|
528
558
|
issues = validate_against_catalog(
|
|
529
559
|
definition,
|
|
530
|
-
catalog
|
|
560
|
+
catalog,
|
|
531
561
|
connections=connections,
|
|
532
562
|
pipelines=pipelines,
|
|
533
|
-
|
|
563
|
+
blocks=blocks,
|
|
564
|
+
format_checker=checker,
|
|
534
565
|
target=targets.get(definition.pipeline) if isinstance(definition, TriggersDefinition) else None,
|
|
535
566
|
)
|
|
536
567
|
if issues:
|
|
@@ -544,7 +575,7 @@ def validate_command(
|
|
|
544
575
|
)
|
|
545
576
|
failures += 1
|
|
546
577
|
continue
|
|
547
|
-
checked = "document and catalog" if
|
|
578
|
+
checked = "document and catalog" if server else "document and installed blocks"
|
|
548
579
|
emit_fact(
|
|
549
580
|
"validation",
|
|
550
581
|
message="valid",
|
|
@@ -614,6 +645,10 @@ def init_command(
|
|
|
614
645
|
] = None,
|
|
615
646
|
admin: Annotated[str, typer.Option(help="The first admin account's username.")] = "admin",
|
|
616
647
|
password: Annotated[str | None, typer.Option(help="Its password; asked for when omitted.")] = None,
|
|
648
|
+
agents: Annotated[
|
|
649
|
+
bool,
|
|
650
|
+
typer.Option("--agents", help="Write only AGENTS.md and CLAUDE.md, into a project that already exists."),
|
|
651
|
+
] = False,
|
|
617
652
|
) -> None:
|
|
618
653
|
"""Initialise a uv project and the instance it addresses, ready for `dg dev`.
|
|
619
654
|
|
|
@@ -624,7 +659,8 @@ def init_command(
|
|
|
624
659
|
and mints it a token and a secret key, both kept in `.env`. `--template compose` writes the
|
|
625
660
|
documents and a container stack instead, and initialises nothing locally: the instance is
|
|
626
661
|
the containers. `--template documents` writes the documents alone, against an instance
|
|
627
|
-
somebody else runs.
|
|
662
|
+
somebody else runs. `--agents` writes a coding agent's notes alone, into a project made
|
|
663
|
+
before `dg init` wrote them, and keeps any the project already has.
|
|
628
664
|
"""
|
|
629
665
|
import asyncio
|
|
630
666
|
|
|
@@ -633,6 +669,20 @@ def init_command(
|
|
|
633
669
|
root = directory.resolve()
|
|
634
670
|
version = cli_version()
|
|
635
671
|
unasked = template is None and service is None and pack is None and pipeline is None and not workflow
|
|
672
|
+
if agents:
|
|
673
|
+
if not unasked or password is not None or admin != "admin":
|
|
674
|
+
_init_fail(INIT_REFUSED, detail="--agents writes the notes alone and takes no other option")
|
|
675
|
+
if not directory.is_dir():
|
|
676
|
+
_init_fail(INIT_REFUSED, detail=f"{directory} is not a directory; --agents writes into a project")
|
|
677
|
+
notes = agent_notes(directory)
|
|
678
|
+
emit_fact(
|
|
679
|
+
"project.scaffolded",
|
|
680
|
+
message="scaffolded",
|
|
681
|
+
directory=str(directory),
|
|
682
|
+
files=[_within(path, directory) for path in notes.files],
|
|
683
|
+
**({"skipped": [_within(path, directory) for path in notes.skipped]} if notes.skipped else {}),
|
|
684
|
+
)
|
|
685
|
+
return
|
|
636
686
|
asked = unasked and sys.stdin.isatty() and sys.stdout.isatty()
|
|
637
687
|
# Refusing happens before anything is written or asked for, so a run that cannot finish
|
|
638
688
|
# has not half-created a project, and nobody fills a form for a command that was going
|
|
@@ -947,6 +997,29 @@ def pipeline_validate(
|
|
|
947
997
|
raise typer.Exit(code=1)
|
|
948
998
|
|
|
949
999
|
|
|
1000
|
+
@pipeline_app.command("schema")
|
|
1001
|
+
def pipeline_schema(
|
|
1002
|
+
file: Annotated[
|
|
1003
|
+
Path | None, typer.Option("-f", "--file", help="Write the schema to this file instead of into the record.")
|
|
1004
|
+
] = None,
|
|
1005
|
+
) -> None:
|
|
1006
|
+
"""Compose the JSON Schema a document is written against, from the blocks installed beside dg.
|
|
1007
|
+
|
|
1008
|
+
It is the format and every installed block's config in one Draft 2020-12 schema, which an
|
|
1009
|
+
editor or a language model checks a document against before anything validates it. Written
|
|
1010
|
+
to a file, it is what a ``# yaml-language-server: $schema=`` comment names.
|
|
1011
|
+
"""
|
|
1012
|
+
from dirigent_core.documentschema import document_schema
|
|
1013
|
+
|
|
1014
|
+
catalog = installed_catalog()
|
|
1015
|
+
schema = document_schema(catalog)
|
|
1016
|
+
if file is not None:
|
|
1017
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
1018
|
+
file.write_text(json.dumps(schema, indent=2) + "\n")
|
|
1019
|
+
return emit_fact("pipeline.schema", message="written", path=str(file), blocks=len(catalog.blocks))
|
|
1020
|
+
emit_fact("pipeline.schema", message="composed", blocks=len(catalog.blocks), schema=schema)
|
|
1021
|
+
|
|
1022
|
+
|
|
950
1023
|
@pipeline_app.command("activate")
|
|
951
1024
|
def pipeline_activate(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> None:
|
|
952
1025
|
"""Make a pipeline runnable again."""
|
|
@@ -1080,6 +1153,23 @@ def parse_priority(value: str | None) -> RunPriority | None:
|
|
|
1080
1153
|
)
|
|
1081
1154
|
|
|
1082
1155
|
|
|
1156
|
+
def parse_input(value: str | None) -> Any:
|
|
1157
|
+
"""Read what a hand run hands its source: JSON or YAML written out, or ``@FILE`` holding either."""
|
|
1158
|
+
if value is None:
|
|
1159
|
+
return None
|
|
1160
|
+
text = value
|
|
1161
|
+
if value.startswith("@"):
|
|
1162
|
+
path = Path(value[1:])
|
|
1163
|
+
try:
|
|
1164
|
+
text = path.read_text()
|
|
1165
|
+
except OSError as error:
|
|
1166
|
+
fail(INPUT_FILE_UNREADABLE, path=str(path), detail=error.strerror or str(error))
|
|
1167
|
+
try:
|
|
1168
|
+
return yaml.safe_load(text)
|
|
1169
|
+
except yaml.YAMLError as error:
|
|
1170
|
+
fail(INPUT_REFUSED, detail=str(error).splitlines()[0])
|
|
1171
|
+
|
|
1172
|
+
|
|
1083
1173
|
def parse_importance(value: str | None) -> Importance | None:
|
|
1084
1174
|
"""Read the importance a rule was given, naming the vocabulary when it is not one."""
|
|
1085
1175
|
if value is None:
|
|
@@ -1164,6 +1254,20 @@ def run_command(
|
|
|
1164
1254
|
"Omitted takes the pipeline's own.",
|
|
1165
1255
|
),
|
|
1166
1256
|
] = None,
|
|
1257
|
+
source: Annotated[
|
|
1258
|
+
str | None,
|
|
1259
|
+
typer.Option(
|
|
1260
|
+
"--source",
|
|
1261
|
+
help="The source step a pipeline with sources is entered at. Omitted takes its only one.",
|
|
1262
|
+
),
|
|
1263
|
+
] = None,
|
|
1264
|
+
given: Annotated[
|
|
1265
|
+
str | None,
|
|
1266
|
+
typer.Option(
|
|
1267
|
+
"--input",
|
|
1268
|
+
help="What arrives at that source, as JSON or YAML, or @FILE. Omitted takes the source's sample.",
|
|
1269
|
+
),
|
|
1270
|
+
] = None,
|
|
1167
1271
|
) -> None:
|
|
1168
1272
|
"""Run a pipeline by code, or apply and run a document in one command.
|
|
1169
1273
|
|
|
@@ -1182,6 +1286,7 @@ def run_command(
|
|
|
1182
1286
|
wanted = parse_priority(priority)
|
|
1183
1287
|
if wanted is not None and local:
|
|
1184
1288
|
fail(PRIORITY_IS_NOT_LOCAL)
|
|
1289
|
+
arrives = parse_input(given)
|
|
1185
1290
|
if local:
|
|
1186
1291
|
_run_locally(
|
|
1187
1292
|
state,
|
|
@@ -1197,6 +1302,8 @@ def run_command(
|
|
|
1197
1302
|
strict=strict,
|
|
1198
1303
|
keep=keep,
|
|
1199
1304
|
root=root,
|
|
1305
|
+
source=source,
|
|
1306
|
+
given=arrives,
|
|
1200
1307
|
)
|
|
1201
1308
|
return
|
|
1202
1309
|
with client_for(state) as dg:
|
|
@@ -1224,7 +1331,11 @@ def run_command(
|
|
|
1224
1331
|
stored = dg.call(dg.pipelines.get(code))
|
|
1225
1332
|
schema = dict((stored.document or {}).get("params") or {})
|
|
1226
1333
|
params = parse_params(param, schema=schema, files=params_file)
|
|
1227
|
-
accepted = dg.call(
|
|
1334
|
+
accepted = dg.call(
|
|
1335
|
+
dg.pipelines.run(
|
|
1336
|
+
code, params=params, window=covered, log_levels=levels, priority=wanted, source=source, input=arrives
|
|
1337
|
+
)
|
|
1338
|
+
)
|
|
1228
1339
|
if accepted.run_id is None:
|
|
1229
1340
|
if state.json_output:
|
|
1230
1341
|
emit_problem(
|
|
@@ -1315,6 +1426,8 @@ def _run_locally(
|
|
|
1315
1426
|
strict: bool,
|
|
1316
1427
|
keep: bool = False,
|
|
1317
1428
|
root: Path | None = None,
|
|
1429
|
+
source: str | None = None,
|
|
1430
|
+
given: Any = None,
|
|
1318
1431
|
) -> None:
|
|
1319
1432
|
"""Apply and run a document in a throwaway instance, streaming what it logs."""
|
|
1320
1433
|
if state.url is not None or state.profile is not None or state.token is not None:
|
|
@@ -1344,6 +1457,8 @@ def _run_locally(
|
|
|
1344
1457
|
state=state,
|
|
1345
1458
|
keep=keep,
|
|
1346
1459
|
root=root,
|
|
1460
|
+
source=source,
|
|
1461
|
+
given=given,
|
|
1347
1462
|
)
|
|
1348
1463
|
)
|
|
1349
1464
|
if outcome is None: # pragma: no cover - the generator always ends with an outcome
|
|
@@ -1384,6 +1499,8 @@ async def _stream_local(
|
|
|
1384
1499
|
state: CliState,
|
|
1385
1500
|
keep: bool = False,
|
|
1386
1501
|
root: Path | None = None,
|
|
1502
|
+
source: str | None = None,
|
|
1503
|
+
given: Any = None,
|
|
1387
1504
|
) -> LocalOutcome | None:
|
|
1388
1505
|
"""Drive a local run, printing each transition and log line as it lands."""
|
|
1389
1506
|
outcome: LocalOutcome | None = None
|
|
@@ -1400,6 +1517,8 @@ async def _stream_local(
|
|
|
1400
1517
|
inherited=settings,
|
|
1401
1518
|
keep=keep,
|
|
1402
1519
|
root=root,
|
|
1520
|
+
source=source,
|
|
1521
|
+
given=given,
|
|
1403
1522
|
):
|
|
1404
1523
|
match item:
|
|
1405
1524
|
case LocalStarted():
|
|
@@ -2246,16 +2365,35 @@ def blocks_new(
|
|
|
2246
2365
|
console.print(" [bold]uv sync && uv run pytest[/]")
|
|
2247
2366
|
|
|
2248
2367
|
|
|
2368
|
+
#: What ``--local`` means on the block commands.
|
|
2369
|
+
LOCAL_BLOCKS = "Read the blocks installed beside dg, instead of the instance's own."
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
def installed_catalog() -> Catalog:
|
|
2373
|
+
"""Read the catalog every plugin installed beside this `dg` contributes, without addressing any server."""
|
|
2374
|
+
from dirigent_core.plugins import load_plugin_host
|
|
2375
|
+
|
|
2376
|
+
return load_plugin_host().catalog()
|
|
2377
|
+
|
|
2378
|
+
|
|
2249
2379
|
@blocks_app.command("list")
|
|
2250
2380
|
def blocks_list(
|
|
2251
2381
|
ctx: typer.Context,
|
|
2252
2382
|
kind: Annotated[str | None, typer.Option(help="Only operators, or only sensors.")] = None,
|
|
2383
|
+
local: Annotated[bool, typer.Option("--local", help=LOCAL_BLOCKS)] = False,
|
|
2253
2384
|
) -> None:
|
|
2254
|
-
"""List the blocks this instance can run."""
|
|
2385
|
+
"""List the blocks this instance can run, or with --local the ones installed beside dg."""
|
|
2255
2386
|
if kind is not None and kind not in set(BlockKind):
|
|
2256
2387
|
fail(NOT_A_BLOCK_KIND, kind=repr(kind), allowed=", ".join(sorted(BlockKind)))
|
|
2257
|
-
|
|
2258
|
-
|
|
2388
|
+
wanted = BlockKind(kind) if kind else None
|
|
2389
|
+
if local:
|
|
2390
|
+
catalog = installed_catalog()
|
|
2391
|
+
catalog = catalog.model_copy(
|
|
2392
|
+
update={"blocks": [block for block in catalog.blocks if wanted is None or block.kind is wanted]}
|
|
2393
|
+
)
|
|
2394
|
+
else:
|
|
2395
|
+
with client_for(state_of(ctx)) as dg:
|
|
2396
|
+
catalog = dg.call(dg.blocks.catalog(kind=wanted))
|
|
2259
2397
|
if state_of(ctx).json_output:
|
|
2260
2398
|
emit_records("block", catalog.blocks)
|
|
2261
2399
|
emit_records("storage_scheme", catalog.storage_schemes)
|
|
@@ -2276,10 +2414,17 @@ def blocks_list(
|
|
|
2276
2414
|
def blocks_show(
|
|
2277
2415
|
ctx: typer.Context,
|
|
2278
2416
|
block_id: Annotated[str, typer.Argument(help="The block to describe.")],
|
|
2417
|
+
local: Annotated[bool, typer.Option("--local", help=LOCAL_BLOCKS)] = False,
|
|
2279
2418
|
) -> None:
|
|
2280
2419
|
"""Show one block's config and output schemas."""
|
|
2281
|
-
|
|
2282
|
-
|
|
2420
|
+
if local:
|
|
2421
|
+
found = [block for block in installed_catalog().blocks if block.id == block_id]
|
|
2422
|
+
if not found:
|
|
2423
|
+
fail(NO_SUCH_BLOCK, block=repr(block_id))
|
|
2424
|
+
entry = found[0]
|
|
2425
|
+
else:
|
|
2426
|
+
with client_for(state_of(ctx)) as dg:
|
|
2427
|
+
entry = dg.call(dg.blocks.get(block_id))
|
|
2283
2428
|
if state_of(ctx).json_output:
|
|
2284
2429
|
return emit_one("block", entry)
|
|
2285
2430
|
fields(
|
|
@@ -25,6 +25,7 @@ from dirigent_common import Duration, JsonMap, base_format_checker, format_durat
|
|
|
25
25
|
from dirigent_core.engine.definition import ParameterError, PipelineDefinition, RetryPolicy, StepDefinition
|
|
26
26
|
from dirigent_core.engine.failure import backoff_delay
|
|
27
27
|
from dirigent_core.engine.references import ReferenceScope, UnknownReference, resolve
|
|
28
|
+
from dirigent_core.engine.state import item_wise_parent
|
|
28
29
|
|
|
29
30
|
#: What a cardinality says when the document alone cannot fix it.
|
|
30
31
|
UNKNOWN: Final = "unknown"
|
|
@@ -65,7 +66,15 @@ def explain(definition: PipelineDefinition, catalog: Catalog | None = None) -> D
|
|
|
65
66
|
order = definition.topological_order()
|
|
66
67
|
shapes: dict[str, StepShape] = {}
|
|
67
68
|
for name in order:
|
|
68
|
-
shapes[name] = _step_shape(
|
|
69
|
+
shapes[name] = _step_shape(
|
|
70
|
+
name,
|
|
71
|
+
definition.steps[name],
|
|
72
|
+
params,
|
|
73
|
+
shapes,
|
|
74
|
+
catalog,
|
|
75
|
+
definition.grid_source(name),
|
|
76
|
+
item_wise_parent(definition, name),
|
|
77
|
+
)
|
|
69
78
|
rows = [shapes[name] for name in order]
|
|
70
79
|
return DocumentShape(
|
|
71
80
|
attempts_max=sum(row.max_attempts * (1 if row.elements is None else row.elements) for row in rows),
|
|
@@ -95,20 +104,23 @@ def _step_shape(
|
|
|
95
104
|
done: Mapping[str, StepShape],
|
|
96
105
|
catalog: Catalog | None,
|
|
97
106
|
source: str | None,
|
|
107
|
+
paired: str | None = None,
|
|
98
108
|
) -> StepShape:
|
|
99
109
|
"""Read one step as the work it will become, against the steps already read.
|
|
100
110
|
|
|
101
|
-
``source`` is the step a late grid waits for,
|
|
111
|
+
``source`` is the step a late grid waits for, and ``paired`` the fan-out whose items this
|
|
112
|
+
step's items wait for one by one; the row names both.
|
|
102
113
|
"""
|
|
103
114
|
cardinality, elements = _cardinality(step, params, done, source)
|
|
104
115
|
poll, deadline, from_block = _waits(step, _entry(step, catalog))
|
|
105
116
|
return StepShape(
|
|
106
117
|
name=name,
|
|
107
|
-
block=step.
|
|
118
|
+
block=step.block_id,
|
|
108
119
|
depends_on=list(step.depends_on),
|
|
109
120
|
cardinality=cardinality,
|
|
110
121
|
elements=elements,
|
|
111
122
|
grid_source=source,
|
|
123
|
+
item_wise_parent=paired,
|
|
112
124
|
reference=step.for_each if isinstance(step.for_each, str) else None,
|
|
113
125
|
items=step.items.value,
|
|
114
126
|
max_attempts=step.retry.max_attempts,
|
|
@@ -147,7 +159,7 @@ def _cardinality(
|
|
|
147
159
|
|
|
148
160
|
def _entry(step: StepDefinition, catalog: Catalog | None) -> BlockEntry | None:
|
|
149
161
|
"""Find what the catalog says about this step's block, where there is a catalog."""
|
|
150
|
-
return None if catalog is None else catalog.block(step.block)
|
|
162
|
+
return None if catalog is None or step.block is None else catalog.block(step.block)
|
|
151
163
|
|
|
152
164
|
|
|
153
165
|
def _waits(step: StepDefinition, entry: BlockEntry | None) -> tuple[timedelta | None, timedelta | None, list[str]]:
|
|
@@ -30,7 +30,7 @@ def steps_of_document(document: Mapping[str, Any]) -> list[GraphStep]:
|
|
|
30
30
|
read.append(
|
|
31
31
|
GraphStep(
|
|
32
32
|
name=str(name),
|
|
33
|
-
block=
|
|
33
|
+
block=_block_of(held),
|
|
34
34
|
depends_on=tuple(str(item) for item in cast("list[Any]", depends or ())),
|
|
35
35
|
rule=str(held.get("rule", DEFAULT_RULE)),
|
|
36
36
|
)
|
|
@@ -38,6 +38,16 @@ def steps_of_document(document: Mapping[str, Any]) -> list[GraphStep]:
|
|
|
38
38
|
return read
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def _block_of(step: Mapping[str, Any]) -> str:
|
|
42
|
+
"""Name what a step runs: its block, a source's kind, or ``reply``."""
|
|
43
|
+
source = step.get("source")
|
|
44
|
+
if isinstance(source, dict):
|
|
45
|
+
return str(cast("dict[str, Any]", source).get("kind", "source"))
|
|
46
|
+
if "reply" in step:
|
|
47
|
+
return "reply"
|
|
48
|
+
return str(step.get("block", "-"))
|
|
49
|
+
|
|
50
|
+
|
|
41
51
|
def render_graph(steps: Sequence[GraphStep]) -> list[str]:
|
|
42
52
|
"""Draw the graph as indented lines, each step under the last step it waits for.
|
|
43
53
|
|