dirigent-cli 0.16.7__tar.gz → 0.17.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.16.7 → dirigent_cli-0.17.0}/PKG-INFO +9 -8
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/pyproject.toml +12 -8
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/pyproject.toml.orig +10 -8
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/commands.py +134 -67
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/context.py +6 -3
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/health.py +77 -34
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/main.py +59 -36
- dirigent_cli-0.17.0/src/dirigent_cli/messages.py +302 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/output.py +21 -4
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/project.py +2 -2
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/reaper.py +3 -3
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/seeding.py +1 -1
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/summaries.py +9 -5
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/triggers.py +15 -7
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/LICENSE +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/README.md +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/__init__.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/aliases.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/formatters.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/graph.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/init_form.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/local.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/params.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/profiles.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/py.typed +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/scaffold.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/schemas.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/sources.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/starters.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/stream.py +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
- {dirigent_cli-0.16.7 → dirigent_cli-0.17.0}/src/dirigent_cli/timing.py +0 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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-
|
|
10
|
-
Requires-Dist: dirigent-
|
|
11
|
-
Requires-Dist: dirigent-
|
|
12
|
-
Requires-Dist: dirigent-
|
|
13
|
-
Requires-Dist: dirigent-
|
|
14
|
-
Requires-Dist: dirigent-
|
|
15
|
-
Requires-Dist: dirigent-
|
|
9
|
+
Requires-Dist: dirigent-block-execute==0.17.0
|
|
10
|
+
Requires-Dist: dirigent-blocks==0.17.0
|
|
11
|
+
Requires-Dist: dirigent-client==0.17.0
|
|
12
|
+
Requires-Dist: dirigent-common==0.17.0
|
|
13
|
+
Requires-Dist: dirigent-core==0.17.0
|
|
14
|
+
Requires-Dist: dirigent-examples==0.17.0
|
|
15
|
+
Requires-Dist: dirigent-plugin==0.17.0
|
|
16
|
+
Requires-Dist: dirigent-server==0.17.0
|
|
16
17
|
Requires-Dist: httpx2>=2.12.0
|
|
17
18
|
Requires-Dist: python-dotenv>=1.1.0
|
|
18
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.17.0"
|
|
4
4
|
description = "The dirigent command line interface (dirigent / dg)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,13 +11,14 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-
|
|
15
|
-
"dirigent-
|
|
16
|
-
"dirigent-
|
|
17
|
-
"dirigent-
|
|
18
|
-
"dirigent-
|
|
19
|
-
"dirigent-
|
|
20
|
-
"dirigent-
|
|
14
|
+
"dirigent-block-execute==0.17.0",
|
|
15
|
+
"dirigent-blocks==0.17.0",
|
|
16
|
+
"dirigent-client==0.17.0",
|
|
17
|
+
"dirigent-common==0.17.0",
|
|
18
|
+
"dirigent-core==0.17.0",
|
|
19
|
+
"dirigent-examples==0.17.0",
|
|
20
|
+
"dirigent-plugin==0.17.0",
|
|
21
|
+
"dirigent-server==0.17.0",
|
|
21
22
|
"httpx2>=2.12.0",
|
|
22
23
|
"python-dotenv>=1.1.0",
|
|
23
24
|
"pyyaml>=6.0.3",
|
|
@@ -36,6 +37,9 @@ dg = "dirigent_cli.main:run"
|
|
|
36
37
|
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
37
38
|
build-backend = "uv_build"
|
|
38
39
|
|
|
40
|
+
[tool.uv.sources.dirigent-block-execute]
|
|
41
|
+
workspace = true
|
|
42
|
+
|
|
39
43
|
[tool.uv.sources.dirigent-blocks]
|
|
40
44
|
workspace = true
|
|
41
45
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.17.0"
|
|
4
4
|
description = "The dirigent command line interface (dirigent / dg)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,13 +11,14 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-
|
|
15
|
-
"dirigent-
|
|
16
|
-
"dirigent-
|
|
17
|
-
"dirigent-
|
|
18
|
-
"dirigent-
|
|
19
|
-
"dirigent-
|
|
20
|
-
"dirigent-
|
|
14
|
+
"dirigent-block-execute==0.17.0",
|
|
15
|
+
"dirigent-blocks==0.17.0",
|
|
16
|
+
"dirigent-client==0.17.0",
|
|
17
|
+
"dirigent-common==0.17.0",
|
|
18
|
+
"dirigent-core==0.17.0",
|
|
19
|
+
"dirigent-examples==0.17.0",
|
|
20
|
+
"dirigent-plugin==0.17.0",
|
|
21
|
+
"dirigent-server==0.17.0",
|
|
21
22
|
"httpx2>=2.12.0",
|
|
22
23
|
"python-dotenv>=1.1.0",
|
|
23
24
|
"pyyaml>=6.0.3",
|
|
@@ -37,6 +38,7 @@ requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
|
37
38
|
build-backend = "uv_build"
|
|
38
39
|
|
|
39
40
|
[tool.uv.sources]
|
|
41
|
+
dirigent-block-execute = { workspace = true }
|
|
40
42
|
dirigent-blocks = { workspace = true }
|
|
41
43
|
dirigent-client = { workspace = true }
|
|
42
44
|
dirigent-core = { workspace = true }
|
|
@@ -29,6 +29,60 @@ from dirigent_cli.local import (
|
|
|
29
29
|
load_schema_specs,
|
|
30
30
|
run_document,
|
|
31
31
|
)
|
|
32
|
+
from dirigent_cli.messages import (
|
|
33
|
+
ADMIN_IS_FIXED_IN_A_STACK,
|
|
34
|
+
ALL_TAKES_NO_CODE,
|
|
35
|
+
ALREADY_THERE,
|
|
36
|
+
AS_IS_ONE_DOCUMENT,
|
|
37
|
+
CANNOT_PROMPT,
|
|
38
|
+
CANNOT_PROMPT_SET,
|
|
39
|
+
CONCURRENCY_REFUSED,
|
|
40
|
+
DOCUMENT_INVALID,
|
|
41
|
+
GUARD_REFUSED,
|
|
42
|
+
INIT_CANCELLED,
|
|
43
|
+
INIT_REFUSED,
|
|
44
|
+
INIT_WAY,
|
|
45
|
+
INSTANCE_ALREADY_HERE,
|
|
46
|
+
KEEP_IS_LOCAL_ONLY,
|
|
47
|
+
LOCAL_RUN_HAD_NO_OUTCOME,
|
|
48
|
+
LOCAL_TAKES_A_DOCUMENT,
|
|
49
|
+
LOCAL_TAKES_NO_SERVER,
|
|
50
|
+
LOG_LEVEL_NO_PATTERN,
|
|
51
|
+
LOG_LEVEL_NOT_A_LEVEL,
|
|
52
|
+
NAME_A_PIPELINE,
|
|
53
|
+
NAME_ANOTHER_CODE,
|
|
54
|
+
NO_DOCUMENT_AND_NO_PROJECT,
|
|
55
|
+
NO_FAILURE_TO_RETRY,
|
|
56
|
+
NO_FIRST_PASSWORD,
|
|
57
|
+
NO_REPORT_DOCUMENT,
|
|
58
|
+
NO_SUCH_EXAMPLE,
|
|
59
|
+
NOT_A_BLOCK_KIND,
|
|
60
|
+
NOT_A_PRIORITY,
|
|
61
|
+
NOT_A_RUN_STATUS,
|
|
62
|
+
NOT_A_STARTER,
|
|
63
|
+
NOT_A_WINDOW,
|
|
64
|
+
NOT_AUTHENTICATED,
|
|
65
|
+
OR_A_PROFILE_TOKEN,
|
|
66
|
+
OR_DG_AUTH_LOGIN,
|
|
67
|
+
PARAMS_REFUSED,
|
|
68
|
+
PRIORITY_IS_NOT_LOCAL,
|
|
69
|
+
PROJECT_IS_EMPTY,
|
|
70
|
+
PRUNE_IS_PROJECT_WIDE,
|
|
71
|
+
ROOT_IS_LOCAL_ONLY,
|
|
72
|
+
RUN_SKIPPED,
|
|
73
|
+
SCAFFOLD_REFUSED,
|
|
74
|
+
SCHEMA_NOT_AN_OBJECT,
|
|
75
|
+
SCHEMA_UNREADABLE,
|
|
76
|
+
SET_DG_TOKEN,
|
|
77
|
+
SOURCE_REFUSED,
|
|
78
|
+
STARTER_TAG_NEEDED,
|
|
79
|
+
STRICT_NEEDS_WATCH,
|
|
80
|
+
TRIGGERS_DOCUMENT_NOT_RUNNABLE,
|
|
81
|
+
UNKNOWN_CONNECTION_KIND,
|
|
82
|
+
UNSAFE_FOR_THE_INSTANCE,
|
|
83
|
+
UNSAFE_FOR_THIS_RUN,
|
|
84
|
+
)
|
|
85
|
+
from dirigent_cli.messages import WINDOW_GRAMMAR as WINDOW_GRAMMAR_MESSAGE
|
|
32
86
|
from dirigent_cli.output import (
|
|
33
87
|
Detail,
|
|
34
88
|
age,
|
|
@@ -100,7 +154,7 @@ from dirigent_client import (
|
|
|
100
154
|
RunStatus,
|
|
101
155
|
ValidationIssue,
|
|
102
156
|
)
|
|
103
|
-
from dirigent_common import JsonMap
|
|
157
|
+
from dirigent_common import Issue, JsonMap, Message
|
|
104
158
|
from dirigent_core import migrations
|
|
105
159
|
from dirigent_core.config import STATE_DIR, Settings
|
|
106
160
|
from dirigent_core.documents import (
|
|
@@ -164,9 +218,9 @@ def paged[T](fetch: Callable[[str | None, int], Page[T]], limit: int | None) ->
|
|
|
164
218
|
after = page.next
|
|
165
219
|
|
|
166
220
|
|
|
167
|
-
def fail(message:
|
|
221
|
+
def fail(message: Message, /, **params: Any) -> NoReturn:
|
|
168
222
|
"""Write a refusal the CLI decided on as a record, and exit non-zero."""
|
|
169
|
-
refuse(message)
|
|
223
|
+
refuse(message, **params)
|
|
170
224
|
raise typer.Exit(code=1)
|
|
171
225
|
|
|
172
226
|
|
|
@@ -182,7 +236,7 @@ def ask(label: str, *, hide: bool = False) -> str:
|
|
|
182
236
|
invocation fails with a JSON object instead of blocking on a terminal that is a pipe.
|
|
183
237
|
"""
|
|
184
238
|
if json_mode():
|
|
185
|
-
fail(
|
|
239
|
+
fail(CANNOT_PROMPT, label=label)
|
|
186
240
|
return str(typer.prompt(label, hide_input=hide))
|
|
187
241
|
|
|
188
242
|
|
|
@@ -199,7 +253,7 @@ def _apply_one(
|
|
|
199
253
|
try:
|
|
200
254
|
body = parse_document(document)
|
|
201
255
|
except SourceError as error:
|
|
202
|
-
fail(str(error))
|
|
256
|
+
fail(SOURCE_REFUSED, detail=str(error))
|
|
203
257
|
return dg.call(
|
|
204
258
|
dg.pipelines.apply(
|
|
205
259
|
body,
|
|
@@ -271,7 +325,7 @@ def _runnable(text: str) -> PipelineDefinition:
|
|
|
271
325
|
"""Read a document a local run is about to execute, refusing the kind that cannot be run."""
|
|
272
326
|
definition = load_text(text)
|
|
273
327
|
if not isinstance(definition, PipelineDefinition):
|
|
274
|
-
fail(
|
|
328
|
+
fail(TRIGGERS_DOCUMENT_NOT_RUNNABLE)
|
|
275
329
|
return definition
|
|
276
330
|
|
|
277
331
|
|
|
@@ -312,7 +366,7 @@ def apply_command(
|
|
|
312
366
|
"""
|
|
313
367
|
state = state_of(ctx)
|
|
314
368
|
if prune and reference is not None:
|
|
315
|
-
fail(
|
|
369
|
+
fail(PRUNE_IS_PROJECT_WIDE)
|
|
316
370
|
documents = _documents_to_apply(reference, as_code)
|
|
317
371
|
source = ProvenanceSource.DIRECTORY if prune else None
|
|
318
372
|
with client_for(state) as dg:
|
|
@@ -356,18 +410,18 @@ def _documents_to_apply(reference: str | None, as_code: str | None) -> list[Docu
|
|
|
356
410
|
try:
|
|
357
411
|
return [read_document(reference)]
|
|
358
412
|
except SourceError as error:
|
|
359
|
-
fail(str(error))
|
|
413
|
+
fail(SOURCE_REFUSED, detail=str(error))
|
|
360
414
|
project = find_project()
|
|
361
415
|
if project is None:
|
|
362
|
-
fail(
|
|
416
|
+
fail(NO_DOCUMENT_AND_NO_PROJECT)
|
|
363
417
|
if as_code is not None:
|
|
364
|
-
fail(
|
|
418
|
+
fail(AS_IS_ONE_DOCUMENT)
|
|
365
419
|
try:
|
|
366
420
|
paths = cast("Any", project).documents()
|
|
367
421
|
except ProjectError as error:
|
|
368
|
-
fail(str(error))
|
|
422
|
+
fail(SOURCE_REFUSED, detail=str(error))
|
|
369
423
|
if not paths:
|
|
370
|
-
fail(
|
|
424
|
+
fail(PROJECT_IS_EMPTY, directory=cast("Any", project).pipelines_dir)
|
|
371
425
|
# Pipelines go before triggers documents: one that names a pipeline the instance does not
|
|
372
426
|
# hold yet is refused, and applying the pipeline first is what makes it present.
|
|
373
427
|
return sorted((read_path(path) for path in paths), key=_declares_triggers)
|
|
@@ -432,7 +486,7 @@ def validate_command(
|
|
|
432
486
|
level="error",
|
|
433
487
|
message="invalid",
|
|
434
488
|
document=document.label,
|
|
435
|
-
problems=
|
|
489
|
+
problems=[issue.model_dump(mode="json") for issue in error.problems],
|
|
436
490
|
)
|
|
437
491
|
failures += 1
|
|
438
492
|
catalog: Catalog | None = None
|
|
@@ -466,7 +520,7 @@ def validate_command(
|
|
|
466
520
|
message="invalid",
|
|
467
521
|
code=definition.code,
|
|
468
522
|
document=label,
|
|
469
|
-
problems=[
|
|
523
|
+
problems=[issue.issue().model_dump(mode="json") for issue in issues],
|
|
470
524
|
)
|
|
471
525
|
failures += 1
|
|
472
526
|
continue
|
|
@@ -562,7 +616,7 @@ def init_command(
|
|
|
562
616
|
root, version=version, admin=admin, password=password or os.environ.get(BOOTSTRAP_PASSWORD_ENV, "")
|
|
563
617
|
)
|
|
564
618
|
if chosen is None:
|
|
565
|
-
_init_fail(
|
|
619
|
+
_init_fail(INIT_CANCELLED)
|
|
566
620
|
choices = chosen
|
|
567
621
|
else:
|
|
568
622
|
choices = InitChoices(
|
|
@@ -577,17 +631,17 @@ def init_command(
|
|
|
577
631
|
try:
|
|
578
632
|
check_choices(choices)
|
|
579
633
|
except ProjectError as error:
|
|
580
|
-
_init_fail(str(error))
|
|
634
|
+
_init_fail(INIT_REFUSED, detail=str(error))
|
|
581
635
|
if choices.stack and admin != "admin":
|
|
582
|
-
_init_fail(
|
|
636
|
+
_init_fail(ADMIN_IS_FIXED_IN_A_STACK)
|
|
583
637
|
if choices.template != "documents" and not choices.password:
|
|
584
638
|
choices = choices.model_copy(update={"password": _prompt_for_a_password(stack=choices.stack)})
|
|
585
639
|
if choices.template != "documents" and len(choices.password) < MIN_PASSWORD_LENGTH:
|
|
586
|
-
_init_fail(str(WeakPassword()))
|
|
640
|
+
_init_fail(INIT_REFUSED, detail=str(WeakPassword()))
|
|
587
641
|
try:
|
|
588
642
|
made = scaffold(directory, choices, version=version)
|
|
589
643
|
except ProjectError as error:
|
|
590
|
-
_init_fail(str(error))
|
|
644
|
+
_init_fail(INIT_REFUSED, detail=str(error))
|
|
591
645
|
left = [_within(path, directory) for path in made.skipped]
|
|
592
646
|
described = {
|
|
593
647
|
"template": choices.template,
|
|
@@ -649,16 +703,16 @@ def instance_settings(root: Path) -> Settings:
|
|
|
649
703
|
)
|
|
650
704
|
|
|
651
705
|
|
|
652
|
-
def _init_fail(message:
|
|
706
|
+
def _init_fail(message: Message, /, *, ways: Sequence[tuple[str, str]] = (), **params: Any) -> NoReturn:
|
|
653
707
|
"""Refuse an init: a record where records were asked for, plain sentences otherwise.
|
|
654
708
|
|
|
655
709
|
Each way out is a command and what it does; the record carries them as one sentence
|
|
656
710
|
each, and the rendering sets the commands in a column of their own.
|
|
657
711
|
"""
|
|
658
712
|
if json_mode():
|
|
659
|
-
refuse(message, problems=[
|
|
713
|
+
refuse(message, problems=[Issue.of(INIT_WAY, command=command, what=what) for command, what in ways], **params)
|
|
660
714
|
else:
|
|
661
|
-
error_console.print(f"[red]{escape(message)}[/]", highlight=False)
|
|
715
|
+
error_console.print(f"[red]{escape(message.render(**params))}[/]", highlight=False)
|
|
662
716
|
width = max((len(command) for command, _ in ways), default=0)
|
|
663
717
|
for command, what in ways:
|
|
664
718
|
error_console.print(f" [bold]{escape(command).ljust(width)}[/] [dim]{escape(what)}[/]", highlight=False)
|
|
@@ -674,7 +728,8 @@ def _refuse_an_existing_instance(root: Path) -> None:
|
|
|
674
728
|
existing = instance_settings(root).sqlite_path
|
|
675
729
|
if existing is not None and existing.exists():
|
|
676
730
|
_init_fail(
|
|
677
|
-
|
|
731
|
+
INSTANCE_ALREADY_HERE,
|
|
732
|
+
path=existing,
|
|
678
733
|
ways=[
|
|
679
734
|
("dg dev", "starts it"),
|
|
680
735
|
("dg db upgrade", "brings its schema forward"),
|
|
@@ -686,7 +741,7 @@ def _refuse_an_existing_instance(root: Path) -> None:
|
|
|
686
741
|
def _prompt_for_a_password(*, stack: bool = False) -> str:
|
|
687
742
|
"""Ask for the first admin's password, or say how to give it without a prompt."""
|
|
688
743
|
if not sys.stdin.isatty():
|
|
689
|
-
_init_fail(
|
|
744
|
+
_init_fail(NO_FIRST_PASSWORD, variable=BOOTSTRAP_PASSWORD_ENV)
|
|
690
745
|
asked = "password for the stack's first admin" if stack else "password for the first admin"
|
|
691
746
|
return str(typer.prompt(asked, hide_input=True, confirmation_prompt=True))
|
|
692
747
|
|
|
@@ -714,7 +769,7 @@ async def first_admin(settings: Settings, username: str, password: str) -> str:
|
|
|
714
769
|
issued = await issue_token(session, user, name="init")
|
|
715
770
|
return issued.secret.get_secret_value()
|
|
716
771
|
except AuthError as error:
|
|
717
|
-
_init_fail(str(error))
|
|
772
|
+
_init_fail(INIT_REFUSED, detail=str(error))
|
|
718
773
|
finally:
|
|
719
774
|
await engine.dispose()
|
|
720
775
|
|
|
@@ -827,9 +882,9 @@ def pipeline_validate(
|
|
|
827
882
|
"""
|
|
828
883
|
state = state_of(ctx)
|
|
829
884
|
if every and (code is not None or version is not None):
|
|
830
|
-
fail(
|
|
885
|
+
fail(ALL_TAKES_NO_CODE)
|
|
831
886
|
if not every and code is None:
|
|
832
|
-
fail(
|
|
887
|
+
fail(NAME_A_PIPELINE)
|
|
833
888
|
with client_for(state) as dg:
|
|
834
889
|
codes = (
|
|
835
890
|
[row.code for row in paged(lambda after, size: dg.call(dg.pipelines.list(after=after, limit=size)), None)]
|
|
@@ -899,7 +954,7 @@ def parse_params(
|
|
|
899
954
|
try:
|
|
900
955
|
built = build_params(schema or {}, pairs=pairs or [], files=files or [])
|
|
901
956
|
except ParamError as error:
|
|
902
|
-
fail(str(error))
|
|
957
|
+
fail(PARAMS_REFUSED, detail=str(error))
|
|
903
958
|
properties = cast("dict[str, Any]", (schema or {}).get("properties") or {})
|
|
904
959
|
return {
|
|
905
960
|
name: value
|
|
@@ -912,7 +967,7 @@ def parse_params(
|
|
|
912
967
|
WINDOW_SEPARATOR: Final = ".."
|
|
913
968
|
|
|
914
969
|
#: The grammar, said once, so every refusal says the same thing.
|
|
915
|
-
WINDOW_GRAMMAR: Final =
|
|
970
|
+
WINDOW_GRAMMAR: Final = WINDOW_GRAMMAR_MESSAGE.render()
|
|
916
971
|
|
|
917
972
|
|
|
918
973
|
def read_instant(value: str, flag: str) -> datetime:
|
|
@@ -940,7 +995,7 @@ def read_window(value: str) -> tuple[datetime, datetime]:
|
|
|
940
995
|
"""Read a ``START..END`` window, or say what the grammar is."""
|
|
941
996
|
start_text, separator, end_text = value.partition(WINDOW_SEPARATOR)
|
|
942
997
|
if not separator:
|
|
943
|
-
raise ParamError(
|
|
998
|
+
raise ParamError(NOT_A_WINDOW.render(grammar=WINDOW_GRAMMAR, value=repr(value)))
|
|
944
999
|
start = read_instant(start_text, "--window")
|
|
945
1000
|
end = read_instant(end_text, "--window")
|
|
946
1001
|
check_interval(start, end)
|
|
@@ -954,7 +1009,7 @@ def parse_window(value: str | None) -> tuple[datetime, datetime] | None:
|
|
|
954
1009
|
try:
|
|
955
1010
|
return read_window(value)
|
|
956
1011
|
except ParamError as error:
|
|
957
|
-
fail(str(error))
|
|
1012
|
+
fail(PARAMS_REFUSED, detail=str(error))
|
|
958
1013
|
|
|
959
1014
|
|
|
960
1015
|
def parse_log_levels(values: list[str] | None) -> dict[str, LogLevel] | None:
|
|
@@ -971,12 +1026,12 @@ def parse_log_levels(values: list[str] | None) -> dict[str, LogLevel] | None:
|
|
|
971
1026
|
if not named:
|
|
972
1027
|
pattern, named = "*", value
|
|
973
1028
|
if not pattern:
|
|
974
|
-
fail(
|
|
1029
|
+
fail(LOG_LEVEL_NO_PATTERN, value=repr(value))
|
|
975
1030
|
try:
|
|
976
1031
|
levels[pattern] = LogLevel(named.lower())
|
|
977
1032
|
except ValueError:
|
|
978
1033
|
allowed = ", ".join(level.value for level in LogLevel)
|
|
979
|
-
fail(
|
|
1034
|
+
fail(LOG_LEVEL_NOT_A_LEVEL, value=repr(value), named=repr(named), allowed=allowed)
|
|
980
1035
|
return levels
|
|
981
1036
|
|
|
982
1037
|
|
|
@@ -987,7 +1042,11 @@ def parse_priority(value: str | None) -> RunPriority | None:
|
|
|
987
1042
|
try:
|
|
988
1043
|
return RunPriority(value.lower())
|
|
989
1044
|
except ValueError:
|
|
990
|
-
fail(
|
|
1045
|
+
fail(
|
|
1046
|
+
NOT_A_PRIORITY,
|
|
1047
|
+
value=repr(value),
|
|
1048
|
+
allowed=", ".join(priority.value for priority in RunPriority),
|
|
1049
|
+
)
|
|
991
1050
|
|
|
992
1051
|
|
|
993
1052
|
def run_command(
|
|
@@ -1068,16 +1127,16 @@ def run_command(
|
|
|
1068
1127
|
"""
|
|
1069
1128
|
state = state_of(ctx)
|
|
1070
1129
|
if strict and not (watch or local):
|
|
1071
|
-
fail(
|
|
1130
|
+
fail(STRICT_NEEDS_WATCH)
|
|
1072
1131
|
if keep and not local:
|
|
1073
|
-
fail(
|
|
1132
|
+
fail(KEEP_IS_LOCAL_ONLY)
|
|
1074
1133
|
if root is not None and not local:
|
|
1075
|
-
fail(
|
|
1134
|
+
fail(ROOT_IS_LOCAL_ONLY)
|
|
1076
1135
|
covered = parse_window(window)
|
|
1077
1136
|
levels = parse_log_levels(log_level)
|
|
1078
1137
|
wanted = parse_priority(priority)
|
|
1079
1138
|
if wanted is not None and local:
|
|
1080
|
-
fail(
|
|
1139
|
+
fail(PRIORITY_IS_NOT_LOCAL)
|
|
1081
1140
|
if local:
|
|
1082
1141
|
_run_locally(
|
|
1083
1142
|
state,
|
|
@@ -1103,15 +1162,16 @@ def run_command(
|
|
|
1103
1162
|
document = read_document(target)
|
|
1104
1163
|
schema = dict(load_pipeline_text(document.text).params)
|
|
1105
1164
|
except (SourceError, DocumentError) as error:
|
|
1106
|
-
fail(str(error))
|
|
1165
|
+
fail(SOURCE_REFUSED, detail=str(error))
|
|
1107
1166
|
result = _apply_one(dg, document, code=as_code, dry_run=False)
|
|
1108
1167
|
if not _print_plan(result, document.label):
|
|
1109
1168
|
if state.json_output:
|
|
1110
1169
|
emit_problem(
|
|
1111
|
-
|
|
1170
|
+
DOCUMENT_INVALID,
|
|
1112
1171
|
status=422,
|
|
1113
1172
|
title="Unprocessable Content",
|
|
1114
|
-
problems=[
|
|
1173
|
+
problems=[issue.issue() for issue in result.plan.issues],
|
|
1174
|
+
code=result.plan.code,
|
|
1115
1175
|
)
|
|
1116
1176
|
raise typer.Exit(code=1)
|
|
1117
1177
|
code = result.plan.code
|
|
@@ -1122,9 +1182,14 @@ def run_command(
|
|
|
1122
1182
|
accepted = dg.call(dg.pipelines.run(code, params=params, window=covered, log_levels=levels, priority=wanted))
|
|
1123
1183
|
if accepted.run_id is None:
|
|
1124
1184
|
if state.json_output:
|
|
1125
|
-
emit_problem(
|
|
1185
|
+
emit_problem(
|
|
1186
|
+
RUN_SKIPPED,
|
|
1187
|
+
status=409,
|
|
1188
|
+
title="Skipped",
|
|
1189
|
+
detail=accepted.detail or CONCURRENCY_REFUSED.render(),
|
|
1190
|
+
)
|
|
1126
1191
|
return
|
|
1127
|
-
console.print(f"[yellow]skipped[/] {accepted.detail or
|
|
1192
|
+
console.print(f"[yellow]skipped[/] {accepted.detail or CONCURRENCY_REFUSED.render()}")
|
|
1128
1193
|
return
|
|
1129
1194
|
sink(state).event(
|
|
1130
1195
|
"run",
|
|
@@ -1170,7 +1235,7 @@ def backfill_command(
|
|
|
1170
1235
|
end = read_instant(to, "--to")
|
|
1171
1236
|
check_interval(start, end)
|
|
1172
1237
|
except ParamError as error:
|
|
1173
|
-
fail(str(error))
|
|
1238
|
+
fail(PARAMS_REFUSED, detail=str(error))
|
|
1174
1239
|
with client_for(state) as dg:
|
|
1175
1240
|
accepted = dg.call(dg.pipelines.backfill(pipeline, schedule=schedule, start=start, end=end, dry_run=dry_run))
|
|
1176
1241
|
_report_backfill(accepted, state=state)
|
|
@@ -1208,9 +1273,9 @@ def _run_locally(
|
|
|
1208
1273
|
) -> None:
|
|
1209
1274
|
"""Apply and run a document in a throwaway instance, streaming what it logs."""
|
|
1210
1275
|
if state.url is not None or state.profile is not None or state.token is not None:
|
|
1211
|
-
fail(
|
|
1276
|
+
fail(LOCAL_TAKES_NO_SERVER)
|
|
1212
1277
|
if not looks_like_a_document(target):
|
|
1213
|
-
fail(
|
|
1278
|
+
fail(LOCAL_TAKES_A_DOCUMENT, target=repr(target))
|
|
1214
1279
|
try:
|
|
1215
1280
|
document = read_document(target)
|
|
1216
1281
|
specs: list[ConnectionSpec] = load_connection_specs(connections) if connections else []
|
|
@@ -1218,7 +1283,7 @@ def _run_locally(
|
|
|
1218
1283
|
supporting = [read_document(str(path)).text for path in also_apply]
|
|
1219
1284
|
schema = dict(_runnable(document.text).params)
|
|
1220
1285
|
except (SourceError, LocalError, DocumentError) as error:
|
|
1221
|
-
fail(str(error))
|
|
1286
|
+
fail(SOURCE_REFUSED, detail=str(error))
|
|
1222
1287
|
params = parse_params(pairs, schema=schema, files=params_files)
|
|
1223
1288
|
settings = _local_settings(enable_unsafe)
|
|
1224
1289
|
outcome = asyncio.run(
|
|
@@ -1237,7 +1302,7 @@ def _run_locally(
|
|
|
1237
1302
|
)
|
|
1238
1303
|
)
|
|
1239
1304
|
if outcome is None: # pragma: no cover - the generator always ends with an outcome
|
|
1240
|
-
fail(
|
|
1305
|
+
fail(LOCAL_RUN_HAD_NO_OUTCOME)
|
|
1241
1306
|
_report_outcome(outcome, strict=strict, state=state)
|
|
1242
1307
|
|
|
1243
1308
|
|
|
@@ -1313,13 +1378,13 @@ async def _stream_local(
|
|
|
1313
1378
|
except LocalError as error:
|
|
1314
1379
|
remedies = (
|
|
1315
1380
|
[
|
|
1316
|
-
|
|
1317
|
-
|
|
1381
|
+
Issue.of(UNSAFE_FOR_THIS_RUN),
|
|
1382
|
+
Issue.of(UNSAFE_FOR_THE_INSTANCE),
|
|
1318
1383
|
]
|
|
1319
1384
|
if "DIRIGENT_ENABLED_UNSAFE_BLOCKS" in str(error)
|
|
1320
1385
|
else []
|
|
1321
1386
|
)
|
|
1322
|
-
refuse(
|
|
1387
|
+
refuse(GUARD_REFUSED, status=GUARD_EXIT, title="Refused", problems=remedies, detail=str(error))
|
|
1323
1388
|
raise typer.Exit(code=GUARD_EXIT) from error
|
|
1324
1389
|
return outcome
|
|
1325
1390
|
|
|
@@ -1777,7 +1842,7 @@ def runs_list(
|
|
|
1777
1842
|
) -> None:
|
|
1778
1843
|
"""List runs, newest first, or the ones whose pipeline wears every tag named."""
|
|
1779
1844
|
if status is not None and status not in set(RunStatus):
|
|
1780
|
-
fail(
|
|
1845
|
+
fail(NOT_A_RUN_STATUS, value=repr(status), allowed=", ".join(sorted(RunStatus)))
|
|
1781
1846
|
tags = tag or []
|
|
1782
1847
|
with client_for(state_of(ctx)) as dg:
|
|
1783
1848
|
rows = list(
|
|
@@ -1936,7 +2001,7 @@ def runs_retry(
|
|
|
1936
2001
|
with client_for(state_of(ctx)) as dg:
|
|
1937
2002
|
candidates = [row for row in run_attempts(dg, run_id, step=step) if row.status in RETRYABLE]
|
|
1938
2003
|
if not candidates:
|
|
1939
|
-
fail(
|
|
2004
|
+
fail(NO_FAILURE_TO_RETRY, run_id=run_id, step=repr(step))
|
|
1940
2005
|
retried = candidates if failed_items else candidates[-1:]
|
|
1941
2006
|
labels = item_labels(run_items(dg, run_id), retried)
|
|
1942
2007
|
for attempt in retried:
|
|
@@ -2005,7 +2070,7 @@ def runs_report(
|
|
|
2005
2070
|
if markdown:
|
|
2006
2071
|
document = dg.call(dg.runs.report_document(run_id))
|
|
2007
2072
|
if document is None:
|
|
2008
|
-
fail(
|
|
2073
|
+
fail(NO_REPORT_DOCUMENT, run_id=run_id)
|
|
2009
2074
|
if state.json_output:
|
|
2010
2075
|
return emit_fact("run.report_document", message="rendered", run_id=str(run_id), document=document)
|
|
2011
2076
|
console.print(document, end="", highlight=False, markup=False)
|
|
@@ -2123,7 +2188,7 @@ def blocks_new(
|
|
|
2123
2188
|
try:
|
|
2124
2189
|
written = scaffold_pack(directory.resolve(), name)
|
|
2125
2190
|
except ScaffoldError as error:
|
|
2126
|
-
fail(str(error))
|
|
2191
|
+
fail(SCAFFOLD_REFUSED, detail=str(error))
|
|
2127
2192
|
root = directory.resolve() / f"dirigent-{name}"
|
|
2128
2193
|
if state_of(ctx).json_output:
|
|
2129
2194
|
for path in written:
|
|
@@ -2143,7 +2208,7 @@ def blocks_list(
|
|
|
2143
2208
|
) -> None:
|
|
2144
2209
|
"""List the blocks this instance can run."""
|
|
2145
2210
|
if kind is not None and kind not in set(BlockKind):
|
|
2146
|
-
fail(
|
|
2211
|
+
fail(NOT_A_BLOCK_KIND, kind=repr(kind), allowed=", ".join(sorted(BlockKind)))
|
|
2147
2212
|
with client_for(state_of(ctx)) as dg:
|
|
2148
2213
|
catalog = dg.call(dg.blocks.catalog(kind=BlockKind(kind) if kind else None))
|
|
2149
2214
|
if state_of(ctx).json_output:
|
|
@@ -2260,7 +2325,7 @@ def connection_create(
|
|
|
2260
2325
|
# refusing the invocation for declining it would make the bot-token form of a
|
|
2261
2326
|
# slack connection unreachable from any script.
|
|
2262
2327
|
if json_mode() and field in required:
|
|
2263
|
-
fail(
|
|
2328
|
+
fail(CANNOT_PROMPT_SET, field=field)
|
|
2264
2329
|
if not interactive:
|
|
2265
2330
|
continue
|
|
2266
2331
|
if _is_secret_field(body):
|
|
@@ -2289,7 +2354,7 @@ def _connection_schema(catalog: Catalog, kind_id: str) -> dict[str, Any]:
|
|
|
2289
2354
|
if entry.id == kind_id:
|
|
2290
2355
|
return entry.config_schema
|
|
2291
2356
|
known = ", ".join(entry.id for entry in catalog.connection_kinds) or "none"
|
|
2292
|
-
fail(
|
|
2357
|
+
fail(UNKNOWN_CONNECTION_KIND, kind=repr(kind_id), known=known)
|
|
2293
2358
|
|
|
2294
2359
|
|
|
2295
2360
|
def _is_secret_field(body: dict[str, Any]) -> bool:
|
|
@@ -2378,13 +2443,13 @@ def schema_create(
|
|
|
2378
2443
|
try:
|
|
2379
2444
|
document = read_document(reference)
|
|
2380
2445
|
except SourceError as error:
|
|
2381
|
-
fail(str(error))
|
|
2446
|
+
fail(SOURCE_REFUSED, detail=str(error))
|
|
2382
2447
|
try:
|
|
2383
2448
|
body = yaml.safe_load(document.text)
|
|
2384
2449
|
except yaml.YAMLError as error:
|
|
2385
|
-
fail(
|
|
2450
|
+
fail(SCHEMA_UNREADABLE, label=document.label, detail=str(error))
|
|
2386
2451
|
if not isinstance(body, dict):
|
|
2387
|
-
fail(
|
|
2452
|
+
fail(SCHEMA_NOT_AN_OBJECT, label=document.label, kind=type(body).__name__)
|
|
2388
2453
|
schema = cast("JsonMap", body)
|
|
2389
2454
|
resolved = code
|
|
2390
2455
|
if resolved is None and not (isinstance(schema.get("$id"), str) and code_from_id(cast("str", schema["$id"]))):
|
|
@@ -2652,9 +2717,10 @@ def auth_status(ctx: typer.Context) -> None:
|
|
|
2652
2717
|
state = state_of(ctx)
|
|
2653
2718
|
if state.resolved.token is None:
|
|
2654
2719
|
refuse(
|
|
2655
|
-
|
|
2720
|
+
NOT_AUTHENTICATED,
|
|
2656
2721
|
title="Not authenticated",
|
|
2657
|
-
problems=[
|
|
2722
|
+
problems=[Issue.of(SET_DG_TOKEN), Issue.of(OR_A_PROFILE_TOKEN), Issue.of(OR_DG_AUTH_LOGIN)],
|
|
2723
|
+
url=state.resolved.url,
|
|
2658
2724
|
)
|
|
2659
2725
|
raise typer.Exit(code=1)
|
|
2660
2726
|
with client_for(state) as dg:
|
|
@@ -2723,7 +2789,7 @@ def _example(state: CliState, code: str, *, local: bool) -> ExampleDetail:
|
|
|
2723
2789
|
try:
|
|
2724
2790
|
return as_detail(load_plugin_host().example(code))
|
|
2725
2791
|
except UnknownExample as error:
|
|
2726
|
-
refuse(
|
|
2792
|
+
refuse(NO_SUCH_EXAMPLE, title="No such example", detail=str(error))
|
|
2727
2793
|
raise typer.Exit(code=1) from error
|
|
2728
2794
|
|
|
2729
2795
|
|
|
@@ -2814,15 +2880,16 @@ def pipeline_new(
|
|
|
2814
2880
|
entry = _example(state_of(ctx), starter, local=local)
|
|
2815
2881
|
if not entry.starter:
|
|
2816
2882
|
refuse(
|
|
2817
|
-
|
|
2883
|
+
NOT_A_STARTER,
|
|
2818
2884
|
title="Not a starter",
|
|
2819
|
-
problems=[
|
|
2885
|
+
problems=[Issue.of(STARTER_TAG_NEEDED, tag=repr(STARTER_TAG))],
|
|
2886
|
+
code=repr(entry.code),
|
|
2820
2887
|
)
|
|
2821
2888
|
raise typer.Exit(code=1)
|
|
2822
2889
|
new_code = code or entry.code
|
|
2823
2890
|
path = directory / f"{new_code}.yaml"
|
|
2824
2891
|
if path.exists():
|
|
2825
|
-
refuse(
|
|
2892
|
+
refuse(ALREADY_THERE, title="Already there", problems=[Issue.of(NAME_ANOTHER_CODE)], path=path)
|
|
2826
2893
|
raise typer.Exit(code=1)
|
|
2827
2894
|
text = starters.instantiate(entry.source, new_code)
|
|
2828
2895
|
directory.mkdir(parents=True, exist_ok=True)
|