dirigent-block-execute 0.17.2__tar.gz → 0.17.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.
Files changed (18) hide show
  1. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/PKG-INFO +4 -4
  2. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/pyproject.toml +4 -4
  3. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/pyproject.toml.orig +4 -4
  4. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/build.py +2 -6
  5. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/compose.py +6 -14
  6. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/docker.py +2 -7
  7. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/git.py +3 -6
  8. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/shell.py +1 -6
  9. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/subprocess.py +27 -20
  10. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/LICENSE +0 -0
  11. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/README.md +0 -0
  12. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/__init__.py +0 -0
  13. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/capture.py +0 -0
  14. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/environment.py +0 -0
  15. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/messages.py +0 -0
  16. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/py.typed +0 -0
  17. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/reap.py +0 -0
  18. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.17.3}/src/dirigent_block_execute/secrets.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigent-block-execute
3
- Version: 0.17.2
3
+ Version: 0.17.3
4
4
  Summary: The execute block family for dirigent: shell, docker, compose, buildx, and a checkout.
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3.13
9
- Requires-Dist: dirigent-block-http==0.17.2
10
- Requires-Dist: dirigent-common==0.17.2
11
- Requires-Dist: dirigent-plugin==0.17.2
9
+ Requires-Dist: dirigent-block-http==0.17.3
10
+ Requires-Dist: dirigent-common==0.17.3
11
+ Requires-Dist: dirigent-plugin==0.17.3
12
12
  Requires-Dist: httpx2>=2.12.0
13
13
  Requires-Python: >=3.13
14
14
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-block-execute"
3
- version = "0.17.2"
3
+ version = "0.17.3"
4
4
  description = "The execute block family for dirigent: shell, docker, compose, buildx, and a checkout."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,9 +11,9 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-http==0.17.2",
15
- "dirigent-common==0.17.2",
16
- "dirigent-plugin==0.17.2",
14
+ "dirigent-block-http==0.17.3",
15
+ "dirigent-common==0.17.3",
16
+ "dirigent-plugin==0.17.3",
17
17
  "httpx2>=2.12.0",
18
18
  ]
19
19
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-block-execute"
3
- version = "0.17.2"
3
+ version = "0.17.3"
4
4
  description = "The execute block family for dirigent: shell, docker, compose, buildx, and a checkout."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,9 +11,9 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-http==0.17.2",
15
- "dirigent-common==0.17.2",
16
- "dirigent-plugin==0.17.2",
14
+ "dirigent-block-http==0.17.3",
15
+ "dirigent-common==0.17.3",
16
+ "dirigent-plugin==0.17.3",
17
17
  "httpx2>=2.12.0",
18
18
  ]
19
19
 
@@ -176,22 +176,18 @@ class DockerBuildOperator(Operator[DockerBuildConfig, DockerBuildOutput]):
176
176
  if config.push and (settings is None or not settings.authenticates):
177
177
  raise BlockFailure(NO_REGISTRY_CREDENTIAL, error_class=ErrorClass.REJECTED)
178
178
  timeout = config.timeout.total_seconds()
179
- artifacts = subprocess.prefix(ctx, "build")
180
- stdout_uri = f"{artifacts}-stdout.txt"
181
- build_log_uri = f"{artifacts}-build.log"
182
179
 
183
180
  with sealed(settings, root, isolate_config=config.push) as material:
184
181
  environ = daemon_environment(
185
182
  subprocess.environment([*DAEMON_ENV, *config.env_allowlist], config.env, root), material
186
183
  )
187
184
  write_cli_config(root / ".docker")
188
- code, out, err = await subprocess.run(
185
+ code, out, err, stdout_uri, build_log_uri = await subprocess.run(
189
186
  directory=root,
190
187
  ctx=ctx,
191
- stdout_uri=stdout_uri,
192
- stderr_uri=build_log_uri,
193
188
  timeout_seconds=timeout,
194
189
  environ=environ,
190
+ stderr_name="build.log",
195
191
  argv=build_argv(config, context, iidfile),
196
192
  what="docker buildx build",
197
193
  redact=material.secrets,
@@ -342,7 +342,6 @@ class DockerComposeUpOperator(Operator[DockerComposeUpConfig, DockerComposeUpOut
342
342
  assert compose_file is not None
343
343
  project = config.project_name or _default_project(ctx)
344
344
  timeout = config.timeout.total_seconds()
345
- stdout_uri, stderr_uri = _artifact_uris(ctx, "up")
346
345
 
347
346
  with sealed(_connection(config.connection, ctx), root) as material:
348
347
  environ = daemon_environment(
@@ -350,13 +349,13 @@ class DockerComposeUpOperator(Operator[DockerComposeUpConfig, DockerComposeUpOut
350
349
  )
351
350
  write_cli_config(root / ".docker")
352
351
  try:
353
- code, out, err = await subprocess.run(
352
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
354
353
  directory=root,
355
354
  ctx=ctx,
356
- stdout_uri=stdout_uri,
357
- stderr_uri=stderr_uri,
358
355
  timeout_seconds=timeout,
359
356
  environ=environ,
357
+ stdout_name="up-stdout",
358
+ stderr_name="up-stderr",
360
359
  argv=up_argv(config, project, compose_file, root),
361
360
  what="docker compose up",
362
361
  )
@@ -411,20 +410,19 @@ class DockerComposeDownOperator(Operator[DockerComposeDownConfig, DockerComposeD
411
410
  compose_file = _compose_file(config.file, config.content, ctx, root)
412
411
  project = config.project_name or _default_project(ctx)
413
412
  timeout = config.timeout.total_seconds()
414
- stdout_uri, stderr_uri = _artifact_uris(ctx, "down")
415
413
 
416
414
  with sealed(_connection(config.connection, ctx), root) as material:
417
415
  environ = daemon_environment(
418
416
  subprocess.environment([*DAEMON_ENV, *config.env_allowlist], config.env, root), material
419
417
  )
420
418
  write_cli_config(root / ".docker")
421
- code, out, err = await subprocess.run(
419
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
422
420
  directory=root,
423
421
  ctx=ctx,
424
- stdout_uri=stdout_uri,
425
- stderr_uri=stderr_uri,
426
422
  timeout_seconds=timeout,
427
423
  environ=environ,
424
+ stdout_name="down-stdout",
425
+ stderr_name="down-stderr",
428
426
  argv=down_argv(config, project, compose_file, root),
429
427
  what="docker compose down",
430
428
  )
@@ -656,12 +654,6 @@ def _work_relative(path: Path, root: Path) -> str:
656
654
  return str(path.relative_to(root))
657
655
 
658
656
 
659
- def _artifact_uris(ctx: StepContext, action: str) -> tuple[str, str]:
660
- """Where the CLI's two streams are written for this attempt."""
661
- root = f"{subprocess.prefix(ctx, 'compose')}-{action}"
662
- return f"{root}-stdout.txt", f"{root}-stderr.txt"
663
-
664
-
665
657
  def _default_project(ctx: StepContext) -> str:
666
658
  """A project name that is the same across a run's up, drive and down, and unique between runs."""
667
659
  return f"dirigent-{ctx.run_id.hex}"
@@ -582,21 +582,16 @@ class DockerRunOperator(Operator[DockerRunConfig, DockerRunOutput]):
582
582
 
583
583
  async def fetch(self, handle: RemoteHandle, config: DockerRunConfig, ctx: StepContext) -> DockerRunOutput:
584
584
  """Collect the exit code, the streams, and the declared files, then drop the container."""
585
- artifacts = subprocess.prefix(ctx, "docker")
586
- stdout_uri = f"{artifacts}-stdout.txt"
587
- stderr_uri = f"{artifacts}-stderr.txt"
588
585
  with _material(config, ctx) as environ:
589
586
  async with _daemon(config, environ) as daemon:
590
587
  inspected = await daemon.inspect(handle.ref)
591
588
  if inspected is None:
592
589
  raise BlockFailure(CONTAINER_GONE, error_class=ErrorClass.TRANSIENT, container=handle.ref[:12])
593
- async with (
594
- ctx.storage.open_write(stdout_uri) as out_sink,
595
- ctx.storage.open_write(stderr_uri) as err_sink,
596
- ):
590
+ async with ctx.capture("stdout") as out_sink, ctx.capture("stderr") as err_sink:
597
591
  stdout, stderr = await drain_logs(
598
592
  daemon, handle.ref, ctx.inline_capture, sinks=(out_sink, err_sink)
599
593
  )
594
+ stdout_uri, stderr_uri = out_sink.uri, err_sink.uri
600
595
  collected = await _collect_outputs(config, handle, ctx)
601
596
  # The run log gets only what the probes have not already streamed: the segment
602
597
  # between the last committed cursor and the exit. The full read above is for
@@ -381,16 +381,13 @@ class _Git:
381
381
  artifact rather than a buffer in the worker. Each command writes its own pair, named for
382
382
  its subcommand; the pair the last one wrote is what the output carries.
383
383
  """
384
- artifacts = f"{subprocess.prefix(self.ctx, 'git')}-{argv[0]}"
385
- stdout_uri = f"{artifacts}-stdout.txt"
386
- stderr_uri = f"{artifacts}-stderr.txt"
387
- code, out, err = await subprocess.run(
384
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
388
385
  directory=directory,
389
386
  ctx=self.ctx,
390
- stdout_uri=stdout_uri,
391
- stderr_uri=stderr_uri,
392
387
  timeout_seconds=self.timeout,
393
388
  environ=self.environ,
389
+ stdout_name=f"{argv[0]}-stdout",
390
+ stderr_name=f"{argv[0]}-stderr",
394
391
  argv=[self.binary, *argv],
395
392
  what=f"git {argv[0]}",
396
393
  redact=self.secrets,
@@ -136,14 +136,9 @@ class ShellRunOperator(Operator[ShellRunConfig, ShellRunOutput]):
136
136
  workspace = subprocess.workspace(ctx, "shell")
137
137
  directory = workspace / config.cwd if config.cwd else workspace
138
138
  directory.mkdir(parents=True, exist_ok=True)
139
- artifacts = subprocess.prefix(ctx, "shell")
140
- stdout_uri = f"{artifacts}-stdout.txt"
141
- stderr_uri = f"{artifacts}-stderr.txt"
142
- code, out, err = await subprocess.run(
139
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
143
140
  directory=directory,
144
141
  ctx=ctx,
145
- stdout_uri=stdout_uri,
146
- stderr_uri=stderr_uri,
147
142
  timeout_seconds=config.timeout.total_seconds(),
148
143
  environ=_environment(config, directory),
149
144
  argv=config.argv or None,
@@ -14,6 +14,7 @@ import os
14
14
  import signal
15
15
  from collections.abc import Sequence
16
16
  from pathlib import Path
17
+ from typing import NamedTuple
17
18
 
18
19
  from dirigent_block_execute.capture import Drained, LiveLines, drain
19
20
  from dirigent_block_execute.environment import allowed
@@ -24,22 +25,26 @@ from dirigent_plugin import BlockFailure, ErrorClass, StepContext
24
25
  BASELINE_ENV = ("PATH", "LANG", "LC_ALL", "TZ")
25
26
 
26
27
 
28
+ class Ran(NamedTuple):
29
+ """What one child process left behind: how it exited, both streams, and where they were written."""
30
+
31
+ code: int
32
+ out: Drained
33
+ err: Drained
34
+ stdout_uri: str
35
+ stderr_uri: str
36
+
37
+
27
38
  def segment(ctx: StepContext, name: str) -> str:
28
- """The path this attempt's files sit at, under either of the run's two roots.
39
+ """The path this attempt's files sit at, inside the run's work directory.
29
40
 
30
- ``{name}/{step}[/{item}]/attempt-{n}``. Both roots are the run's, so the step key and,
31
- inside a fan-out, the item id are what keep two steps of one run and two items of one step
32
- out of each other's files and working directories.
41
+ ``{name}/{step}[/{item}]/attempt-{n}``. The step key, and inside a fan-out the item id,
42
+ are what keep two steps of one run and two items of one step out of each other's files.
33
43
  """
34
44
  item = f"/{ctx.run_item_id}" if ctx.run_item_id is not None else ""
35
45
  return f"{name}/{ctx.step}{item}/attempt-{ctx.attempt}"
36
46
 
37
47
 
38
- def prefix(ctx: StepContext, name: str) -> str:
39
- """The storage URI this attempt's artifact names are built on, under the run's scratch prefix."""
40
- return f"{ctx.scratch.rstrip('/')}/{segment(ctx, name)}"
41
-
42
-
43
48
  def workspace(ctx: StepContext, name: str) -> Path:
44
49
  """This attempt's working directory inside the run's work directory, made if absent."""
45
50
  directory = ctx.work / segment(ctx, name)
@@ -59,22 +64,24 @@ async def run(
59
64
  *,
60
65
  directory: Path,
61
66
  ctx: StepContext,
62
- stdout_uri: str,
63
- stderr_uri: str,
64
67
  timeout_seconds: float,
65
68
  environ: dict[str, str],
69
+ stdout_name: str = "stdout",
70
+ stderr_name: str = "stderr",
66
71
  argv: list[str] | None = None,
67
72
  command: str | None = None,
68
73
  what: str = "the command",
69
74
  redact: Sequence[str] = (),
70
- ) -> tuple[int, Drained, Drained]:
75
+ ) -> Ran:
71
76
  """Start the process, drain it under the timeout, and leave nothing of it behind.
72
77
 
73
- Both pipes are read as the process writes them and written straight through to storage,
74
- so what is held is two bounded ends of each stream rather than all of it: a process that
75
- prints more than the worker has memory for is a file, not a dead worker. Each stream's
76
- first lines go into the run log where they are printed, so a long command is visible
77
- working; ``redact`` names the strings it was handed that must not reach one of them.
78
+ Both pipes are read as the process writes them and written straight through to a capture
79
+ the context opens and names, so what is held is two bounded ends of each stream rather
80
+ than all of it: a process that prints more than the worker has memory for is a file, not
81
+ a dead worker. The two names distinguish one command's streams from another's where a
82
+ step runs more than one. Each stream's first lines go into the run log where they are
83
+ printed, so a long command is visible working; ``redact`` names the strings it was
84
+ handed that must not reach one of them.
78
85
 
79
86
  The process leads a session of its own, so what it starts is killable with it: a command
80
87
  is usually ``sh -c``, and killing the shell alone leaves the children doing the work.
@@ -106,7 +113,7 @@ async def run(
106
113
  pgid = group_of(process)
107
114
  try:
108
115
  async with asyncio.timeout(timeout_seconds):
109
- async with ctx.storage.open_write(stdout_uri) as out_sink, ctx.storage.open_write(stderr_uri) as err_sink:
116
+ async with ctx.capture(stdout_name) as out_sink, ctx.capture(stderr_name) as err_sink:
110
117
  out, err = await asyncio.gather(
111
118
  drain(
112
119
  process.stdout,
@@ -126,7 +133,7 @@ async def run(
126
133
  raise BlockFailure(TIMED_OUT, error_class=ErrorClass.TRANSIENT, what=what, seconds=timeout_seconds) from error
127
134
  finally:
128
135
  await end(process, pgid)
129
- return process.returncode or 0, out, err
136
+ return Ran(process.returncode or 0, out, err, out_sink.uri, err_sink.uri)
130
137
 
131
138
 
132
139
  async def output(
@@ -207,12 +214,12 @@ def kill(pgid: int, pid: int) -> None:
207
214
 
208
215
  __all__ = [
209
216
  "BASELINE_ENV",
217
+ "Ran",
210
218
  "end",
211
219
  "environment",
212
220
  "group_of",
213
221
  "kill",
214
222
  "output",
215
- "prefix",
216
223
  "run",
217
224
  "segment",
218
225
  "workspace",