dirigent-cli 0.10.2__tar.gz → 0.12.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.
Files changed (33) hide show
  1. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/PKG-INFO +1 -1
  2. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/pyproject.toml +1 -1
  3. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/pyproject.toml.orig +1 -1
  4. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/commands.py +11 -9
  5. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/context.py +0 -7
  6. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/main.py +38 -19
  7. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/summaries.py +6 -1
  8. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/LICENSE +0 -0
  9. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/README.md +0 -0
  10. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/__init__.py +0 -0
  11. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/aliases.py +0 -0
  12. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/formatters.py +0 -0
  13. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/graph.py +0 -0
  14. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/health.py +0 -0
  15. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/init_form.py +0 -0
  16. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/local.py +0 -0
  17. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/output.py +0 -0
  18. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/params.py +0 -0
  19. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/profiles.py +0 -0
  20. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/project.py +0 -0
  21. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/py.typed +0 -0
  22. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/reaper.py +0 -0
  23. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/scaffold.py +0 -0
  24. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/schemas.py +0 -0
  25. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/sources.py +0 -0
  26. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/stream.py +0 -0
  27. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
  28. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
  29. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
  30. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
  31. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
  32. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/src/dirigent_cli/timing.py +0 -0
  33. {dirigent_cli-0.10.2 → dirigent_cli-0.12.0}/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.10.2
3
+ Version: 0.12.0
4
4
  Summary: The dirigent command line interface (dirigent / dg).
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.10.2"
3
+ version = "0.12.0"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.10.2"
3
+ version = "0.12.0"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -32,7 +32,6 @@ from dirigent_cli.local import (
32
32
  from dirigent_cli.output import (
33
33
  Detail,
34
34
  age,
35
- configure,
36
35
  console,
37
36
  elapsed,
38
37
  emit_event,
@@ -510,10 +509,6 @@ def init_command(
510
509
 
511
510
  from dirigent_core.auth import MIN_PASSWORD_LENGTH, WeakPassword
512
511
 
513
- state = state_of(ctx)
514
- # Run once by a person at a terminal, so it renders unless records were asked for.
515
- if not state.chosen:
516
- configure(output="console")
517
512
  root = directory.resolve()
518
513
  version = cli_version()
519
514
  unasked = template is None and service is None and pack is None and not workflow
@@ -1523,7 +1518,7 @@ def log_events(entries: list[LogEntryOut], labels: dict[UUID, str] | None = None
1523
1518
 
1524
1519
  def print_events(events: list[WatchEvent], level: Detail = Detail.SUMMARY, *, out: Sink | None = None) -> None:
1525
1520
  """Write transitions and log lines in the order they happened."""
1526
- writer = out or Sink()
1521
+ writer = out or Sink(output_mode())
1527
1522
  for when, _, subject, label in sorted(events, key=lambda event: (event[0], event[1])):
1528
1523
  if isinstance(subject, LogEntryOut):
1529
1524
  if subject.level is LogLevel.DEBUG and level is not Detail.FULL:
@@ -2365,7 +2360,7 @@ def workers_command(
2365
2360
  def system_info(
2366
2361
  ctx: typer.Context,
2367
2362
  ) -> None:
2368
- """Describe the instance, and check every connection it holds."""
2363
+ """Describe the instance, and repeat what each connection said when it was last checked."""
2369
2364
  with client_for(state_of(ctx)) as dg:
2370
2365
  info = dg.call(dg.system.info())
2371
2366
  if state_of(ctx).json_output:
@@ -2388,9 +2383,16 @@ def system_info(
2388
2383
  console.print()
2389
2384
  table(
2390
2385
  "connections",
2391
- ["code", "name", "kind", "connected", "detail"],
2386
+ ["code", "name", "kind", "last check", "healthy", "detail"],
2392
2387
  [
2393
- [row.code, row.name or "-", row.kind, render_bool(row.connected), row.detail or "-"]
2388
+ [
2389
+ row.code,
2390
+ row.name or "-",
2391
+ row.kind,
2392
+ moment(row.last_check_at),
2393
+ render_bool(row.last_check_healthy),
2394
+ row.last_check_detail or "-",
2395
+ ]
2394
2396
  for row in info.connections
2395
2397
  ],
2396
2398
  )
@@ -40,13 +40,6 @@ class CliState(BaseModel):
40
40
  debug_all: bool = False
41
41
  output: Format = "json"
42
42
 
43
- chosen: bool = False
44
- """Whether a flag or the environment named the output, rather than it being the default.
45
-
46
- A command run once by a person -- ``dg init`` -- renders unless it was asked for records,
47
- and it can only tell the difference if the default is distinguishable from a choice.
48
- """
49
-
50
43
  @property
51
44
  def json_output(self) -> bool:
52
45
  """Report whether this invocation writes records rather than a rendering."""
@@ -7,7 +7,7 @@ from collections.abc import Sequence
7
7
  from datetime import UTC, datetime, timedelta
8
8
  from importlib.metadata import PackageNotFoundError, version
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Annotated, Any, cast
10
+ from typing import TYPE_CHECKING, Annotated, Any, TextIO, cast
11
11
 
12
12
  import typer
13
13
  from pydantic import ValidationError
@@ -23,7 +23,7 @@ from dirigent_client.enums import UserRole
23
23
  from dirigent_core import migrations
24
24
  from dirigent_core.config import STATE_DIR, Settings, get_settings, redacted_url, reset_settings_cache
25
25
  from dirigent_core.logging import LOG_FORMAT_ENV, configure_logging, silence_stdout
26
- from dirigent_core.protocol import FORMATS, Format, Record, as_json, make
26
+ from dirigent_core.protocol import FORMATS, Format, Record, make
27
27
  from dirigent_core.telemetry import configure_telemetry
28
28
 
29
29
  if TYPE_CHECKING:
@@ -158,22 +158,22 @@ def main_callback(
158
158
  verbose=verbose,
159
159
  debug=debug,
160
160
  debug_all=debug_all,
161
- output=_output_format(output, json_output=json_output),
162
- chosen=output is not None or json_output or LOG_FORMAT_ENV in os.environ,
161
+ output=resolve_output(output, json_output=json_output),
163
162
  )
164
163
  ctx.obj.configure_output()
165
164
 
166
165
 
167
- def _output_format(named: str | None, *, json_output: bool) -> Format:
168
- """Resolve the output: the flag, then the environment, then NDJSON.
166
+ def resolve_output(named: str | None, *, json_output: bool) -> Format:
167
+ """Resolve the output: the flag, then the environment, then the terminal.
169
168
 
170
169
  ``--json`` is the same request as ``--output json``, and the environment is where a
171
- container says it once. NDJSON is what an unasked invocation writes, so what a command
172
- emits does not depend on who happens to be reading it; ``-o console`` renders it.
170
+ container says it once. Unasked, a terminal gets the rendering and anything else gets
171
+ NDJSON: a pipe, a container's log, an agent's shell and CI are never terminals, so a
172
+ script reads records without asking, and a person reads lines without asking.
173
173
  """
174
174
  chosen = named or ("json" if json_output else None) or os.environ.get(LOG_FORMAT_ENV)
175
175
  if chosen is None:
176
- return "json"
176
+ return "console" if sys.stdout.isatty() else "json"
177
177
  resolved = chosen.lower()
178
178
  if resolved not in FORMATS:
179
179
  # The output that was asked for is exactly what is missing, so the refusal is written
@@ -643,7 +643,7 @@ def server(
643
643
  os.environ[UI_ENV] = "true" if ui else "false"
644
644
  reset_settings_cache()
645
645
  settings = get_settings()
646
- configure_logging(_level(ctx, settings), "json", cap_foreign=_cap_foreign(ctx), stream=sys.stdout)
646
+ _process_logging(_level(ctx, settings), cap_foreign=_cap_foreign(ctx))
647
647
  if settings.is_sqlite and settings.scheduler_enabled:
648
648
  refuse(
649
649
  "dg server embeds the scheduler, and leadership is a PostgreSQL advisory lock: on SQLite "
@@ -687,7 +687,7 @@ def docker_reap(
687
687
  from dirigent_cli.stream import Sink
688
688
 
689
689
  settings = get_settings()
690
- configure_logging(_level(ctx, settings), "json", cap_foreign=_cap_foreign(ctx), stream=sys.stderr)
690
+ _process_logging(_level(ctx, settings), cap_foreign=_cap_foreign(ctx), stream=sys.stderr)
691
691
  if not reaper.reachable():
692
692
  refuse(
693
693
  "docker is not on this host's PATH, so there is no daemon to reap stacks from",
@@ -830,11 +830,9 @@ def dev(
830
830
  settings = get_settings()
831
831
  # A developer convenience starts its own logging at WARNING and -v is what asks for more,
832
832
  # while a named DIRIGENT_LOG_LEVEL still wins.
833
- configure_logging(
833
+ _process_logging(
834
834
  state.level if state is not None else "WARNING",
835
- "json",
836
835
  cap_foreign=state is None or not state.debug_all,
837
- stream=sys.stdout,
838
836
  )
839
837
  if not settings.is_sqlite:
840
838
  refuse(
@@ -919,18 +917,39 @@ def dev_started(
919
917
 
920
918
 
921
919
  def emit(record: Record) -> None:
922
- """Write one record to the stream and flush it: a reader is waiting on this line.
920
+ """Write one record to the stream in this invocation's output, and flush it.
923
921
 
924
922
  A reader that has gone ends the stream rather than the process: the pipe is closed and
925
923
  every later write goes to the void, while shutdown runs to completion.
926
924
  """
925
+ from dirigent_cli.output import output_mode
926
+ from dirigent_cli.stream import Sink
927
+
927
928
  try:
928
- sys.stdout.write(f"{as_json(record)}\n")
929
- sys.stdout.flush()
929
+ Sink(output_mode()).write(record)
930
930
  except BrokenPipeError:
931
931
  silence_stdout()
932
932
 
933
933
 
934
+ def _process_logging(level: str, *, cap_foreign: bool, stream: TextIO | None = None) -> None:
935
+ """Point a process's logging at its stream, in this invocation's output.
936
+
937
+ The console grammar is the one records are rendered in, so a process read at a terminal
938
+ is one stream of lines, and a process read by a pipe is one stream of records.
939
+ """
940
+ from dirigent_cli.output import output_mode
941
+ from dirigent_cli.stream import ansi
942
+
943
+ mode = output_mode()
944
+ configure_logging(
945
+ level,
946
+ mode,
947
+ cap_foreign=cap_foreign,
948
+ stream=stream if stream is not None else sys.stdout,
949
+ paint=ansi if mode == "console" else None,
950
+ )
951
+
952
+
934
953
  async def dev_admin(settings: Settings) -> tuple[str | None, str | None]:
935
954
  """Name the admin to log in as, and mint it a token where one is owed.
936
955
 
@@ -1007,7 +1026,7 @@ def worker(
1007
1026
  import asyncio
1008
1027
 
1009
1028
  settings = get_settings()
1010
- configure_logging(_level(ctx, settings), "json", cap_foreign=_cap_foreign(ctx), stream=sys.stdout)
1029
+ _process_logging(_level(ctx, settings), cap_foreign=_cap_foreign(ctx))
1011
1030
  configure_telemetry(settings)
1012
1031
  if settings.is_sqlite:
1013
1032
  refuse(
@@ -1042,7 +1061,7 @@ def scheduler_command(ctx: typer.Context) -> None:
1042
1061
  import asyncio
1043
1062
 
1044
1063
  settings = get_settings()
1045
- configure_logging(_level(ctx, settings), "json", cap_foreign=_cap_foreign(ctx), stream=sys.stdout)
1064
+ _process_logging(_level(ctx, settings), cap_foreign=_cap_foreign(ctx))
1046
1065
  configure_telemetry(settings)
1047
1066
  if settings.is_sqlite:
1048
1067
  refuse(
@@ -217,7 +217,11 @@ def _kept(record: Record) -> list[RenderableType]:
217
217
 
218
218
 
219
219
  def _issued(record: Record) -> RenderableType | None:
220
- """Hand over a minted token: the form it is used in, and that it is shown once."""
220
+ """Hand over a minted token: the form it is used in, and that it is shown once.
221
+
222
+ A login's ``token.issued`` and ``dg dev``'s ``starting`` both carry one exactly once; a
223
+ process record without a token, ``ready`` or ``state cleared``, is its line alone.
224
+ """
221
225
  token = record.get("token")
222
226
  if not token:
223
227
  return None
@@ -404,6 +408,7 @@ RENDERERS: Final[Mapping[str, Callable[[Record], RenderableType | None]]] = {
404
408
  "validation": _validation,
405
409
  "error": _refusal,
406
410
  "token.issued": _issued,
411
+ "process": _issued,
407
412
  "project.scaffolded": _scaffolded,
408
413
  "instance.initialised": _initialised,
409
414
  }
File without changes
File without changes