dirigent-cli 0.9.2__tar.gz → 0.9.4__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.9.2 → dirigent_cli-0.9.4}/PKG-INFO +2 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/pyproject.toml +2 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/pyproject.toml.orig +2 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/commands.py +11 -3
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/context.py +15 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/main.py +7 -10
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/profiles.py +19 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/project.py +15 -10
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/summaries.py +9 -4
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/LICENSE +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/README.md +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/__init__.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/aliases.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/formatters.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/graph.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/health.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/local.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/output.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/params.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/py.typed +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/reaper.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/scaffold.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/schemas.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/sources.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/stream.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/timing.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/triggers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: The dirigent command line interface (dirigent / dg).
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
@@ -11,6 +11,7 @@ Requires-Dist: dirigent-core
|
|
|
11
11
|
Requires-Dist: dirigent-plugin
|
|
12
12
|
Requires-Dist: dirigent-server
|
|
13
13
|
Requires-Dist: httpx2>=2.12.0
|
|
14
|
+
Requires-Dist: python-dotenv>=1.1.0
|
|
14
15
|
Requires-Dist: pyyaml>=6.0.3
|
|
15
16
|
Requires-Dist: rich>=15.0.0
|
|
16
17
|
Requires-Dist: tomli-w>=1.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-cli"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.4"
|
|
4
4
|
description = "The dirigent command line interface (dirigent / dg)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -14,6 +14,7 @@ dependencies = [
|
|
|
14
14
|
"dirigent-plugin",
|
|
15
15
|
"dirigent-server",
|
|
16
16
|
"httpx2>=2.12.0",
|
|
17
|
+
"python-dotenv>=1.1.0",
|
|
17
18
|
"pyyaml>=6.0.3",
|
|
18
19
|
"rich>=15.0.0",
|
|
19
20
|
"tomli-w>=1.2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-cli"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.4"
|
|
4
4
|
description = "The dirigent command line interface (dirigent / dg)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -14,6 +14,7 @@ dependencies = [
|
|
|
14
14
|
"dirigent-plugin",
|
|
15
15
|
"dirigent-server",
|
|
16
16
|
"httpx2>=2.12.0",
|
|
17
|
+
"python-dotenv>=1.1.0",
|
|
17
18
|
"pyyaml>=6.0.3",
|
|
18
19
|
"rich>=15.0.0",
|
|
19
20
|
"tomli-w>=1.2.0",
|
|
@@ -56,7 +56,14 @@ from dirigent_cli.output import (
|
|
|
56
56
|
table,
|
|
57
57
|
)
|
|
58
58
|
from dirigent_cli.params import ParamError, build_params
|
|
59
|
-
from dirigent_cli.project import
|
|
59
|
+
from dirigent_cli.project import (
|
|
60
|
+
COMPOSE_TEMPLATE_NAME,
|
|
61
|
+
ProjectError,
|
|
62
|
+
check_template,
|
|
63
|
+
find_project,
|
|
64
|
+
scaffold,
|
|
65
|
+
write_token_env,
|
|
66
|
+
)
|
|
60
67
|
from dirigent_cli.scaffold import ScaffoldedRecord, ScaffoldError, ScaffoldRecord, scaffold_pack
|
|
61
68
|
from dirigent_cli.sources import Document, SourceError, looks_like_a_document, read_document, read_path
|
|
62
69
|
from dirigent_cli.stream import Sink, track_steps, use_scratch_prefix
|
|
@@ -546,6 +553,7 @@ def init_command(
|
|
|
546
553
|
migrated = migrations.head_revision(settings) or "none"
|
|
547
554
|
migrations.upgrade("head", settings)
|
|
548
555
|
token = asyncio.run(first_admin(settings, admin, secret))
|
|
556
|
+
env_file = write_token_env(root, token)
|
|
549
557
|
emit_fact(
|
|
550
558
|
"instance.initialised",
|
|
551
559
|
message="initialised",
|
|
@@ -556,7 +564,7 @@ def init_command(
|
|
|
556
564
|
admin=admin,
|
|
557
565
|
token=token,
|
|
558
566
|
version=version,
|
|
559
|
-
files=[_within(path, root) for path in made.files],
|
|
567
|
+
files=[_within(path, root) for path in [*made.files, env_file]],
|
|
560
568
|
**({"skipped": [_within(path, root) for path in made.skipped]} if made.skipped else {}),
|
|
561
569
|
)
|
|
562
570
|
|
|
@@ -600,7 +608,7 @@ def _refuse_an_existing_instance(root: Path) -> None:
|
|
|
600
608
|
_init_fail(
|
|
601
609
|
f"{existing} already exists, so this directory holds an instance already",
|
|
602
610
|
problems=[
|
|
603
|
-
"dg dev
|
|
611
|
+
"dg dev starts it",
|
|
604
612
|
"dg db upgrade brings its schema forward",
|
|
605
613
|
"dg init --documents-only scaffolds documents beside it",
|
|
606
614
|
],
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
from collections.abc import Awaitable
|
|
6
6
|
from functools import cached_property
|
|
7
|
+
from urllib.parse import urlsplit
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
10
|
from pydantic import BaseModel, ConfigDict
|
|
@@ -17,7 +18,7 @@ from dirigent_cli.output import (
|
|
|
17
18
|
)
|
|
18
19
|
from dirigent_cli.profiles import Endpoint, ProfileError, resolve_endpoint
|
|
19
20
|
from dirigent_cli.stream import ansi
|
|
20
|
-
from dirigent_client import BlockingDirigent, DirigentError
|
|
21
|
+
from dirigent_client import BlockingDirigent, DirigentError, TransportError
|
|
21
22
|
from dirigent_core.logging import NOISY_LOGGERS, configure_logging
|
|
22
23
|
from dirigent_core.protocol import Format
|
|
23
24
|
|
|
@@ -115,6 +116,17 @@ def state_of(ctx: typer.Context) -> CliState:
|
|
|
115
116
|
return found if found is not None else CliState()
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
#: What a refused connection to a loopback address adds: the local instance is not running.
|
|
120
|
+
LOCAL_INSTANCE_HINT = "nothing is listening there; uv run dg dev in the project starts its instance"
|
|
121
|
+
|
|
122
|
+
LOOPBACK_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_local(url: str | None) -> bool:
|
|
126
|
+
"""Whether a URL addresses this machine, where a refused connection means nothing is running."""
|
|
127
|
+
return bool(url) and (urlsplit(url or "").hostname or "") in LOOPBACK_HOSTS
|
|
128
|
+
|
|
129
|
+
|
|
118
130
|
class Session(BlockingDirigent):
|
|
119
131
|
"""The SDK, driven from the CLI's synchronous commands."""
|
|
120
132
|
|
|
@@ -125,6 +137,8 @@ class Session(BlockingDirigent):
|
|
|
125
137
|
except DirigentError as error:
|
|
126
138
|
if error.problem is not None:
|
|
127
139
|
write_refusal(error.problem)
|
|
140
|
+
elif isinstance(error, TransportError) and _is_local(error.url):
|
|
141
|
+
refuse(error.message, problems=[LOCAL_INSTANCE_HINT])
|
|
128
142
|
else:
|
|
129
143
|
refuse(error.message, status=error.status or 1)
|
|
130
144
|
raise typer.Exit(code=1) from error
|
|
@@ -810,18 +810,16 @@ def dev(
|
|
|
810
810
|
bool,
|
|
811
811
|
typer.Option(
|
|
812
812
|
"--wipe-state/--keep-state",
|
|
813
|
-
help="Delete the state directory before starting,
|
|
813
|
+
help="Delete the state directory before starting, instead of running the instance that is there.",
|
|
814
814
|
),
|
|
815
|
-
] =
|
|
815
|
+
] = False,
|
|
816
816
|
) -> None:
|
|
817
817
|
"""Run the API and an embedded worker in one process, on SQLite, with no dependencies.
|
|
818
818
|
|
|
819
819
|
The database and the artifacts live in .dirigent/state under the working directory, so
|
|
820
820
|
starting this somewhere else means a different instance, with none of the same runs.
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
is a development artifact, and a database left over from an older schema answers
|
|
824
|
-
strangely rather than failing. Only a directory dirigent named itself is removed.
|
|
821
|
+
An instance that is there, made by dg init or by an earlier start, is the one that runs;
|
|
822
|
+
--wipe-state deletes it first, and only a directory dirigent named itself is removed.
|
|
825
823
|
"""
|
|
826
824
|
import asyncio
|
|
827
825
|
import os
|
|
@@ -879,10 +877,10 @@ def clear_state(settings: Settings) -> Path | None:
|
|
|
879
877
|
|
|
880
878
|
|
|
881
879
|
def state_cleared(directory: Path) -> Record:
|
|
882
|
-
"""Build the record saying the state went,
|
|
880
|
+
"""Build the record saying the state went, as --wipe-state asked.
|
|
883
881
|
|
|
884
|
-
Emitted only when something was actually deleted, so a
|
|
885
|
-
|
|
882
|
+
Emitted only when something was actually deleted, so a wipe of an empty machine stays
|
|
883
|
+
quiet.
|
|
886
884
|
"""
|
|
887
885
|
return make(
|
|
888
886
|
"process",
|
|
@@ -890,7 +888,6 @@ def state_cleared(directory: Path) -> Record:
|
|
|
890
888
|
message="state cleared",
|
|
891
889
|
process="dev",
|
|
892
890
|
state=str(directory),
|
|
893
|
-
hint="--keep-state keeps it",
|
|
894
891
|
)
|
|
895
892
|
|
|
896
893
|
|
|
@@ -12,12 +12,14 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any, Final, cast
|
|
13
13
|
|
|
14
14
|
import yaml
|
|
15
|
+
from dotenv import dotenv_values
|
|
15
16
|
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
16
17
|
|
|
17
18
|
from dirigent_client import API_PREFIX
|
|
18
19
|
from dirigent_common import EntityName
|
|
19
20
|
|
|
20
21
|
PROJECT_PROFILES: Final = Path(".dirigent") / "profiles.yaml"
|
|
22
|
+
PROJECT_ENV_FILE: Final = ".env"
|
|
21
23
|
USER_PROFILES: Final = Path.home() / ".config" / "dirigent" / "profiles.yaml"
|
|
22
24
|
|
|
23
25
|
URL_ENV: Final = "DG_URL"
|
|
@@ -109,6 +111,21 @@ class ProfileStore(BaseModel):
|
|
|
109
111
|
profiles: dict[EntityName, Profile] = Field(default_factory=dict[str, Profile])
|
|
110
112
|
path: Path | None = None
|
|
111
113
|
|
|
114
|
+
@property
|
|
115
|
+
def env_file(self) -> Path | None:
|
|
116
|
+
"""The ``.env`` at the root of the project this file belongs to, or none for the user file."""
|
|
117
|
+
if self.path is None or self.path.parent.name != PROJECT_PROFILES.parent.name:
|
|
118
|
+
return None
|
|
119
|
+
return self.path.parent.parent / PROJECT_ENV_FILE
|
|
120
|
+
|
|
121
|
+
def environment(self, environ: Mapping[str, str]) -> dict[str, str]:
|
|
122
|
+
"""The environment a profile resolves in: the process's, with the project's ``.env`` filling gaps."""
|
|
123
|
+
path = self.env_file
|
|
124
|
+
if path is None or not path.is_file():
|
|
125
|
+
return dict(environ)
|
|
126
|
+
from_file = {key: value for key, value in dotenv_values(path).items() if value is not None}
|
|
127
|
+
return {**from_file, **environ}
|
|
128
|
+
|
|
112
129
|
def select(self, name: str | None) -> Profile | None:
|
|
113
130
|
"""Choose a profile by name, by the file's default, or by there being only one."""
|
|
114
131
|
wanted = name or self.default
|
|
@@ -189,11 +206,12 @@ def resolve_endpoint(
|
|
|
189
206
|
) -> Endpoint:
|
|
190
207
|
"""Resolve the server and token: flags first, then ``DG_*``, then the selected profile.
|
|
191
208
|
|
|
209
|
+
A project's ``.env`` stands in for any ``DG_*`` variable the environment does not set.
|
|
192
210
|
With ``needs_token`` off, a profile whose token cannot be resolved yields no token rather
|
|
193
211
|
than refusing, which is what the command that mints one asks for.
|
|
194
212
|
"""
|
|
195
|
-
env = environ if environ is not None else dict(os.environ)
|
|
196
213
|
store = find_store(start)
|
|
214
|
+
env = store.environment(environ if environ is not None else os.environ)
|
|
197
215
|
chosen = store.select(profile or env.get(PROFILE_ENV))
|
|
198
216
|
|
|
199
217
|
if url is not None:
|
|
@@ -477,14 +477,17 @@ ROOT_IGNORE_TEMPLATE = """\
|
|
|
477
477
|
# The environment uv sync builds from pyproject.toml.
|
|
478
478
|
.venv/
|
|
479
479
|
__pycache__/
|
|
480
|
-
"""
|
|
481
480
|
|
|
482
|
-
|
|
483
|
-
COMPOSE_IGNORE_TEMPLATE = """\
|
|
484
|
-
# Holds this instance's key and its first admin's password.
|
|
481
|
+
# This instance's token, or the stack's key and first password. Never shared.
|
|
485
482
|
.env
|
|
486
483
|
"""
|
|
487
484
|
|
|
485
|
+
TOKEN_ENV_TEMPLATE = """\
|
|
486
|
+
# The token dg init minted for this instance's first admin. The local profile in
|
|
487
|
+
# .dirigent/profiles.yaml reads it from here when the shell does not export it.
|
|
488
|
+
DG_TOKEN=__TOKEN__
|
|
489
|
+
"""
|
|
490
|
+
|
|
488
491
|
PYPROJECT_TEMPLATE = """\
|
|
489
492
|
[project]
|
|
490
493
|
name = "__NAME__"
|
|
@@ -516,13 +519,13 @@ __RUN__
|
|
|
516
519
|
README_RUN: Final = {
|
|
517
520
|
"basic": (
|
|
518
521
|
"uv sync",
|
|
519
|
-
"uv run dg dev
|
|
522
|
+
"uv run dg dev",
|
|
520
523
|
"uv run dg apply",
|
|
521
524
|
"uv run dg run hello-world --watch",
|
|
522
525
|
),
|
|
523
526
|
"ci": (
|
|
524
527
|
"uv sync",
|
|
525
|
-
"uv run dg dev
|
|
528
|
+
"uv run dg dev",
|
|
526
529
|
"uv run dg apply",
|
|
527
530
|
"uv run dg run hello-world --watch",
|
|
528
531
|
),
|
|
@@ -585,10 +588,7 @@ def scaffold(
|
|
|
585
588
|
_record(directory / "pyproject.toml", pyproject, written, skipped)
|
|
586
589
|
readme = README_TEMPLATE.replace("__NAME__", name).replace("__RUN__", "\n".join(README_RUN[template]))
|
|
587
590
|
_record(directory / "README.md", readme, written, skipped)
|
|
588
|
-
|
|
589
|
-
if template == COMPOSE_TEMPLATE_NAME:
|
|
590
|
-
ignore += "\n" + COMPOSE_IGNORE_TEMPLATE
|
|
591
|
-
_merge_ignore(directory / ".gitignore", ignore, written, skipped)
|
|
591
|
+
_merge_ignore(directory / ".gitignore", ROOT_IGNORE_TEMPLATE, written, skipped)
|
|
592
592
|
if template == "ci":
|
|
593
593
|
written.append(_write(directory / ".github" / "workflows" / "dirigent.yml", WORKFLOW_TEMPLATE))
|
|
594
594
|
if template == COMPOSE_TEMPLATE_NAME:
|
|
@@ -603,6 +603,11 @@ def scaffold(
|
|
|
603
603
|
return Scaffolded(files=written, skipped=skipped)
|
|
604
604
|
|
|
605
605
|
|
|
606
|
+
def write_token_env(directory: Path, token: str) -> Path:
|
|
607
|
+
"""Write the ``.env`` holding a new instance's token, readable by its owner alone."""
|
|
608
|
+
return _write(directory / ".env", TOKEN_ENV_TEMPLATE.replace("__TOKEN__", token), mode=0o600)
|
|
609
|
+
|
|
610
|
+
|
|
606
611
|
def _record(path: Path, content: str, written: list[Path], skipped: list[Path]) -> None:
|
|
607
612
|
"""Write a file a project may already have of its own, leaving any existing one alone."""
|
|
608
613
|
if path.exists():
|
|
@@ -285,12 +285,17 @@ def _initialised(record: Record) -> RenderableType | None:
|
|
|
285
285
|
},
|
|
286
286
|
)
|
|
287
287
|
)
|
|
288
|
+
admin = escape(str(record.get("admin") or "admin"))
|
|
288
289
|
parts.append(
|
|
289
|
-
"\nThe
|
|
290
|
-
f"\
|
|
291
|
-
"\
|
|
290
|
+
"\nThe instance lives in this directory. Its database is .dirigent/state, and the"
|
|
291
|
+
f"\ntoken of its first admin, [bold]{admin}[/], is in .env, where the local profile reads it."
|
|
292
|
+
"\nShown here once:"
|
|
293
|
+
f"\n [bold]{token}[/]"
|
|
294
|
+
"\n\nStart the instance in a second terminal here; it keeps running:"
|
|
292
295
|
"\n [bold]uv sync[/]"
|
|
293
|
-
"\n [bold]uv run dg dev
|
|
296
|
+
"\n [bold]uv run dg dev[/]"
|
|
297
|
+
f"\nThe UI is at http://127.0.0.1:3333, and {admin} logs in with the password you gave."
|
|
298
|
+
"\n\nThen apply the example here, and run it:"
|
|
294
299
|
"\n [bold]uv run dg apply[/]"
|
|
295
300
|
"\n [bold]uv run dg run hello-world --watch[/]"
|
|
296
301
|
"\n\n[yellow]This is an instance for one person on one machine[/]: SQLite on this disk, and"
|
|
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.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.9.2 → dirigent_cli-0.9.4}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|