dirigent-cli 0.16.3__tar.gz → 0.16.5__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 (35) hide show
  1. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/PKG-INFO +10 -8
  2. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/pyproject.toml +12 -8
  3. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/pyproject.toml.orig +12 -8
  4. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/commands.py +27 -3
  5. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/main.py +33 -1
  6. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/output.py +16 -5
  7. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/LICENSE +0 -0
  8. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/README.md +0 -0
  9. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/__init__.py +0 -0
  10. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/aliases.py +0 -0
  11. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/context.py +0 -0
  12. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/formatters.py +0 -0
  13. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/graph.py +0 -0
  14. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/health.py +0 -0
  15. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/init_form.py +0 -0
  16. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/local.py +0 -0
  17. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/params.py +0 -0
  18. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/profiles.py +0 -0
  19. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/project.py +0 -0
  20. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/py.typed +0 -0
  21. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/reaper.py +0 -0
  22. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/scaffold.py +0 -0
  23. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/schemas.py +0 -0
  24. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/seeding.py +0 -0
  25. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/sources.py +0 -0
  26. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/starters.py +0 -0
  27. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/stream.py +0 -0
  28. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/summaries.py +0 -0
  29. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
  30. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
  31. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
  32. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
  33. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
  34. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/timing.py +0 -0
  35. {dirigent_cli-0.16.3 → dirigent_cli-0.16.5}/src/dirigent_cli/triggers.py +0 -0
@@ -1,16 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigent-cli
3
- Version: 0.16.3
3
+ Version: 0.16.5
4
4
  Summary: The dirigent command line interface (dirigent / dg).
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
7
- Requires-Dist: dirigent-blocks==0.16.3
8
- Requires-Dist: dirigent-client==0.16.3
9
- Requires-Dist: dirigent-common==0.16.3
10
- Requires-Dist: dirigent-core==0.16.3
11
- Requires-Dist: dirigent-examples==0.16.3
12
- Requires-Dist: dirigent-plugin==0.16.3
13
- Requires-Dist: dirigent-server==0.16.3
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Requires-Dist: dirigent-blocks==0.16.5
10
+ Requires-Dist: dirigent-client==0.16.5
11
+ Requires-Dist: dirigent-common==0.16.5
12
+ Requires-Dist: dirigent-core==0.16.5
13
+ Requires-Dist: dirigent-examples==0.16.5
14
+ Requires-Dist: dirigent-plugin==0.16.5
15
+ Requires-Dist: dirigent-server==0.16.5
14
16
  Requires-Dist: httpx2>=2.12.0
15
17
  Requires-Dist: python-dotenv>=1.1.0
16
18
  Requires-Dist: pyyaml>=6.0.3
@@ -1,19 +1,23 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.16.3"
3
+ version = "0.16.5"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  license = "LicenseRef-Proprietary"
8
8
  license-files = ["LICENSE"]
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3",
11
+ "Programming Language :: Python :: 3.13",
12
+ ]
9
13
  dependencies = [
10
- "dirigent-blocks==0.16.3",
11
- "dirigent-client==0.16.3",
12
- "dirigent-common==0.16.3",
13
- "dirigent-core==0.16.3",
14
- "dirigent-examples==0.16.3",
15
- "dirigent-plugin==0.16.3",
16
- "dirigent-server==0.16.3",
14
+ "dirigent-blocks==0.16.5",
15
+ "dirigent-client==0.16.5",
16
+ "dirigent-common==0.16.5",
17
+ "dirigent-core==0.16.5",
18
+ "dirigent-examples==0.16.5",
19
+ "dirigent-plugin==0.16.5",
20
+ "dirigent-server==0.16.5",
17
21
  "httpx2>=2.12.0",
18
22
  "python-dotenv>=1.1.0",
19
23
  "pyyaml>=6.0.3",
@@ -1,19 +1,23 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.16.3"
3
+ version = "0.16.5"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  license = "LicenseRef-Proprietary"
8
8
  license-files = ["LICENSE"]
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3",
11
+ "Programming Language :: Python :: 3.13",
12
+ ]
9
13
  dependencies = [
10
- "dirigent-blocks==0.16.3",
11
- "dirigent-client==0.16.3",
12
- "dirigent-common==0.16.3",
13
- "dirigent-core==0.16.3",
14
- "dirigent-examples==0.16.3",
15
- "dirigent-plugin==0.16.3",
16
- "dirigent-server==0.16.3",
14
+ "dirigent-blocks==0.16.5",
15
+ "dirigent-client==0.16.5",
16
+ "dirigent-common==0.16.5",
17
+ "dirigent-core==0.16.5",
18
+ "dirigent-examples==0.16.5",
19
+ "dirigent-plugin==0.16.5",
20
+ "dirigent-server==0.16.5",
17
21
  "httpx2>=2.12.0",
18
22
  "python-dotenv>=1.1.0",
19
23
  "pyyaml>=6.0.3",
@@ -170,6 +170,11 @@ def fail(message: str) -> NoReturn:
170
170
  raise typer.Exit(code=1)
171
171
 
172
172
 
173
+ def at_a_terminal() -> bool:
174
+ """Report whether somebody is there to answer a prompt."""
175
+ return sys.stdin.isatty()
176
+
177
+
173
178
  def ask(label: str, *, hide: bool = False) -> str:
174
179
  """Read one value from the terminal, refusing under --json where nothing can answer.
175
180
 
@@ -885,11 +890,21 @@ def parse_params(
885
890
  schema: dict[str, Any] | None = None,
886
891
  files: list[Path] | None = None,
887
892
  ) -> dict[str, Any]:
888
- """Build a run's parameters from files and flags, coerced against the pipeline's schema."""
893
+ """Build a run's parameters from files and flags, coerced against the pipeline's schema.
894
+
895
+ An empty value for a secret field means unset: ``--set api_token=`` leaves the field out
896
+ altogether, so nothing stores a credential that every read afterwards reports as set.
897
+ """
889
898
  try:
890
- return build_params(schema or {}, pairs=pairs or [], files=files or [])
899
+ built = build_params(schema or {}, pairs=pairs or [], files=files or [])
891
900
  except ParamError as error:
892
901
  fail(str(error))
902
+ properties = cast("dict[str, Any]", (schema or {}).get("properties") or {})
903
+ return {
904
+ name: value
905
+ for name, value in built.items()
906
+ if value != "" or not _is_secret_field(cast("dict[str, Any]", properties.get(name) or {}))
907
+ }
893
908
 
894
909
 
895
910
  #: What separates the two ends of a ``--window`` value.
@@ -2224,12 +2239,19 @@ def connection_create(
2224
2239
  """Create a connection, prompting for the secret fields without echoing them.
2225
2240
 
2226
2241
  Prompting happens only on a terminal; in a script every value must arrive via ``--set``.
2242
+ An invocation carrying ``--set`` is prompted only for what it cannot do without: a
2243
+ required field it left out. A bare invocation is the interactive form and is offered
2244
+ every secret the kind declares.
2227
2245
  """
2228
2246
  with client_for(state_of(ctx)) as dg:
2229
2247
  schema = _connection_schema(dg.call(dg.blocks.catalog()), kind_id)
2230
2248
  config = parse_params(set_value, schema=schema)
2231
2249
  required = set(cast("list[str]", schema.get("required") or []))
2232
- interactive = sys.stdin.isatty() and not json_mode()
2250
+ interactive = at_a_terminal() and not json_mode()
2251
+ # An invocation that carried --set said what it wanted, so only a required secret is
2252
+ # still asked for there: an optional one it left out is left out. With no --set at
2253
+ # all the command is the form, and every secret is offered.
2254
+ offered = not set_value
2233
2255
  for field, body in cast("dict[str, Any]", schema.get("properties") or {}).items():
2234
2256
  if field in config:
2235
2257
  continue
@@ -2241,6 +2263,8 @@ def connection_create(
2241
2263
  if not interactive:
2242
2264
  continue
2243
2265
  if _is_secret_field(body):
2266
+ if not offered and field not in required:
2267
+ continue
2244
2268
  value = typer.prompt(f"{field}", hide_input=True, default="", show_default=False)
2245
2269
  if value:
2246
2270
  config[field] = value
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  import shutil
5
5
  import sys
6
- from collections.abc import Sequence
6
+ from collections.abc import Callable, Sequence
7
7
  from datetime import UTC, datetime, timedelta
8
8
  from importlib.metadata import PackageNotFoundError, version
9
9
  from pathlib import Path
@@ -1076,8 +1076,15 @@ async def _dev(
1076
1076
  install_signal_handlers(worker)
1077
1077
  server_config = uvicorn.Config(build_app(), host=host, port=port, log_config=None)
1078
1078
  api = uvicorn.Server(server_config)
1079
+
1080
+ def shut_down() -> None:
1081
+ """Ask for the shutdown SIGTERM asks for: the worker drains, and serve returns."""
1082
+ worker.request_stop()
1083
+ api.should_exit = True
1084
+
1079
1085
  worker_task = asyncio.create_task(worker.run())
1080
1086
  ready = asyncio.create_task(_announce_ready(api))
1087
+ orphaned = asyncio.create_task(watch_parent(os.getppid, shut_down))
1081
1088
  seeding = (
1082
1089
  asyncio.create_task(_seed(api, local_url(host, port), bearer, seed, installed)) if seed or installed else None
1083
1090
  )
@@ -1085,6 +1092,7 @@ async def _dev(
1085
1092
  await api.serve()
1086
1093
  finally:
1087
1094
  ready.cancel()
1095
+ orphaned.cancel()
1088
1096
  if seeding is not None:
1089
1097
  seeding.cancel()
1090
1098
  worker.request_stop()
@@ -1107,6 +1115,30 @@ async def _announce_ready(api: "uvicorn.Server") -> None:
1107
1115
  emit(make("process", at=datetime.now(UTC), message="ready", process="dev"))
1108
1116
 
1109
1117
 
1118
+ #: How often the instance looks at whether the process that started it is still there.
1119
+ PARENT_POLL_SECONDS = 1.0
1120
+
1121
+
1122
+ async def watch_parent(
1123
+ parent: Callable[[], int], stop: Callable[[], None], *, every: float = PARENT_POLL_SECONDS
1124
+ ) -> None:
1125
+ """Shut the instance down once the process that started it is gone.
1126
+
1127
+ ``uv run dg dev`` forwards SIGTERM to the instance, but a wrapper that was killed outright
1128
+ cannot, and the instance it leaves behind goes on claiming runs against the same state.
1129
+ macOS has no PR_SET_PDEATHSIG, so the parent pid is polled: a pid that changed means this
1130
+ process was reparented to init. A dev instance therefore does not outlive the process that
1131
+ started it, backgrounding it past the shell that launched it included.
1132
+ """
1133
+ import asyncio
1134
+
1135
+ started = parent()
1136
+ while parent() == started:
1137
+ await asyncio.sleep(every)
1138
+ emit(make("process", at=datetime.now(UTC), message="parent gone", process="dev", parent=started))
1139
+ stop()
1140
+
1141
+
1110
1142
  async def _seed(
1111
1143
  api: "uvicorn.Server",
1112
1144
  url: str,
@@ -16,6 +16,7 @@ from pydantic import BaseModel
16
16
  from rich.console import Console, Group, RenderableType
17
17
  from rich.markup import escape
18
18
  from rich.table import Table
19
+ from rich.text import Text
19
20
 
20
21
  from dirigent_client.schemas.common import Problem
21
22
  from dirigent_core.protocol import Format, Record, as_json, make
@@ -500,16 +501,26 @@ def elapsed(value: object) -> str:
500
501
  return f"{int(hours)}h{int(minutes)}m"
501
502
 
502
503
 
504
+ def titled(title: str, built: Table) -> RenderableType:
505
+ """Put a title above a table, on a line of its own.
506
+
507
+ Rich folds a table's own title to the width of the table, which the widest row sets, so a
508
+ title carrying a 36-character run id breaks in half wherever the rows are narrower than
509
+ it. Above the table the title is folded only by the console.
510
+ """
511
+ return Group(Text.from_markup(title, style="table.title"), built)
512
+
513
+
503
514
  def build_table(title: str, columns: Sequence[str], rows: Iterable[Sequence[str]]) -> RenderableType:
504
515
  """Build one rich table, or the quiet line that stands in for an empty one."""
505
- built = Table(title=title, show_header=True, header_style="bold", title_justify="left")
516
+ built = Table(show_header=True, header_style="bold")
506
517
  for column in columns:
507
518
  built.add_column(column)
508
519
  count = 0
509
520
  for row in rows:
510
521
  built.add_row(*row)
511
522
  count += 1
512
- return built if count else f"[dim]{title}: nothing to show.[/]"
523
+ return titled(title, built) if count else f"[dim]{title}: nothing to show.[/]"
513
524
 
514
525
 
515
526
  def table(title: str, columns: Sequence[str], rows: Iterable[Sequence[str]]) -> None:
@@ -518,13 +529,13 @@ def table(title: str, columns: Sequence[str], rows: Iterable[Sequence[str]]) ->
518
529
 
519
530
 
520
531
  def build_fields(title: str, values: Mapping[str, Any]) -> RenderableType:
521
- """Build one record as a two-column table."""
522
- built = Table(title=title, show_header=False, box=None, title_justify="left", padding=(0, 2, 0, 0))
532
+ """Build one record as a two-column table under its title."""
533
+ built = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
523
534
  built.add_column(style="bold")
524
535
  built.add_column()
525
536
  for name, value in values.items():
526
537
  built.add_row(name, styled(value))
527
- return built
538
+ return titled(title, built)
528
539
 
529
540
 
530
541
  def fields(title: str, values: Mapping[str, Any]) -> None:
File without changes
File without changes