cuprum 0.1.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.
cuprum-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.3
2
+ Name: cuprum
3
+ Version: 0.1.0
4
+ Summary: cuprum package
5
+ Author: Payton McIntosh
6
+ Author-email: Payton McIntosh <pmcintosh@df12.net>
7
+ License: ISC
8
+ Requires-Python: >=3.12
9
+ Project-URL: Homepage, https://df12.studio/docs.html
10
+ Project-URL: Repository, https://github.com/leynos/cuprum
11
+ Description-Content-Type: text/markdown
12
+
13
+ # cuprum
14
+
15
+ Typed, async command execution for Python—so you can ditch the shell scripts
16
+ without losing your mind.
17
+
18
+ ## What is this?
19
+
20
+ If you've ever written a Python script that calls out to external commands,
21
+ you've probably experienced the joys of `subprocess`: stringly-typed arguments,
22
+ mysterious failures, and output that vanishes into the void. Cuprum is here to
23
+ help.
24
+
25
+ We give you a **typed, safe approach to running external programs**. Instead of
26
+ passing arbitrary strings to a shell, you work with a curated catalogue of
27
+ approved executables. Each command carries metadata about its project, so
28
+ downstream tooling knows how to filter noise from logs or where to find
29
+ documentation.
30
+
31
+ Cuprum is async-first but provides synchronous wrappers for scripts that don't
32
+ need the full async machinery. Whether you're building deployment helpers, CI
33
+ glue, or maintenance scripts, we want "Python instead of Bash" to feel like an
34
+ upgrade rather than a chore.
35
+
36
+ ## Quick taste
37
+
38
+ ```python
39
+ from cuprum import ECHO, ExecutionContext, sh
40
+
41
+ # Create a builder for a curated program
42
+ echo = sh.make(ECHO)
43
+
44
+ # Build a command with typed arguments
45
+ cmd = echo("-n", "hello, cuprum!")
46
+
47
+ # Run it (async)
48
+ result = await cmd.run(echo=True)
49
+ if result.ok:
50
+ print(f"Output: {result.stdout}")
51
+
52
+ # Or run it synchronously
53
+ result = cmd.run_sync()
54
+ ```
55
+
56
+ ## Features
57
+
58
+ - **Catalogue-based safety** – Only approved programs can run; unknown
59
+ executables raise `UnknownProgramError`.
60
+ - **Typed command building** – `sh.make()` returns builders that validate
61
+ arguments and carry project metadata.
62
+ - **Async-first execution** – `await cmd.run()` with capture/echo toggles,
63
+ environment overlays, and working directory control.
64
+ - **Synchronous convenience** – `cmd.run_sync()` when you don't need async.
65
+ - **Graceful cancellation** – Cancelled tasks send `SIGTERM`, wait briefly,
66
+ then escalate to `SIGKILL`.
67
+ - **Structured results** – `CommandResult` gives you exit code, pid, stdout,
68
+ stderr, and an `ok` helper.
69
+ - **Zero dependencies** – Just Python 3.12+ and the standard library.
70
+
71
+ ## Installation
72
+
73
+ ```shell
74
+ pip install cuprum
75
+ ```
76
+
77
+ Or with [uv](https://docs.astral.sh/uv/):
78
+
79
+ ```shell
80
+ uv add cuprum
81
+ ```
82
+
83
+ ## Status
84
+
85
+ Cuprum is in early development. The typed command core and execution runtime
86
+ are complete (Phase 1 of the [roadmap](docs/roadmap.md)), but context-scoped
87
+ allowlists and hooks are still on the way. The API may shift as we learn what
88
+ works best.
89
+
90
+ ## Documentation
91
+
92
+ For the full guide—including how to build your own program catalogues, write
93
+ project-specific builders, and control execution contexts—see the
94
+ [users' guide](docs/users-guide.md).
95
+
96
+ ## Why "cuprum"?
97
+
98
+ The name is a tip of the hat to [Plumbum](https://plumbum.readthedocs.io/), the
99
+ library that showed us shell-like scripting in Python could actually be
100
+ pleasant. "Cuprum" is Latin for copper—another metal used in pipes—and we hope
101
+ to carry that spirit forward with a focus on type safety and explicit
102
+ allowlists.
103
+
104
+ ## Licence
105
+
106
+ [ISC](LICENSE)
cuprum-0.1.0/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # cuprum
2
+
3
+ Typed, async command execution for Python—so you can ditch the shell scripts
4
+ without losing your mind.
5
+
6
+ ## What is this?
7
+
8
+ If you've ever written a Python script that calls out to external commands,
9
+ you've probably experienced the joys of `subprocess`: stringly-typed arguments,
10
+ mysterious failures, and output that vanishes into the void. Cuprum is here to
11
+ help.
12
+
13
+ We give you a **typed, safe approach to running external programs**. Instead of
14
+ passing arbitrary strings to a shell, you work with a curated catalogue of
15
+ approved executables. Each command carries metadata about its project, so
16
+ downstream tooling knows how to filter noise from logs or where to find
17
+ documentation.
18
+
19
+ Cuprum is async-first but provides synchronous wrappers for scripts that don't
20
+ need the full async machinery. Whether you're building deployment helpers, CI
21
+ glue, or maintenance scripts, we want "Python instead of Bash" to feel like an
22
+ upgrade rather than a chore.
23
+
24
+ ## Quick taste
25
+
26
+ ```python
27
+ from cuprum import ECHO, ExecutionContext, sh
28
+
29
+ # Create a builder for a curated program
30
+ echo = sh.make(ECHO)
31
+
32
+ # Build a command with typed arguments
33
+ cmd = echo("-n", "hello, cuprum!")
34
+
35
+ # Run it (async)
36
+ result = await cmd.run(echo=True)
37
+ if result.ok:
38
+ print(f"Output: {result.stdout}")
39
+
40
+ # Or run it synchronously
41
+ result = cmd.run_sync()
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - **Catalogue-based safety** – Only approved programs can run; unknown
47
+ executables raise `UnknownProgramError`.
48
+ - **Typed command building** – `sh.make()` returns builders that validate
49
+ arguments and carry project metadata.
50
+ - **Async-first execution** – `await cmd.run()` with capture/echo toggles,
51
+ environment overlays, and working directory control.
52
+ - **Synchronous convenience** – `cmd.run_sync()` when you don't need async.
53
+ - **Graceful cancellation** – Cancelled tasks send `SIGTERM`, wait briefly,
54
+ then escalate to `SIGKILL`.
55
+ - **Structured results** – `CommandResult` gives you exit code, pid, stdout,
56
+ stderr, and an `ok` helper.
57
+ - **Zero dependencies** – Just Python 3.12+ and the standard library.
58
+
59
+ ## Installation
60
+
61
+ ```shell
62
+ pip install cuprum
63
+ ```
64
+
65
+ Or with [uv](https://docs.astral.sh/uv/):
66
+
67
+ ```shell
68
+ uv add cuprum
69
+ ```
70
+
71
+ ## Status
72
+
73
+ Cuprum is in early development. The typed command core and execution runtime
74
+ are complete (Phase 1 of the [roadmap](docs/roadmap.md)), but context-scoped
75
+ allowlists and hooks are still on the way. The API may shift as we learn what
76
+ works best.
77
+
78
+ ## Documentation
79
+
80
+ For the full guide—including how to build your own program catalogues, write
81
+ project-specific builders, and control execution contexts—see the
82
+ [users' guide](docs/users-guide.md).
83
+
84
+ ## Why "cuprum"?
85
+
86
+ The name is a tip of the hat to [Plumbum](https://plumbum.readthedocs.io/), the
87
+ library that showed us shell-like scripting in Python could actually be
88
+ pleasant. "Cuprum" is Latin for copper—another metal used in pipes—and we hope
89
+ to carry that spirit forward with a focus on type safety and explicit
90
+ allowlists.
91
+
92
+ ## Licence
93
+
94
+ [ISC](LICENSE)
@@ -0,0 +1,99 @@
1
+ """cuprum package.
2
+
3
+ Provides a typed programme catalogue system for managing curated, allowlisted
4
+ executables. Re-exports core types and the default catalogue for convenience.
5
+
6
+ Example:
7
+ >>> from cuprum import DEFAULT_CATALOGUE, ECHO
8
+ >>> entry = DEFAULT_CATALOGUE.lookup(ECHO)
9
+ >>> entry.project_name
10
+ 'core-ops'
11
+
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from cuprum.catalogue import (
17
+ CORE_OPS_PROJECT,
18
+ DEFAULT_CATALOGUE,
19
+ DEFAULT_PROJECTS,
20
+ DOC_TOOL,
21
+ DOCUMENTATION_PROJECT,
22
+ ECHO,
23
+ LS,
24
+ ProgramCatalogue,
25
+ ProgramEntry,
26
+ ProjectSettings,
27
+ UnknownProgramError,
28
+ )
29
+ from cuprum.context import (
30
+ AfterHook,
31
+ AllowRegistration,
32
+ BeforeHook,
33
+ CuprumContext,
34
+ ExecHook,
35
+ ForbiddenProgramError,
36
+ HookRegistration,
37
+ after,
38
+ allow,
39
+ before,
40
+ current_context,
41
+ get_context,
42
+ observe,
43
+ scoped,
44
+ )
45
+ from cuprum.events import ExecEvent
46
+ from cuprum.logging_hooks import LoggingHookRegistration, logging_hook
47
+ from cuprum.program import Program
48
+ from cuprum.sh import (
49
+ CommandResult,
50
+ ExecutionContext,
51
+ Pipeline,
52
+ PipelineResult,
53
+ SafeCmd,
54
+ SafeCmdBuilder,
55
+ )
56
+
57
+ from . import sh
58
+
59
+ PACKAGE_NAME = "cuprum"
60
+
61
+ __all__ = [
62
+ "CORE_OPS_PROJECT",
63
+ "DEFAULT_CATALOGUE",
64
+ "DEFAULT_PROJECTS",
65
+ "DOCUMENTATION_PROJECT",
66
+ "DOC_TOOL",
67
+ "ECHO",
68
+ "LS",
69
+ "PACKAGE_NAME",
70
+ "AfterHook",
71
+ "AllowRegistration",
72
+ "BeforeHook",
73
+ "CommandResult",
74
+ "CuprumContext",
75
+ "ExecEvent",
76
+ "ExecHook",
77
+ "ExecutionContext",
78
+ "ForbiddenProgramError",
79
+ "HookRegistration",
80
+ "LoggingHookRegistration",
81
+ "Pipeline",
82
+ "PipelineResult",
83
+ "Program",
84
+ "ProgramCatalogue",
85
+ "ProgramEntry",
86
+ "ProjectSettings",
87
+ "SafeCmd",
88
+ "SafeCmdBuilder",
89
+ "UnknownProgramError",
90
+ "after",
91
+ "allow",
92
+ "before",
93
+ "current_context",
94
+ "get_context",
95
+ "logging_hook",
96
+ "observe",
97
+ "scoped",
98
+ "sh",
99
+ ]
@@ -0,0 +1,77 @@
1
+ """Internal helpers for structured execution event emission."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import types
8
+ import typing as typ
9
+
10
+ if typ.TYPE_CHECKING:
11
+ import collections.abc as cabc
12
+
13
+ from cuprum.events import ExecEvent, ExecHook
14
+
15
+
16
+ def _freeze_str_mapping(
17
+ mapping: cabc.Mapping[str, str] | None,
18
+ ) -> cabc.Mapping[str, str] | None:
19
+ if mapping is None:
20
+ return None
21
+ return types.MappingProxyType(dict(mapping))
22
+
23
+
24
+ def _merge_tags(*tags: cabc.Mapping[str, object] | None) -> cabc.Mapping[str, object]:
25
+ merged: dict[str, object] = {}
26
+ for mapping in tags:
27
+ if not mapping:
28
+ continue
29
+ merged.update(mapping)
30
+ return types.MappingProxyType(merged)
31
+
32
+
33
+ def _emit_exec_event(
34
+ hooks: tuple[ExecHook, ...],
35
+ event: ExecEvent,
36
+ *,
37
+ pending_tasks: list[asyncio.Task[None]],
38
+ ) -> None:
39
+ """Invoke observe hooks and schedule async hooks as background tasks."""
40
+ for hook in hooks:
41
+ result = hook(event)
42
+ if inspect.isawaitable(result):
43
+ pending_tasks.append(asyncio.create_task(_await_awaitable(result)))
44
+
45
+
46
+ async def _await_awaitable(awaitable: cabc.Awaitable[None]) -> None:
47
+ await awaitable
48
+
49
+
50
+ async def _wait_for_exec_hook_tasks(pending_tasks: list[asyncio.Task[None]]) -> None:
51
+ """Await background observe-hook tasks and surface the first failure.
52
+
53
+ Observe hooks may return awaitables; those awaitables are scheduled as tasks
54
+ by ``_emit_exec_event`` and added to ``pending_tasks``. This helper awaits
55
+ all pending tasks and re-raises the first ``BaseException`` encountered.
56
+
57
+ Notes
58
+ -----
59
+ When multiple hooks fail, only the first exception is raised; subsequent
60
+ exceptions are not surfaced and may be masked by the first failure.
61
+
62
+ """
63
+ if not pending_tasks:
64
+ return
65
+ results = await asyncio.gather(*pending_tasks, return_exceptions=True)
66
+ pending_tasks.clear()
67
+ for result in results:
68
+ if isinstance(result, BaseException):
69
+ raise result
70
+
71
+
72
+ __all__ = [
73
+ "_emit_exec_event",
74
+ "_freeze_str_mapping",
75
+ "_merge_tags",
76
+ "_wait_for_exec_hook_tasks",
77
+ ]
@@ -0,0 +1,278 @@
1
+ """Internal pipeline execution coordination and fail-fast semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses as dc
6
+ import sys
7
+ import time
8
+ import typing as typ
9
+ from pathlib import Path
10
+
11
+ from cuprum._observability import (
12
+ _emit_exec_event,
13
+ _freeze_str_mapping,
14
+ _merge_tags,
15
+ _wait_for_exec_hook_tasks,
16
+ )
17
+ from cuprum._pipeline_spawn import _spawn_pipeline_processes
18
+ from cuprum._pipeline_streams import (
19
+ _cancel_stream_tasks,
20
+ _create_pipe_tasks,
21
+ _gather_optional_text_tasks,
22
+ _PipelineRunConfig,
23
+ _prepare_pipeline_config,
24
+ )
25
+ from cuprum._pipeline_wait import _PipelineWaitResult, _wait_for_pipeline
26
+ from cuprum.context import current_context
27
+ from cuprum.events import ExecEvent
28
+
29
+ if typ.TYPE_CHECKING:
30
+ import asyncio
31
+ import types
32
+
33
+ from cuprum.context import AfterHook, BeforeHook
34
+ from cuprum.events import ExecHook
35
+ from cuprum.sh import CommandResult, ExecutionContext, PipelineResult, SafeCmd
36
+
37
+ _MIN_PIPELINE_STAGES = 2
38
+
39
+
40
+ def _sh_module() -> types.ModuleType:
41
+ module = sys.modules.get("cuprum.sh")
42
+ if module is None:
43
+ msg = "cuprum.sh must be imported before running pipelines"
44
+ raise RuntimeError(msg)
45
+ return module
46
+
47
+
48
+ @dc.dataclass(frozen=True, slots=True)
49
+ class _ExecutionHooks:
50
+ before_hooks: tuple[BeforeHook, ...]
51
+ after_hooks: tuple[AfterHook, ...]
52
+ observe_hooks: tuple[ExecHook, ...]
53
+
54
+
55
+ def _run_before_hooks(cmd: SafeCmd) -> _ExecutionHooks:
56
+ """Collect hooks for a command after enforcing the current allowlist."""
57
+ ctx = current_context()
58
+ ctx.check_allowed(cmd.program)
59
+ return _ExecutionHooks(
60
+ before_hooks=ctx.before_hooks,
61
+ after_hooks=ctx.after_hooks,
62
+ observe_hooks=ctx.observe_hooks,
63
+ )
64
+
65
+
66
+ @dc.dataclass(frozen=True, slots=True)
67
+ class _StageObservation:
68
+ cmd: SafeCmd
69
+ hooks: _ExecutionHooks
70
+ tags: typ.Mapping[str, object]
71
+ cwd: Path | None
72
+ env_overlay: typ.Mapping[str, str] | None
73
+ pending_tasks: list[asyncio.Task[None]]
74
+
75
+ def emit(
76
+ self,
77
+ phase: typ.Literal["plan", "start", "stdout", "stderr", "exit"],
78
+ details: _EventDetails,
79
+ ) -> None:
80
+ if not self.hooks.observe_hooks:
81
+ return
82
+ _emit_exec_event(
83
+ self.hooks.observe_hooks,
84
+ ExecEvent(
85
+ phase=phase,
86
+ program=self.cmd.program,
87
+ argv=self.cmd.argv_with_program,
88
+ cwd=self.cwd,
89
+ env=self.env_overlay,
90
+ pid=details.pid,
91
+ timestamp=time.time(),
92
+ line=details.line,
93
+ exit_code=details.exit_code,
94
+ duration_s=details.duration_s,
95
+ tags=self.tags,
96
+ ),
97
+ pending_tasks=self.pending_tasks,
98
+ )
99
+
100
+
101
+ @dc.dataclass(frozen=True, slots=True)
102
+ class _EventDetails:
103
+ pid: int | None
104
+ line: str | None = None
105
+ exit_code: int | None = None
106
+ duration_s: float | None = None
107
+
108
+
109
+ @dc.dataclass(frozen=True, slots=True)
110
+ class _PipelineStageResultInputs:
111
+ wait_result: _PipelineWaitResult
112
+ stderr_by_stage: tuple[str | None, ...]
113
+ final_stdout: str | None
114
+
115
+
116
+ def _build_pipeline_observations(
117
+ parts: tuple[SafeCmd, ...],
118
+ config: _PipelineRunConfig,
119
+ *,
120
+ pending_tasks: list[asyncio.Task[None]],
121
+ ) -> tuple[_StageObservation, ...]:
122
+ hooks_by_stage = tuple(_run_before_hooks(cmd) for cmd in parts)
123
+ cwd = None if config.ctx.cwd is None else Path(config.ctx.cwd)
124
+ env_overlay = _freeze_str_mapping(config.ctx.env)
125
+ return tuple(
126
+ _StageObservation(
127
+ cmd=cmd,
128
+ hooks=hooks,
129
+ tags=_merge_tags(
130
+ {
131
+ "project": cmd.project.name,
132
+ "capture": config.capture,
133
+ "echo": config.echo,
134
+ "pipeline_stage_index": idx,
135
+ "pipeline_stages": len(parts),
136
+ },
137
+ config.ctx.tags,
138
+ ),
139
+ cwd=cwd,
140
+ env_overlay=env_overlay,
141
+ pending_tasks=pending_tasks,
142
+ )
143
+ for idx, (cmd, hooks) in enumerate(zip(parts, hooks_by_stage, strict=True))
144
+ )
145
+
146
+
147
+ def _emit_plan_events_and_run_before_hooks(
148
+ observations: tuple[_StageObservation, ...],
149
+ ) -> None:
150
+ for obs in observations:
151
+ obs.emit("plan", _EventDetails(pid=None))
152
+ for hook in obs.hooks.before_hooks:
153
+ hook(obs.cmd)
154
+
155
+
156
+ def _build_pipeline_stage_results(
157
+ parts: tuple[SafeCmd, ...],
158
+ observations: tuple[_StageObservation, ...],
159
+ *,
160
+ processes: list[asyncio.subprocess.Process],
161
+ inputs: _PipelineStageResultInputs,
162
+ ) -> list[CommandResult]:
163
+ sh = _sh_module()
164
+ stage_results: list[CommandResult] = []
165
+ for idx, obs in enumerate(observations):
166
+ process = processes[idx]
167
+ ended_at = inputs.wait_result.ended_at[idx]
168
+ duration_s = (
169
+ None
170
+ if ended_at is None
171
+ else max(0.0, ended_at - inputs.wait_result.started_at[idx])
172
+ )
173
+ obs.emit(
174
+ "exit",
175
+ _EventDetails(
176
+ pid=process.pid,
177
+ exit_code=inputs.wait_result.exit_codes[idx],
178
+ duration_s=duration_s,
179
+ ),
180
+ )
181
+ stage_results.append(
182
+ sh.CommandResult(
183
+ program=obs.cmd.program,
184
+ argv=obs.cmd.argv,
185
+ exit_code=inputs.wait_result.exit_codes[idx],
186
+ pid=process.pid if process.pid is not None else -1,
187
+ stdout=inputs.final_stdout if idx == len(parts) - 1 else None,
188
+ stderr=inputs.stderr_by_stage[idx],
189
+ ),
190
+ )
191
+ return stage_results
192
+
193
+
194
+ async def _finalize_pipeline_execution(
195
+ parts: tuple[SafeCmd, ...],
196
+ observations: tuple[_StageObservation, ...],
197
+ stage_results: list[CommandResult],
198
+ pending_tasks: list[asyncio.Task[None]],
199
+ ) -> None:
200
+ hooks_by_stage = tuple(obs.hooks for obs in observations)
201
+ _run_pipeline_after_hooks(parts, hooks_by_stage, stage_results)
202
+ await _wait_for_exec_hook_tasks(pending_tasks)
203
+
204
+
205
+ async def _run_pipeline(
206
+ parts: tuple[SafeCmd, ...],
207
+ *,
208
+ capture: bool,
209
+ echo: bool,
210
+ context: ExecutionContext | None,
211
+ ) -> PipelineResult:
212
+ """Execute a pipeline and return a structured result."""
213
+ config = _prepare_pipeline_config(capture=capture, echo=echo, context=context)
214
+ pending_tasks: list[asyncio.Task[None]] = []
215
+ observations = _build_pipeline_observations(
216
+ parts,
217
+ config,
218
+ pending_tasks=pending_tasks,
219
+ )
220
+ try:
221
+ _emit_plan_events_and_run_before_hooks(observations)
222
+ (
223
+ processes,
224
+ stderr_tasks,
225
+ stdout_task,
226
+ started_at,
227
+ ) = await _spawn_pipeline_processes(
228
+ parts,
229
+ config,
230
+ observations=observations,
231
+ )
232
+ except BaseException:
233
+ await _wait_for_exec_hook_tasks(pending_tasks)
234
+ raise
235
+ try:
236
+ wait_result = await _wait_for_pipeline(
237
+ processes,
238
+ pipe_tasks=_create_pipe_tasks(processes),
239
+ cancel_grace=config.ctx.cancel_grace,
240
+ started_at=started_at,
241
+ )
242
+ inputs = _PipelineStageResultInputs(
243
+ wait_result=wait_result,
244
+ stderr_by_stage=await _gather_optional_text_tasks(stderr_tasks),
245
+ final_stdout=None if stdout_task is None else await stdout_task,
246
+ )
247
+ except BaseException:
248
+ await _cancel_stream_tasks(stderr_tasks, stdout_task)
249
+ await _wait_for_exec_hook_tasks(pending_tasks)
250
+ raise
251
+ stage_results = _build_pipeline_stage_results(
252
+ parts,
253
+ observations,
254
+ processes=processes,
255
+ inputs=inputs,
256
+ )
257
+ await _finalize_pipeline_execution(
258
+ parts,
259
+ observations,
260
+ stage_results,
261
+ pending_tasks,
262
+ )
263
+
264
+ return _sh_module().PipelineResult(
265
+ stages=tuple(stage_results),
266
+ failure_index=inputs.wait_result.failure_index,
267
+ )
268
+
269
+
270
+ def _run_pipeline_after_hooks(
271
+ parts: tuple[SafeCmd, ...],
272
+ hooks_by_stage: tuple[_ExecutionHooks, ...],
273
+ results: list[CommandResult],
274
+ ) -> None:
275
+ """Run registered after hooks for each pipeline stage."""
276
+ for cmd, hooks, result in zip(parts, hooks_by_stage, results, strict=True):
277
+ for hook in hooks.after_hooks:
278
+ hook(cmd, result)
@@ -0,0 +1,20 @@
1
+ """Pipeline subprocess spawning and stream wiring.
2
+
3
+ This module is a thin wrapper around the underlying spawn and cleanup helpers.
4
+ The implementations live in ``cuprum._process_lifecycle`` to keep pipeline
5
+ orchestration cohesive and avoid import cycles.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from cuprum._process_lifecycle import (
11
+ _build_spawn_observations,
12
+ _cleanup_spawned_processes,
13
+ _spawn_pipeline_processes,
14
+ )
15
+
16
+ __all__ = [
17
+ "_build_spawn_observations",
18
+ "_cleanup_spawned_processes",
19
+ "_spawn_pipeline_processes",
20
+ ]