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