dirigent-cli 0.17.2__tar.gz → 0.18.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.17.2 → dirigent_cli-0.18.0}/PKG-INFO +9 -9
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/pyproject.toml +9 -9
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/pyproject.toml.orig +9 -9
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/commands.py +5 -4
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/init_form.py +1 -1
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/main.py +6 -5
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/project.py +1 -1
- dirigent_cli-0.18.0/src/dirigent_cli/starters.py +293 -0
- dirigent_cli-0.17.2/src/dirigent_cli/starters.py +0 -121
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/LICENSE +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/README.md +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/__init__.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/aliases.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/context.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/formatters.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/graph.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/health.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/local.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/messages.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/output.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/params.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/profiles.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/py.typed +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/reaper.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/scaffold.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/schemas.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/seeding.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/sources.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/stream.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/summaries.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/timing.py +0 -0
- {dirigent_cli-0.17.2 → dirigent_cli-0.18.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.18.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.18.0
|
|
10
|
+
Requires-Dist: dirigent-blocks==0.18.0
|
|
11
|
+
Requires-Dist: dirigent-client==0.18.0
|
|
12
|
+
Requires-Dist: dirigent-common==0.18.0
|
|
13
|
+
Requires-Dist: dirigent-core==0.18.0
|
|
14
|
+
Requires-Dist: dirigent-examples==0.18.0
|
|
15
|
+
Requires-Dist: dirigent-plugin==0.18.0
|
|
16
|
+
Requires-Dist: dirigent-server==0.18.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.18.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.18.0",
|
|
15
|
+
"dirigent-blocks==0.18.0",
|
|
16
|
+
"dirigent-client==0.18.0",
|
|
17
|
+
"dirigent-common==0.18.0",
|
|
18
|
+
"dirigent-core==0.18.0",
|
|
19
|
+
"dirigent-examples==0.18.0",
|
|
20
|
+
"dirigent-plugin==0.18.0",
|
|
21
|
+
"dirigent-server==0.18.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.18.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.18.0",
|
|
15
|
+
"dirigent-blocks==0.18.0",
|
|
16
|
+
"dirigent-client==0.18.0",
|
|
17
|
+
"dirigent-common==0.18.0",
|
|
18
|
+
"dirigent-core==0.18.0",
|
|
19
|
+
"dirigent-examples==0.18.0",
|
|
20
|
+
"dirigent-plugin==0.18.0",
|
|
21
|
+
"dirigent-server==0.18.0",
|
|
22
22
|
"httpx2>=2.12.0",
|
|
23
23
|
"python-dotenv>=1.1.0",
|
|
24
24
|
"pyyaml>=6.0.3",
|
|
@@ -1838,7 +1838,7 @@ def runs_list(
|
|
|
1838
1838
|
list[str] | None,
|
|
1839
1839
|
typer.Option("--tag", help="Only runs whose pipeline wears this tag; repeat it to name more."),
|
|
1840
1840
|
] = None,
|
|
1841
|
-
limit: Annotated[int, typer.Option(help="How many runs at most;
|
|
1841
|
+
limit: Annotated[int, typer.Option(help=f"How many runs at most; {RUNS_PAGE} when omitted.")] = RUNS_PAGE,
|
|
1842
1842
|
) -> None:
|
|
1843
1843
|
"""List runs, newest first, or the ones whose pipeline wears every tag named."""
|
|
1844
1844
|
if status is not None and status not in set(RunStatus):
|
|
@@ -2873,8 +2873,9 @@ def pipeline_new(
|
|
|
2873
2873
|
) -> None:
|
|
2874
2874
|
"""Copy a starter into this project as a pipeline of its own.
|
|
2875
2875
|
|
|
2876
|
-
The copy is the starter's text verbatim, with the top-level `code:` rewritten
|
|
2877
|
-
`starter` tag dropped,
|
|
2876
|
+
The copy is the starter's text verbatim, with the top-level `code:` rewritten, the
|
|
2877
|
+
`starter` tag dropped, and the connections and schemas the original carried named under
|
|
2878
|
+
`requires:` instead, so every teaching comment in it survives. What it needs from the
|
|
2878
2879
|
instance is the copy's own `requires`, which this prints as the list to work through.
|
|
2879
2880
|
"""
|
|
2880
2881
|
entry = _example(state_of(ctx), starter, local=local)
|
|
@@ -2902,5 +2903,5 @@ def pipeline_new(
|
|
|
2902
2903
|
code=new_code,
|
|
2903
2904
|
starter=entry.code,
|
|
2904
2905
|
requires=copied.model_dump(mode="json"),
|
|
2905
|
-
preflight=starters.preflight(copied),
|
|
2906
|
+
preflight=starters.preflight(copied, starters.carried_kinds(entry.source)),
|
|
2906
2907
|
)
|
|
@@ -244,7 +244,7 @@ class InitForm(App[InitChoices | None]):
|
|
|
244
244
|
elif choices.instance:
|
|
245
245
|
files += [".env", ".dirigent/state/"]
|
|
246
246
|
if choices.workflow:
|
|
247
|
-
files.append(".github/workflows/dirigent.
|
|
247
|
+
files.append(".github/workflows/dirigent.yaml")
|
|
248
248
|
self.query_one("#files", Label).update("Will write: " + ", ".join(files))
|
|
249
249
|
|
|
250
250
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -51,7 +51,7 @@ from dirigent_client.enums import UserRole
|
|
|
51
51
|
from dirigent_common import Issue
|
|
52
52
|
from dirigent_core import migrations
|
|
53
53
|
from dirigent_core.config import STATE_DIR, Settings, get_settings, redacted_url, reset_settings_cache
|
|
54
|
-
from dirigent_core.logging import
|
|
54
|
+
from dirigent_core.logging import configure_logging, silence_stdout
|
|
55
55
|
from dirigent_core.protocol import FORMATS, Format, Record, make
|
|
56
56
|
from dirigent_core.telemetry import configure_telemetry
|
|
57
57
|
|
|
@@ -194,14 +194,15 @@ def main_callback(
|
|
|
194
194
|
|
|
195
195
|
|
|
196
196
|
def resolve_output(named: str | None, *, json_output: bool) -> Format:
|
|
197
|
-
"""Resolve the output: the flag, then the
|
|
197
|
+
"""Resolve the output: the flag, then the ``log_format`` setting, then the terminal.
|
|
198
198
|
|
|
199
|
-
``--json`` is the same request as ``--output json
|
|
200
|
-
|
|
199
|
+
``--json`` is the same request as ``--output json``. ``log_format`` is where a container,
|
|
200
|
+
a project's ``.env`` or its ``dirigent.yaml`` says it once, read through the same layers
|
|
201
|
+
every other setting is. Unasked, a terminal gets the rendering and anything else gets
|
|
201
202
|
NDJSON: a pipe, a container's log, an agent's shell and CI are never terminals, so a
|
|
202
203
|
script reads records without asking, and a person reads lines without asking.
|
|
203
204
|
"""
|
|
204
|
-
chosen = named or ("json" if json_output else None) or
|
|
205
|
+
chosen = named or ("json" if json_output else None) or get_settings().log_format
|
|
205
206
|
if chosen is None:
|
|
206
207
|
return "console" if sys.stdout.isatty() else "json"
|
|
207
208
|
resolved = chosen.lower()
|
|
@@ -919,7 +919,7 @@ def scaffold(directory: Path, choices: InitChoices, *, version: str = "0.0.0") -
|
|
|
919
919
|
_record(directory / "README.md", readme, written, skipped)
|
|
920
920
|
_merge_ignore(directory / ".gitignore", ROOT_IGNORE_TEMPLATE, written, skipped)
|
|
921
921
|
if choices.workflow:
|
|
922
|
-
written.append(_write(directory / ".github" / "workflows" / "dirigent.
|
|
922
|
+
written.append(_write(directory / ".github" / "workflows" / "dirigent.yaml", WORKFLOW_TEMPLATE))
|
|
923
923
|
if choices.stack:
|
|
924
924
|
written.append(_write(directory / "compose.yaml", compose_document(choices, version)))
|
|
925
925
|
written.append(_write(directory / "Dockerfile", dockerfile_document(choices, version)))
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Copying a starter: the lines a copy rewrites, and nothing else.
|
|
2
|
+
|
|
3
|
+
A starter is a plain ``dirigent/v1`` document, not a template, so instantiating one is a
|
|
4
|
+
verbatim copy of its text with the top-level ``code:`` changed, ``starter`` taken off the
|
|
5
|
+
top-level ``tags:``, and every section the document carried named under ``requires:``
|
|
6
|
+
instead. A document carries its connections and its schemas so that it runs alone under
|
|
7
|
+
``dg run --local``, and an instance refuses to store one that does, so a copy names them.
|
|
8
|
+
The edit is at text level rather than through a parser because the teaching comments, the
|
|
9
|
+
blank lines and the quoting are the point of copying a document instead of generating one.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Mapping, Sequence
|
|
14
|
+
from typing import Final, cast
|
|
15
|
+
|
|
16
|
+
from dirigent_client import Requirements
|
|
17
|
+
from dirigent_common import JsonMap
|
|
18
|
+
from dirigent_core.documents import CARRIED, safe_load
|
|
19
|
+
from dirigent_core.examples import STARTER_TAG
|
|
20
|
+
|
|
21
|
+
#: The top-level ``code:`` line: no indentation, so a step's own ``code`` is never touched.
|
|
22
|
+
_CODE = re.compile(r"^code:[^\S\n]*(.*)$", re.MULTILINE)
|
|
23
|
+
|
|
24
|
+
#: The top-level ``tags:`` line, and whatever it carries on the same line.
|
|
25
|
+
_TAGS = re.compile(r"^tags:[^\S\n]*(.*)$", re.MULTILINE)
|
|
26
|
+
|
|
27
|
+
#: One entry of a block list under ``tags:``: two spaces, a dash, the value.
|
|
28
|
+
_TAG_ITEM = re.compile(r"^[^\S\n]*-[^\S\n]*(\S.*?)[^\S\n]*$")
|
|
29
|
+
|
|
30
|
+
#: A mapping key on its own line: its indentation, its name, and what follows the colon.
|
|
31
|
+
_KEY = re.compile(r"^([^\S\n]*)([^\s#][^:]*):[^\S\n]*(.*)$")
|
|
32
|
+
|
|
33
|
+
#: One entry of an indented block list: its indentation, and the value.
|
|
34
|
+
_ITEM = re.compile(r"^([^\S\n]+)-[^\S\n]+(\S.*?)[^\S\n]*$")
|
|
35
|
+
|
|
36
|
+
#: Where a ``requires:`` section goes in a document that has none: after the first of these.
|
|
37
|
+
_ANCHORS: Final = ("tags", "description", "code")
|
|
38
|
+
|
|
39
|
+
_FLOW: Final = ("[", "]")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def instantiate(source: str, code: str) -> str:
|
|
43
|
+
"""Copy a starter's text under a new code, naming what the original carried."""
|
|
44
|
+
return _uncarry(_retag(_recode(source, code)))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _recode(source: str, code: str) -> str:
|
|
48
|
+
"""Rewrite the one top-level ``code:`` line, leaving every other line alone."""
|
|
49
|
+
return _CODE.sub(lambda _: f"code: {code}", source, count=1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _retag(source: str) -> str:
|
|
53
|
+
"""Take ``starter`` off the top-level ``tags:``, whichever shape the list is written in.
|
|
54
|
+
|
|
55
|
+
A document whose only tag was ``starter`` loses the whole ``tags:`` entry: an empty list
|
|
56
|
+
says less than no list at all.
|
|
57
|
+
"""
|
|
58
|
+
found = _TAGS.search(source)
|
|
59
|
+
if found is None:
|
|
60
|
+
return source
|
|
61
|
+
rest = found.group(1).strip()
|
|
62
|
+
if rest.startswith(_FLOW[0]):
|
|
63
|
+
return _rewrite_flow(source, found.start(), found.end(), rest)
|
|
64
|
+
if rest:
|
|
65
|
+
return source
|
|
66
|
+
return _rewrite_block(source, found.start(), found.end())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _rewrite_flow(source: str, start: int, end: int, rest: str) -> str:
|
|
70
|
+
"""Rewrite ``tags: [a, b, starter]``, dropping the entry when nothing is left."""
|
|
71
|
+
inner = rest.removeprefix(_FLOW[0]).removesuffix(_FLOW[1])
|
|
72
|
+
kept = [tag for tag in (one.strip() for one in inner.split(",")) if tag and tag != STARTER_TAG]
|
|
73
|
+
if not kept:
|
|
74
|
+
return _drop_line(source, start, end)
|
|
75
|
+
return source[:start] + f"tags: [{', '.join(kept)}]" + source[end:]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _rewrite_block(source: str, start: int, end: int) -> str:
|
|
79
|
+
"""Rewrite a block list under ``tags:``, dropping the whole entry when nothing is left."""
|
|
80
|
+
lines = source[end:].split("\n")
|
|
81
|
+
items: list[tuple[int, str]] = []
|
|
82
|
+
for index, line in enumerate(lines):
|
|
83
|
+
if index == 0 and not line.strip():
|
|
84
|
+
continue
|
|
85
|
+
matched = _TAG_ITEM.match(line)
|
|
86
|
+
if matched is None:
|
|
87
|
+
break
|
|
88
|
+
items.append((index, matched.group(1)))
|
|
89
|
+
if not items:
|
|
90
|
+
return source
|
|
91
|
+
dropped = [index for index, tag in items if tag == STARTER_TAG]
|
|
92
|
+
if not dropped:
|
|
93
|
+
return source
|
|
94
|
+
if len(dropped) == len(items):
|
|
95
|
+
return _drop_line(source, start, end + len("\n".join(lines[: items[-1][0] + 1])))
|
|
96
|
+
kept = [line for index, line in enumerate(lines) if index not in dropped]
|
|
97
|
+
return source[:end] + "\n".join(kept)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _drop_line(source: str, start: int, end: int) -> str:
|
|
101
|
+
"""Remove a whole entry, including the newline that ended it."""
|
|
102
|
+
tail = source[end:]
|
|
103
|
+
return source[:start] + tail.removeprefix("\n")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _uncarry(source: str) -> str:
|
|
107
|
+
"""Take each carried section out and name the codes it held under ``requires:``."""
|
|
108
|
+
for section in CARRIED:
|
|
109
|
+
codes = _carried(source, section)
|
|
110
|
+
if not codes:
|
|
111
|
+
continue
|
|
112
|
+
source = _require(_strip(source, section), section, codes)
|
|
113
|
+
return source
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _top(lines: list[str], name: str) -> int | None:
|
|
117
|
+
"""Where a top-level key sits, or ``None`` when the document has no such section."""
|
|
118
|
+
for index, line in enumerate(lines):
|
|
119
|
+
matched = _KEY.match(line)
|
|
120
|
+
if matched is not None and not matched.group(1) and matched.group(2) == name:
|
|
121
|
+
return index
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _extent(lines: list[str], at: int) -> tuple[int, int, int]:
|
|
126
|
+
"""What a top-level key owns: its comment run, its last line, and where it stops.
|
|
127
|
+
|
|
128
|
+
The first index is the comment run written immediately above the key, the second is the
|
|
129
|
+
key's last indented line, and the third is the first line it does not own -- the next
|
|
130
|
+
key, or the comment run written above that key.
|
|
131
|
+
"""
|
|
132
|
+
start = at
|
|
133
|
+
while start > 0 and lines[start - 1].startswith("#"):
|
|
134
|
+
start -= 1
|
|
135
|
+
last = at
|
|
136
|
+
end = at + 1
|
|
137
|
+
while end < len(lines) and (not lines[end].strip() or lines[end][:1].isspace()):
|
|
138
|
+
if lines[end].strip():
|
|
139
|
+
last = end
|
|
140
|
+
end += 1
|
|
141
|
+
return start, last, end
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _carried(source: str, section: str) -> list[str]:
|
|
145
|
+
"""The codes a carried section holds, read from the keys one level under it."""
|
|
146
|
+
lines = source.split("\n")
|
|
147
|
+
at = _top(lines, section)
|
|
148
|
+
if at is None:
|
|
149
|
+
return []
|
|
150
|
+
_, last, _ = _extent(lines, at)
|
|
151
|
+
codes: list[str] = []
|
|
152
|
+
indent: str | None = None
|
|
153
|
+
for line in lines[at + 1 : last + 1]:
|
|
154
|
+
matched = _KEY.match(line)
|
|
155
|
+
if matched is None:
|
|
156
|
+
continue
|
|
157
|
+
if indent is None:
|
|
158
|
+
indent = matched.group(1)
|
|
159
|
+
if matched.group(1) == indent:
|
|
160
|
+
codes.append(matched.group(2).strip())
|
|
161
|
+
return codes
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _strip(source: str, section: str) -> str:
|
|
165
|
+
"""Remove a whole top-level section, the comment lines written above it included."""
|
|
166
|
+
lines = source.split("\n")
|
|
167
|
+
at = _top(lines, section)
|
|
168
|
+
if at is None:
|
|
169
|
+
return source
|
|
170
|
+
start, _, end = _extent(lines, at)
|
|
171
|
+
return "\n".join(lines[:start] + lines[end:])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _require(source: str, section: str, codes: Sequence[str]) -> str:
|
|
175
|
+
"""Name each code under ``requires:``, extending the list there or writing the section."""
|
|
176
|
+
lines = source.split("\n")
|
|
177
|
+
at = _top(lines, "requires")
|
|
178
|
+
if at is None:
|
|
179
|
+
return _write_requires(lines, section, codes)
|
|
180
|
+
_, last, _ = _extent(lines, at)
|
|
181
|
+
indent = _indent(lines, at, last)
|
|
182
|
+
for index in range(at + 1, last + 1):
|
|
183
|
+
matched = _KEY.match(lines[index])
|
|
184
|
+
if matched is not None and matched.group(1) == indent and matched.group(2).strip() == section:
|
|
185
|
+
return _extend(lines, index, section, codes)
|
|
186
|
+
return "\n".join(lines[: last + 1] + _entry(indent, section, codes) + lines[last + 1 :])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _indent(lines: list[str], at: int, last: int) -> str:
|
|
190
|
+
"""The indentation the keys under a section are written at, two spaces when it has none."""
|
|
191
|
+
for line in lines[at + 1 : last + 1]:
|
|
192
|
+
matched = _KEY.match(line)
|
|
193
|
+
if matched is not None:
|
|
194
|
+
return matched.group(1)
|
|
195
|
+
return " "
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _entry(indent: str, section: str, codes: Sequence[str]) -> list[str]:
|
|
199
|
+
"""A section under ``requires:``, written as a block list of the codes it names."""
|
|
200
|
+
return [f"{indent}{section}:", *(f"{indent}{indent}- {code}" for code in codes)]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _extend(lines: list[str], at: int, section: str, codes: Sequence[str]) -> str:
|
|
204
|
+
"""Add every code that is not already there to a list under ``requires:``."""
|
|
205
|
+
matched = _KEY.match(lines[at])
|
|
206
|
+
indent = matched.group(1) if matched is not None else " "
|
|
207
|
+
rest = matched.group(3).strip() if matched is not None else ""
|
|
208
|
+
if rest.startswith(_FLOW[0]):
|
|
209
|
+
inner = rest.removeprefix(_FLOW[0]).removesuffix(_FLOW[1])
|
|
210
|
+
held = [one.strip() for one in inner.split(",") if one.strip()]
|
|
211
|
+
listed = held + [code for code in codes if code not in held]
|
|
212
|
+
lines[at] = f"{indent}{section}: [{', '.join(listed)}]"
|
|
213
|
+
return "\n".join(lines)
|
|
214
|
+
if rest:
|
|
215
|
+
return "\n".join(lines)
|
|
216
|
+
items: list[tuple[int, str, str]] = []
|
|
217
|
+
for index in range(at + 1, len(lines)):
|
|
218
|
+
found = _ITEM.match(lines[index])
|
|
219
|
+
if found is None:
|
|
220
|
+
break
|
|
221
|
+
items.append((index, found.group(1), found.group(2)))
|
|
222
|
+
held = [value for _, _, value in items]
|
|
223
|
+
item_indent = items[0][1] if items else indent + indent
|
|
224
|
+
written = [f"{item_indent}- {code}" for code in codes if code not in held]
|
|
225
|
+
after = items[-1][0] + 1 if items else at + 1
|
|
226
|
+
return "\n".join(lines[:after] + written + lines[after:])
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _write_requires(lines: list[str], section: str, codes: Sequence[str]) -> str:
|
|
230
|
+
"""Write the ``requires:`` a document has none of, under the header it follows."""
|
|
231
|
+
for name in _ANCHORS:
|
|
232
|
+
at = _top(lines, name)
|
|
233
|
+
if at is None:
|
|
234
|
+
continue
|
|
235
|
+
_, _, end = _extent(lines, at)
|
|
236
|
+
before = [] if end == 0 or not lines[end - 1].strip() else [""]
|
|
237
|
+
block = ["requires:", *_entry(" ", section, codes)]
|
|
238
|
+
return "\n".join(lines[:end] + before + block + [""] + lines[end:])
|
|
239
|
+
return "\n".join(lines)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def carried_kinds(source: str) -> dict[str, str]:
|
|
243
|
+
"""The kind each connection a document carries declares, keyed by its code."""
|
|
244
|
+
parsed = safe_load(source)
|
|
245
|
+
if not isinstance(parsed, dict):
|
|
246
|
+
return {}
|
|
247
|
+
carried = cast("JsonMap", parsed).get("connections")
|
|
248
|
+
if not isinstance(carried, dict):
|
|
249
|
+
return {}
|
|
250
|
+
kinds: dict[str, str] = {}
|
|
251
|
+
for code, definition in cast("JsonMap", carried).items():
|
|
252
|
+
if not isinstance(definition, dict):
|
|
253
|
+
continue
|
|
254
|
+
kind = cast("JsonMap", definition).get("kind")
|
|
255
|
+
if isinstance(kind, str):
|
|
256
|
+
kinds[code] = kind
|
|
257
|
+
return kinds
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
#: How a requirement's field is named when a summary counts it, singular and plural.
|
|
261
|
+
_COUNTED: Final = (
|
|
262
|
+
("connections", "connection", "connections"),
|
|
263
|
+
("schemas", "schema", "schemas"),
|
|
264
|
+
("pipelines", "pipeline", "pipelines"),
|
|
265
|
+
("storage", "storage scheme", "storage schemes"),
|
|
266
|
+
("blocks", "block", "blocks"),
|
|
267
|
+
("workers", "worker tag", "worker tags"),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def summary(requires: Requirements) -> str:
|
|
272
|
+
"""Say what a document needs in one line: the counts, or nothing when it needs nothing."""
|
|
273
|
+
counted = [
|
|
274
|
+
f"{len(held)} {one if len(held) == 1 else many}"
|
|
275
|
+
for name, one, many in _COUNTED
|
|
276
|
+
if (held := getattr(requires, name))
|
|
277
|
+
]
|
|
278
|
+
return ", ".join(counted) or "-"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def preflight(requires: Requirements, kinds: Mapping[str, str]) -> list[str]:
|
|
282
|
+
"""List what has to exist on the instance before a copy of this document will apply.
|
|
283
|
+
|
|
284
|
+
``kinds`` names the kind of each connection the source carried, which is the one thing
|
|
285
|
+
a ``dg connection create`` line cannot be written without.
|
|
286
|
+
"""
|
|
287
|
+
steps = [f"dg connection create {kinds.get(code, 'KIND')} {code}" for code in requires.connections]
|
|
288
|
+
steps += [f"dg schema create {code}.json --code {code}" for code in requires.schemas]
|
|
289
|
+
steps += [f"apply the pipeline {code} it starts" for code in requires.pipelines]
|
|
290
|
+
steps += [f"a storage backend claiming {scheme}://" for scheme in requires.storage]
|
|
291
|
+
steps += [f"a pack contributing {block}" for block in requires.blocks]
|
|
292
|
+
steps += [f"a worker carrying the {tag} tag" for tag in requires.workers]
|
|
293
|
+
return steps
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""Copying a starter: the two lines a copy rewrites, and nothing else.
|
|
2
|
-
|
|
3
|
-
A starter is a plain ``dirigent/v1`` document, not a template, so instantiating one is a
|
|
4
|
-
verbatim copy of its text with the top-level ``code:`` changed and ``starter`` taken off the
|
|
5
|
-
top-level ``tags:``. The edit is at text level rather than through a parser because the
|
|
6
|
-
teaching comments, the blank lines and the quoting are the point of copying a document
|
|
7
|
-
instead of generating one.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import re
|
|
11
|
-
from typing import Final
|
|
12
|
-
|
|
13
|
-
from dirigent_client import Requirements
|
|
14
|
-
from dirigent_core.examples import STARTER_TAG
|
|
15
|
-
|
|
16
|
-
#: The top-level ``code:`` line: no indentation, so a step's own ``code`` is never touched.
|
|
17
|
-
_CODE = re.compile(r"^code:[^\S\n]*(.*)$", re.MULTILINE)
|
|
18
|
-
|
|
19
|
-
#: The top-level ``tags:`` line, and whatever it carries on the same line.
|
|
20
|
-
_TAGS = re.compile(r"^tags:[^\S\n]*(.*)$", re.MULTILINE)
|
|
21
|
-
|
|
22
|
-
#: One entry of a block list under ``tags:``: two spaces, a dash, the value.
|
|
23
|
-
_TAG_ITEM = re.compile(r"^[^\S\n]*-[^\S\n]*(\S.*?)[^\S\n]*$")
|
|
24
|
-
|
|
25
|
-
_FLOW: Final = ("[", "]")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def instantiate(source: str, code: str) -> str:
|
|
29
|
-
"""Copy a starter's text under a new code, with the ``starter`` tag dropped."""
|
|
30
|
-
return _retag(_recode(source, code))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _recode(source: str, code: str) -> str:
|
|
34
|
-
"""Rewrite the one top-level ``code:`` line, leaving every other line alone."""
|
|
35
|
-
return _CODE.sub(lambda _: f"code: {code}", source, count=1)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _retag(source: str) -> str:
|
|
39
|
-
"""Take ``starter`` off the top-level ``tags:``, whichever shape the list is written in.
|
|
40
|
-
|
|
41
|
-
A document whose only tag was ``starter`` loses the whole ``tags:`` entry: an empty list
|
|
42
|
-
says less than no list at all.
|
|
43
|
-
"""
|
|
44
|
-
found = _TAGS.search(source)
|
|
45
|
-
if found is None:
|
|
46
|
-
return source
|
|
47
|
-
rest = found.group(1).strip()
|
|
48
|
-
if rest.startswith(_FLOW[0]):
|
|
49
|
-
return _rewrite_flow(source, found.start(), found.end(), rest)
|
|
50
|
-
if rest:
|
|
51
|
-
return source
|
|
52
|
-
return _rewrite_block(source, found.start(), found.end())
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _rewrite_flow(source: str, start: int, end: int, rest: str) -> str:
|
|
56
|
-
"""Rewrite ``tags: [a, b, starter]``, dropping the entry when nothing is left."""
|
|
57
|
-
inner = rest.removeprefix(_FLOW[0]).removesuffix(_FLOW[1])
|
|
58
|
-
kept = [tag for tag in (one.strip() for one in inner.split(",")) if tag and tag != STARTER_TAG]
|
|
59
|
-
if not kept:
|
|
60
|
-
return _drop_line(source, start, end)
|
|
61
|
-
return source[:start] + f"tags: [{', '.join(kept)}]" + source[end:]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _rewrite_block(source: str, start: int, end: int) -> str:
|
|
65
|
-
"""Rewrite a block list under ``tags:``, dropping the whole entry when nothing is left."""
|
|
66
|
-
lines = source[end:].split("\n")
|
|
67
|
-
items: list[tuple[int, str]] = []
|
|
68
|
-
for index, line in enumerate(lines):
|
|
69
|
-
if index == 0 and not line.strip():
|
|
70
|
-
continue
|
|
71
|
-
matched = _TAG_ITEM.match(line)
|
|
72
|
-
if matched is None:
|
|
73
|
-
break
|
|
74
|
-
items.append((index, matched.group(1)))
|
|
75
|
-
if not items:
|
|
76
|
-
return source
|
|
77
|
-
dropped = [index for index, tag in items if tag == STARTER_TAG]
|
|
78
|
-
if not dropped:
|
|
79
|
-
return source
|
|
80
|
-
if len(dropped) == len(items):
|
|
81
|
-
return _drop_line(source, start, end + len("\n".join(lines[: items[-1][0] + 1])))
|
|
82
|
-
kept = [line for index, line in enumerate(lines) if index not in dropped]
|
|
83
|
-
return source[:end] + "\n".join(kept)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _drop_line(source: str, start: int, end: int) -> str:
|
|
87
|
-
"""Remove a whole entry, including the newline that ended it."""
|
|
88
|
-
tail = source[end:]
|
|
89
|
-
return source[:start] + tail.removeprefix("\n")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#: How a requirement's field is named when a summary counts it, singular and plural.
|
|
93
|
-
_COUNTED: Final = (
|
|
94
|
-
("connections", "connection", "connections"),
|
|
95
|
-
("schemas", "schema", "schemas"),
|
|
96
|
-
("pipelines", "pipeline", "pipelines"),
|
|
97
|
-
("storage", "storage scheme", "storage schemes"),
|
|
98
|
-
("blocks", "block", "blocks"),
|
|
99
|
-
("workers", "worker tag", "worker tags"),
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def summary(requires: Requirements) -> str:
|
|
104
|
-
"""Say what a document needs in one line: the counts, or nothing when it needs nothing."""
|
|
105
|
-
counted = [
|
|
106
|
-
f"{len(held)} {one if len(held) == 1 else many}"
|
|
107
|
-
for name, one, many in _COUNTED
|
|
108
|
-
if (held := getattr(requires, name))
|
|
109
|
-
]
|
|
110
|
-
return ", ".join(counted) or "-"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def preflight(requires: Requirements) -> list[str]:
|
|
114
|
-
"""List what has to exist on the instance before a copy of this document will apply."""
|
|
115
|
-
steps = [f"dg connection create KIND {code}" for code in requires.connections]
|
|
116
|
-
steps += [f"dg schema create {code}.json --code {code}" for code in requires.schemas]
|
|
117
|
-
steps += [f"apply the pipeline {code} it starts" for code in requires.pipelines]
|
|
118
|
-
steps += [f"a storage backend claiming {scheme}://" for scheme in requires.storage]
|
|
119
|
-
steps += [f"a pack contributing {block}" for block in requires.blocks]
|
|
120
|
-
steps += [f"a worker carrying the {tag} tag" for tag in requires.workers]
|
|
121
|
-
return steps
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/operator.py.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.17.2 → dirigent_cli-0.18.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|