dirigent-cli 0.13.0__tar.gz → 0.14.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.13.0 → dirigent_cli-0.14.0}/PKG-INFO +1 -1
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/pyproject.toml +1 -1
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/pyproject.toml.orig +1 -1
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/main.py +88 -5
- dirigent_cli-0.14.0/src/dirigent_cli/seeding.py +198 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/LICENSE +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/README.md +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/__init__.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/aliases.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/commands.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/context.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/formatters.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/graph.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/health.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/init_form.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/local.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/output.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/params.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/profiles.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/project.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/py.typed +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/reaper.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/scaffold.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/schemas.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/sources.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/stream.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/summaries.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/timing.py +0 -0
- {dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/triggers.py +0 -0
|
@@ -192,6 +192,10 @@ DEV_ADMIN = "dev"
|
|
|
192
192
|
DEV_PASSWORD = "dirigent-dev" # noqa: S105
|
|
193
193
|
DEV_TOKEN_NAME = "dev"
|
|
194
194
|
|
|
195
|
+
#: The credential `dg dev --seed` authenticates its own applies with, and how long it lasts.
|
|
196
|
+
SEED_TOKEN_NAME = "dev seed"
|
|
197
|
+
SEED_TOKEN_LIFETIME = timedelta(hours=1)
|
|
198
|
+
|
|
195
199
|
SCHEDULER_ENV = "DIRIGENT_SCHEDULER_ENABLED"
|
|
196
200
|
|
|
197
201
|
UI_ENV = "DIRIGENT_UI_ENABLED"
|
|
@@ -812,6 +816,14 @@ def dev(
|
|
|
812
816
|
help="Delete the state directory before starting, instead of running the instance that is there.",
|
|
813
817
|
),
|
|
814
818
|
] = False,
|
|
819
|
+
seed: Annotated[
|
|
820
|
+
list[Path] | None,
|
|
821
|
+
typer.Option(
|
|
822
|
+
"--seed",
|
|
823
|
+
help="Apply every dirigent/v1 document under this directory once the API answers, "
|
|
824
|
+
"with its schedules paused; name it more than once to seed one directory after another.",
|
|
825
|
+
),
|
|
826
|
+
] = None,
|
|
815
827
|
) -> None:
|
|
816
828
|
"""Run the API and an embedded worker in one process, on SQLite, with no dependencies.
|
|
817
829
|
|
|
@@ -819,6 +831,11 @@ def dev(
|
|
|
819
831
|
starting this somewhere else means a different instance, with none of the same runs.
|
|
820
832
|
An instance that is there, made by dg init or by an earlier start, is the one that runs;
|
|
821
833
|
--wipe-state deletes it first, and only a directory dirigent named itself is removed.
|
|
834
|
+
|
|
835
|
+
--seed fills the instance from a directory of documents the moment it answers: the
|
|
836
|
+
connections a file or a document declares are created first, then every document is
|
|
837
|
+
applied with its schedules paused. A document an instance will not store is reported
|
|
838
|
+
and passed over, because a corpus holds those on purpose.
|
|
822
839
|
"""
|
|
823
840
|
import asyncio
|
|
824
841
|
import os
|
|
@@ -848,9 +865,13 @@ def dev(
|
|
|
848
865
|
emit(state_cleared(cleared))
|
|
849
866
|
migrated = _migrate_quietly(settings)
|
|
850
867
|
admin, token = asyncio.run(dev_admin(settings))
|
|
851
|
-
|
|
868
|
+
address = host or settings.host
|
|
869
|
+
listening = port or settings.port
|
|
870
|
+
bound = f"http://{address}:{listening}"
|
|
852
871
|
emit(dev_started(settings, bound=bound, admin=admin, token=token, migrated=migrated))
|
|
853
|
-
|
|
872
|
+
directories = seed or []
|
|
873
|
+
bearer = token or (asyncio.run(seed_token(settings)) if directories else None)
|
|
874
|
+
asyncio.run(_dev(settings, address, listening, seed=directories, bearer=bearer))
|
|
854
875
|
|
|
855
876
|
|
|
856
877
|
def clear_state(settings: Settings) -> Path | None:
|
|
@@ -980,10 +1001,46 @@ async def dev_admin(settings: Settings) -> tuple[str | None, str | None]:
|
|
|
980
1001
|
await engine.dispose()
|
|
981
1002
|
|
|
982
1003
|
|
|
983
|
-
async def
|
|
984
|
-
"""
|
|
1004
|
+
async def seed_token(settings: Settings) -> str:
|
|
1005
|
+
"""Mint the short-lived admin token the seeding authenticates with.
|
|
985
1006
|
|
|
986
|
-
The
|
|
1007
|
+
The seeding goes through the API rather than the database, so it needs a bearer even
|
|
1008
|
+
though it runs inside the process that is serving. A --keep-state instance kept its own
|
|
1009
|
+
admin and handed its token over long ago, so one is minted here for the seeding alone.
|
|
1010
|
+
"""
|
|
1011
|
+
from dirigent_core.auth import issue_token, list_users
|
|
1012
|
+
from dirigent_core.database import create_engine, create_session_factory, session_scope
|
|
1013
|
+
|
|
1014
|
+
engine = create_engine(settings)
|
|
1015
|
+
try:
|
|
1016
|
+
async with session_scope(create_session_factory(engine)) as session:
|
|
1017
|
+
admins = [user for user in await list_users(session) if user.role is UserRole.ADMIN and user.active]
|
|
1018
|
+
if not admins:
|
|
1019
|
+
refuse(
|
|
1020
|
+
"dg dev --seed has no admin account to seed as",
|
|
1021
|
+
status=commands.GUARD_EXIT,
|
|
1022
|
+
title="No admin",
|
|
1023
|
+
problems=["create one with dg admin user create --role admin"],
|
|
1024
|
+
)
|
|
1025
|
+
raise typer.Exit(code=commands.GUARD_EXIT)
|
|
1026
|
+
issued = await issue_token(session, admins[0], name=SEED_TOKEN_NAME, lifetime=SEED_TOKEN_LIFETIME)
|
|
1027
|
+
return issued.secret.get_secret_value()
|
|
1028
|
+
finally:
|
|
1029
|
+
await engine.dispose()
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
async def _dev(
|
|
1033
|
+
settings: Settings,
|
|
1034
|
+
host: str,
|
|
1035
|
+
port: int,
|
|
1036
|
+
*,
|
|
1037
|
+
seed: Sequence[Path] = (),
|
|
1038
|
+
bearer: str | None = None,
|
|
1039
|
+
) -> None:
|
|
1040
|
+
"""Run the API, the scheduler, a worker, and any seeding as tasks in one event loop.
|
|
1041
|
+
|
|
1042
|
+
The scheduler is started by the application's own lifespan, not here. The seeding is a
|
|
1043
|
+
task rather than a step before the loop, because it talks to the API this loop serves.
|
|
987
1044
|
"""
|
|
988
1045
|
import asyncio
|
|
989
1046
|
|
|
@@ -997,15 +1054,24 @@ async def _dev(settings: Settings, host: str, port: int) -> None:
|
|
|
997
1054
|
api = uvicorn.Server(server_config)
|
|
998
1055
|
worker_task = asyncio.create_task(worker.run())
|
|
999
1056
|
ready = asyncio.create_task(_announce_ready(api))
|
|
1057
|
+
seeding = asyncio.create_task(_seed(api, local_url(host, port), bearer, seed)) if seed else None
|
|
1000
1058
|
try:
|
|
1001
1059
|
await api.serve()
|
|
1002
1060
|
finally:
|
|
1003
1061
|
ready.cancel()
|
|
1062
|
+
if seeding is not None:
|
|
1063
|
+
seeding.cancel()
|
|
1004
1064
|
worker.request_stop()
|
|
1005
1065
|
await worker_task
|
|
1006
1066
|
await engine.dispose()
|
|
1007
1067
|
|
|
1008
1068
|
|
|
1069
|
+
def local_url(host: str, port: int) -> str:
|
|
1070
|
+
"""Address the instance this process serves, however widely it is bound."""
|
|
1071
|
+
wildcards = {"0.0.0.0", "::", ""} # noqa: S104 - naming the bind-all addresses, not binding one
|
|
1072
|
+
return f"http://{'127.0.0.1' if host in wildcards else host}:{port}"
|
|
1073
|
+
|
|
1074
|
+
|
|
1009
1075
|
async def _announce_ready(api: "uvicorn.Server") -> None:
|
|
1010
1076
|
"""Say ready once the port is actually accepting, and not a moment before."""
|
|
1011
1077
|
import asyncio
|
|
@@ -1015,6 +1081,23 @@ async def _announce_ready(api: "uvicorn.Server") -> None:
|
|
|
1015
1081
|
emit(make("process", at=datetime.now(UTC), message="ready", process="dev"))
|
|
1016
1082
|
|
|
1017
1083
|
|
|
1084
|
+
async def _seed(api: "uvicorn.Server", url: str, bearer: str | None, directories: Sequence[Path]) -> None:
|
|
1085
|
+
"""Apply what --seed named, once the port is accepting, and write a record for each."""
|
|
1086
|
+
import asyncio
|
|
1087
|
+
|
|
1088
|
+
from dirigent_cli.seeding import seed_directories
|
|
1089
|
+
from dirigent_client import Dirigent, DirigentError
|
|
1090
|
+
|
|
1091
|
+
while not api.started:
|
|
1092
|
+
await asyncio.sleep(0.05)
|
|
1093
|
+
try:
|
|
1094
|
+
async with Dirigent(url=url, token=bearer) as client:
|
|
1095
|
+
async for record in seed_directories(client, directories):
|
|
1096
|
+
emit(record)
|
|
1097
|
+
except DirigentError as error:
|
|
1098
|
+
emit(make("error", at=datetime.now(UTC), level="error", message=f"seeding stopped: {error.message}"))
|
|
1099
|
+
|
|
1100
|
+
|
|
1018
1101
|
@app.command(rich_help_panel=PROCESS_PANEL)
|
|
1019
1102
|
def worker(
|
|
1020
1103
|
ctx: typer.Context,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Filling a live instance from directories of documents, which ``dg dev --seed`` does at boot.
|
|
2
|
+
|
|
3
|
+
The seeding goes through the API, so every apply meets the same preflight a person's apply
|
|
4
|
+
meets, and a refusal is a record rather than a failure: a corpus is expected to hold
|
|
5
|
+
documents an instance will not store.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import AsyncIterator, Mapping, Sequence
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Final, cast
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from dirigent_cli.local import ConnectionSpec
|
|
16
|
+
from dirigent_client import Dirigent, DirigentError, PlanAction, ProvenanceSource
|
|
17
|
+
from dirigent_common import JsonMap
|
|
18
|
+
from dirigent_core.documents import safe_load
|
|
19
|
+
from dirigent_core.engine.definition import FORMAT_V1
|
|
20
|
+
from dirigent_core.protocol import Record, make
|
|
21
|
+
|
|
22
|
+
#: The file endings a seed reads. Anything else under a seed directory is passed over, and so
|
|
23
|
+
#: is a file of one of these that does not parse as a mapping.
|
|
24
|
+
SUFFIXES: Final = (".yaml", ".yml", ".json")
|
|
25
|
+
|
|
26
|
+
#: The sections a document may carry so that it runs alone under ``dg run --local``. An
|
|
27
|
+
#: instance refuses to store a document carrying one, so the seed creates what they declare
|
|
28
|
+
#: and applies the document without them.
|
|
29
|
+
CARRIED: Final = ("connections", "schemas")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def readable(directory: Path) -> list[tuple[Path, JsonMap]]:
|
|
33
|
+
"""Read every file under a directory that parses as a mapping, in a stable order."""
|
|
34
|
+
found: list[tuple[Path, JsonMap]] = []
|
|
35
|
+
for path in sorted(one for one in directory.rglob("*") if one.suffix in SUFFIXES and one.is_file()):
|
|
36
|
+
try:
|
|
37
|
+
parsed = safe_load(path.read_text())
|
|
38
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError):
|
|
39
|
+
continue
|
|
40
|
+
if isinstance(parsed, dict):
|
|
41
|
+
found.append((path, cast("JsonMap", parsed)))
|
|
42
|
+
return found
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_document(raw: JsonMap) -> bool:
|
|
46
|
+
"""Say whether a parsed file is a document this instance reads."""
|
|
47
|
+
return raw.get("format") == FORMAT_V1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def specs(declared: object) -> list[ConnectionSpec]:
|
|
51
|
+
"""Read a ``connections:`` section, of a file or of a document, into one spec per code."""
|
|
52
|
+
if not isinstance(declared, Mapping):
|
|
53
|
+
return []
|
|
54
|
+
read: list[ConnectionSpec] = []
|
|
55
|
+
for code, body in sorted(cast("Mapping[str, object]", declared).items()):
|
|
56
|
+
fields = cast("dict[str, Any]", body) if isinstance(body, Mapping) else {}
|
|
57
|
+
read.append(ConnectionSpec.model_validate({**fields, "code": code}))
|
|
58
|
+
return read
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def seed_directories(client: Dirigent, directories: Sequence[Path]) -> AsyncIterator[Record]:
|
|
62
|
+
"""Apply every document under each directory, paused, and create what they need first.
|
|
63
|
+
|
|
64
|
+
The closing record's counts are read off the records the seeding emitted, so a reader of
|
|
65
|
+
the stream and this summary never disagree.
|
|
66
|
+
"""
|
|
67
|
+
pipelines = 0
|
|
68
|
+
refused = 0
|
|
69
|
+
connections: set[str] = set()
|
|
70
|
+
async for record in _walk(client, directories):
|
|
71
|
+
kind = record["kind"]
|
|
72
|
+
if kind == "seed.applied":
|
|
73
|
+
pipelines += 1
|
|
74
|
+
elif kind == "seed.refused":
|
|
75
|
+
refused += 1
|
|
76
|
+
elif kind == "seed.connection":
|
|
77
|
+
connections.add(str(record["connection"]))
|
|
78
|
+
yield record
|
|
79
|
+
yield make(
|
|
80
|
+
"seed.done",
|
|
81
|
+
at=datetime.now(UTC),
|
|
82
|
+
message="seeded",
|
|
83
|
+
directories=[str(one) for one in directories],
|
|
84
|
+
pipelines=pipelines,
|
|
85
|
+
refused=refused,
|
|
86
|
+
connections=sorted(connections),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _walk(client: Dirigent, directories: Sequence[Path]) -> AsyncIterator[Record]:
|
|
91
|
+
"""Seed each directory in the order given: the connections it declares, then its documents.
|
|
92
|
+
|
|
93
|
+
A directory's own connection files go in before any of its documents, so a document that
|
|
94
|
+
names one of them passes the preflight that reads it.
|
|
95
|
+
"""
|
|
96
|
+
for directory in directories:
|
|
97
|
+
files = readable(directory)
|
|
98
|
+
for path, raw in files:
|
|
99
|
+
if not is_document(raw):
|
|
100
|
+
async for record in _connections(client, specs(raw.get("connections")), str(path)):
|
|
101
|
+
yield record
|
|
102
|
+
for path, raw in files:
|
|
103
|
+
if is_document(raw):
|
|
104
|
+
async for record in _document(client, raw, str(path)):
|
|
105
|
+
yield record
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def _document(client: Dirigent, raw: JsonMap, origin: str) -> AsyncIterator[Record]:
|
|
109
|
+
"""Create what one document carries, then apply the document without those sections."""
|
|
110
|
+
async for record in _connections(client, specs(raw.get("connections")), origin):
|
|
111
|
+
yield record
|
|
112
|
+
for code, body in _schemas(raw).items():
|
|
113
|
+
try:
|
|
114
|
+
await _schema(client, code, body)
|
|
115
|
+
except DirigentError as error:
|
|
116
|
+
yield _refused(origin, f"schema {code}: {_reason(error)}")
|
|
117
|
+
yield await _apply(client, raw, origin)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _schemas(raw: JsonMap) -> dict[str, JsonMap]:
|
|
121
|
+
"""Read a document's ``schemas:`` section as one body per code."""
|
|
122
|
+
declared = raw.get("schemas")
|
|
123
|
+
if not isinstance(declared, Mapping):
|
|
124
|
+
return {}
|
|
125
|
+
read = cast("Mapping[str, object]", declared)
|
|
126
|
+
return {code: cast("JsonMap", body) for code, body in sorted(read.items()) if isinstance(body, Mapping)}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def _schema(client: Dirigent, code: str, body: JsonMap) -> None:
|
|
130
|
+
"""Store one named schema, replacing the body of one this instance already holds."""
|
|
131
|
+
try:
|
|
132
|
+
await client.schemas.create(body, code=code)
|
|
133
|
+
except DirigentError:
|
|
134
|
+
await client.schemas.update(code, body=body)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _connections(client: Dirigent, declared: Sequence[ConnectionSpec], origin: str) -> AsyncIterator[Record]:
|
|
138
|
+
"""Create each declared connection, or replace the config of one that is already there."""
|
|
139
|
+
for spec in declared:
|
|
140
|
+
try:
|
|
141
|
+
action = await _connection(client, spec)
|
|
142
|
+
except DirigentError as error:
|
|
143
|
+
yield _refused(origin, f"connection {spec.code}: {_reason(error)}")
|
|
144
|
+
continue
|
|
145
|
+
yield make(
|
|
146
|
+
"seed.connection",
|
|
147
|
+
at=datetime.now(UTC),
|
|
148
|
+
message="connection",
|
|
149
|
+
connection=spec.code,
|
|
150
|
+
connection_kind=spec.kind,
|
|
151
|
+
action=action,
|
|
152
|
+
origin=origin,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def _connection(client: Dirigent, spec: ConnectionSpec) -> str:
|
|
157
|
+
"""Store one connection and say whether it was new, letting a refusal through."""
|
|
158
|
+
try:
|
|
159
|
+
await client.connections.create(spec.code, kind=spec.kind, config=spec.config, name=spec.name)
|
|
160
|
+
except DirigentError:
|
|
161
|
+
await client.connections.update(spec.code, config=spec.config)
|
|
162
|
+
return "updated"
|
|
163
|
+
return "created"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def _apply(client: Dirigent, raw: JsonMap, origin: str) -> Record:
|
|
167
|
+
"""Apply one document with its schedules paused, without the sections it carries."""
|
|
168
|
+
document = {name: value for name, value in raw.items() if name not in CARRIED}
|
|
169
|
+
try:
|
|
170
|
+
result = await client.pipelines.apply(
|
|
171
|
+
document,
|
|
172
|
+
source=ProvenanceSource.FILE,
|
|
173
|
+
source_ref=origin,
|
|
174
|
+
pause_schedules=True,
|
|
175
|
+
)
|
|
176
|
+
except DirigentError as error:
|
|
177
|
+
return _refused(origin, _reason(error))
|
|
178
|
+
if result.plan.action is PlanAction.INVALID:
|
|
179
|
+
return _refused(origin, "; ".join(str(issue) for issue in result.plan.issues))
|
|
180
|
+
return make(
|
|
181
|
+
"seed.applied",
|
|
182
|
+
at=datetime.now(UTC),
|
|
183
|
+
message="applied",
|
|
184
|
+
document=origin,
|
|
185
|
+
pipeline=result.plan.code,
|
|
186
|
+
action=result.plan.action.value,
|
|
187
|
+
schedules_paused=result.triggers.schedules_created,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _refused(origin: str, reason: str) -> Record:
|
|
192
|
+
"""Build the record saying what the instance would not take, and why."""
|
|
193
|
+
return make("seed.refused", at=datetime.now(UTC), message="refused", document=origin, reason=reason)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _reason(error: DirigentError) -> str:
|
|
197
|
+
"""Read a refusal as the one line a record carries."""
|
|
198
|
+
return "; ".join(error.problems) or error.message
|
|
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
|
|
File without changes
|
{dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/operator.py.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.13.0 → dirigent_cli-0.14.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|