dirigent-cli 0.9.2__tar.gz → 0.9.3__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.3}/PKG-INFO +2 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/pyproject.toml +2 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/pyproject.toml.orig +2 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/commands.py +10 -2
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/context.py +15 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/profiles.py +19 -1
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/project.py +13 -8
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/summaries.py +9 -4
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/LICENSE +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/README.md +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/__init__.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/aliases.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/formatters.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/graph.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/health.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/local.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/main.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/output.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/params.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/py.typed +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/reaper.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/scaffold.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/schemas.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/sources.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/stream.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/timing.py +0 -0
- {dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/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.3
|
|
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.3"
|
|
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.3"
|
|
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
|
|
|
@@ -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 --keep-state 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
|
|
@@ -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__"
|
|
@@ -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 --keep-state[/] [dim]# plain dg dev
|
|
296
|
+
"\n [bold]uv run dg dev --keep-state[/] [dim]# plain dg dev empties .dirigent/state first[/]"
|
|
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
|
|
File without changes
|
{dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl
RENAMED
|
File without changes
|
{dirigent_cli-0.9.2 → dirigent_cli-0.9.3}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|