styxkit 0.1.0__py3-none-any.whl

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.
styxkit/__init__.py ADDED
@@ -0,0 +1,217 @@
1
+ """Convenience helpers spanning the Styx runtime backends.
2
+
3
+ ``styxdefs`` is the base contract; each backend (``styxdocker``, ``styxpodman``,
4
+ ``styxsingularity``, ``styxgraph``) is an independent piece. ``styxkit`` sits on
5
+ top and offers one-call runner selection across whichever pieces are installed.
6
+
7
+ Backends are imported lazily, so ``styxkit`` itself only requires ``styxdefs``.
8
+ Calling a ``use_*()`` for a backend that is not installed raises a friendly
9
+ :class:`ModuleNotFoundError` naming the package (and extra) to install.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib
15
+ import importlib.util
16
+ import shutil
17
+ import typing
18
+
19
+ # Re-exported for convenience; the public surface is pinned by ``__all__`` below.
20
+ from styxdefs import (
21
+ DryRunner,
22
+ Execution,
23
+ InputPathType,
24
+ LocalRunner,
25
+ Metadata,
26
+ OutputPathType,
27
+ Runner,
28
+ StyxRuntimeError,
29
+ StyxValidationError,
30
+ get_global_runner,
31
+ set_global_runner,
32
+ )
33
+
34
+ RunnerType = typing.Literal["local", "docker", "podman", "singularity"]
35
+ """A container-runner kind selectable via :func:`resolve_runner` / :func:`use_auto`."""
36
+
37
+
38
+ class _Backend(typing.NamedTuple):
39
+ """A lazily-loaded runner backend: its module, class, exe kwarg, and probes."""
40
+
41
+ module: str
42
+ cls: str
43
+ exe_kwarg: str | None
44
+ executables: tuple[str, ...]
45
+
46
+
47
+ # Constructor kwarg names mirror the real backend classes (field-tested in rbc).
48
+ _BACKENDS: dict[str, _Backend] = {
49
+ "docker": _Backend("styxdocker", "DockerRunner", "docker_executable", ("docker",)),
50
+ "podman": _Backend("styxpodman", "PodmanRunner", "podman_executable", ("podman",)),
51
+ "singularity": _Backend(
52
+ "styxsingularity",
53
+ "SingularityRunner",
54
+ "singularity_executable",
55
+ ("apptainer", "singularity"),
56
+ ),
57
+ "graph": _Backend("styxgraph", "GraphRunner", None, ()),
58
+ }
59
+
60
+
61
+ def _runner_factory(kind: str) -> typing.Callable[..., Runner]:
62
+ """Import a backend's runner class (a Runner factory), or raise a friendly error.
63
+
64
+ Typed as ``Callable[..., Runner]`` rather than ``type[Runner]``: each backend
65
+ constructor has its own keyword signature, none of which the base ``Runner``
66
+ protocol declares, so the constructor kwargs are forwarded opaquely.
67
+ """
68
+ backend = _BACKENDS[kind]
69
+ try:
70
+ module = importlib.import_module(backend.module)
71
+ except ModuleNotFoundError as exc:
72
+ # Only translate the backend package being absent. A ModuleNotFoundError
73
+ # naming something else means an *installed* backend failed to import a
74
+ # transitive dependency - re-raise that as-is rather than mislabeling it
75
+ # as "install styxkit[...]".
76
+ if exc.name != backend.module:
77
+ raise
78
+ raise ModuleNotFoundError(
79
+ f"The {kind!r} runner needs the {backend.module!r} package. "
80
+ f'Install it with `pip install "styxkit[{kind}]"` '
81
+ f"(or `pip install {backend.module}`)."
82
+ ) from exc
83
+ return typing.cast("typing.Callable[..., Runner]", getattr(module, backend.cls))
84
+
85
+
86
+ def _use(runner: Runner) -> Runner:
87
+ """Register ``runner`` as the global runner and return it."""
88
+ set_global_runner(runner)
89
+ return runner
90
+
91
+
92
+ def use_local(**kwargs: typing.Any) -> Runner:
93
+ """Register a ``LocalRunner`` as the global runner and return it."""
94
+ return _use(LocalRunner(**kwargs))
95
+
96
+
97
+ def use_dry() -> Runner:
98
+ """Register a ``DryRunner`` as the global runner and return it.
99
+
100
+ ``DryRunner`` takes no configuration, so this helper accepts no arguments.
101
+ """
102
+ return _use(DryRunner())
103
+
104
+
105
+ def use_docker(**kwargs: typing.Any) -> Runner:
106
+ """Register a ``DockerRunner`` as the global runner and return it.
107
+
108
+ Requires the ``styxdocker`` package (``pip install "styxkit[docker]"``).
109
+ """
110
+ return _use(_runner_factory("docker")(**kwargs))
111
+
112
+
113
+ def use_podman(**kwargs: typing.Any) -> Runner:
114
+ """Register a ``PodmanRunner`` as the global runner and return it.
115
+
116
+ Requires the ``styxpodman`` package (``pip install "styxkit[podman]"``).
117
+ """
118
+ return _use(_runner_factory("podman")(**kwargs))
119
+
120
+
121
+ def use_singularity(**kwargs: typing.Any) -> Runner:
122
+ """Register a ``SingularityRunner`` as the global runner and return it.
123
+
124
+ Requires the ``styxsingularity`` package
125
+ (``pip install "styxkit[singularity]"``).
126
+ """
127
+ return _use(_runner_factory("singularity")(**kwargs))
128
+
129
+
130
+ def use_graph(base: Runner | None = None, **kwargs: typing.Any) -> Runner:
131
+ """Wrap a runner in a ``GraphRunner`` and register it as the global runner.
132
+
133
+ Unlike the leaf runners, ``GraphRunner`` decorates a base runner. When
134
+ ``base`` is omitted it wraps the current global runner, so ``use_graph()``
135
+ starts recording a graph over whatever runner is already active.
136
+
137
+ Requires the ``styxgraph`` package (``pip install "styxkit[graph]"``).
138
+ """
139
+ resolved_base = base if base is not None else get_global_runner()
140
+ return _use(_runner_factory("graph")(resolved_base, **kwargs))
141
+
142
+
143
+ def _available(kind: str) -> bool:
144
+ """True iff a backend is importable and one of its executables is on PATH."""
145
+ backend = _BACKENDS[kind]
146
+ if importlib.util.find_spec(backend.module) is None:
147
+ return False
148
+ return any(shutil.which(exe) for exe in backend.executables)
149
+
150
+
151
+ def resolve_runner(runner: RunnerType | typing.Literal["auto"] = "auto") -> RunnerType:
152
+ """Resolve a runner selection, auto-detecting when ``runner == "auto"``.
153
+
154
+ Auto prefers the first container backend that is both installed and has its
155
+ executable on PATH (docker > podman > singularity), falling back to
156
+ ``"local"``.
157
+
158
+ Args:
159
+ runner: An explicit runner kind, or ``"auto"`` to detect one.
160
+
161
+ Returns:
162
+ The resolved runner kind.
163
+ """
164
+ if runner != "auto":
165
+ return runner
166
+ for kind in ("docker", "podman", "singularity"):
167
+ if _available(kind):
168
+ return typing.cast(RunnerType, kind)
169
+ return "local"
170
+
171
+
172
+ def use_auto(**kwargs: typing.Any) -> Runner:
173
+ """Detect the best available runner, register it as global, and return it.
174
+
175
+ Detection order is described in :func:`resolve_runner`. For a container
176
+ runner, the detected executable (e.g. ``apptainer`` vs ``singularity``) is
177
+ passed through unless the caller already supplied it. Extra keyword
178
+ arguments are forwarded to the selected runner's constructor.
179
+ """
180
+ kind = resolve_runner("auto")
181
+ if kind == "local":
182
+ return use_local(**kwargs)
183
+ backend = _BACKENDS[kind]
184
+ if backend.exe_kwarg and backend.exe_kwarg not in kwargs:
185
+ exe = next((e for e in backend.executables if shutil.which(e)), None)
186
+ if exe is not None:
187
+ kwargs[backend.exe_kwarg] = exe
188
+ dispatch: dict[str, typing.Callable[..., Runner]] = {
189
+ "docker": use_docker,
190
+ "podman": use_podman,
191
+ "singularity": use_singularity,
192
+ }
193
+ return dispatch[kind](**kwargs)
194
+
195
+
196
+ __all__ = [
197
+ "DryRunner",
198
+ "Execution",
199
+ "InputPathType",
200
+ "LocalRunner",
201
+ "Metadata",
202
+ "OutputPathType",
203
+ "Runner",
204
+ "RunnerType",
205
+ "StyxRuntimeError",
206
+ "StyxValidationError",
207
+ "get_global_runner",
208
+ "resolve_runner",
209
+ "set_global_runner",
210
+ "use_auto",
211
+ "use_docker",
212
+ "use_dry",
213
+ "use_graph",
214
+ "use_local",
215
+ "use_podman",
216
+ "use_singularity",
217
+ ]
styxkit/py.typed ADDED
File without changes
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: styxkit
3
+ Version: 0.1.0
4
+ Summary: Convenience helpers spanning the Styx runtime backends.
5
+ Author: Florian Rupprecht
6
+ Author-email: Florian Rupprecht <33600480+nx10@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ Requires-Dist: styxdefs>=0.7.0,<0.8
9
+ Requires-Dist: styxdocker ; extra == 'all'
10
+ Requires-Dist: styxpodman ; extra == 'all'
11
+ Requires-Dist: styxsingularity ; extra == 'all'
12
+ Requires-Dist: styxgraph ; extra == 'all'
13
+ Requires-Dist: styxdocker ; extra == 'docker'
14
+ Requires-Dist: styxgraph ; extra == 'graph'
15
+ Requires-Dist: styxpodman ; extra == 'podman'
16
+ Requires-Dist: styxsingularity ; extra == 'singularity'
17
+ Requires-Python: >=3.10
18
+ Provides-Extra: all
19
+ Provides-Extra: docker
20
+ Provides-Extra: graph
21
+ Provides-Extra: podman
22
+ Provides-Extra: singularity
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Styxkit - convenience helpers for the Styx runtime
26
+
27
+ `styxkit` is a small convenience layer on top of the Styx runtime. It provides
28
+ one-call runner selection across whichever backend packages you have installed,
29
+ so you do not have to import each runner class and wire up the global runner by
30
+ hand.
31
+
32
+ It sits at the top of the runtime stack:
33
+
34
+ - `styxdefs` is the base contract (the `Runner` protocol, `LocalRunner`,
35
+ `DryRunner`, and `set_global_runner` / `get_global_runner`).
36
+ - Each backend (`styxdocker`, `styxpodman`, `styxsingularity`, `styxgraph`) is an
37
+ independent package depending only on `styxdefs`.
38
+ - `styxkit` depends only on `styxdefs` and imports the backends **lazily**, so
39
+ installing it does not pull in any container backend you do not want.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install styxkit # base, plus styxdefs
45
+ pip install "styxkit[docker]" # + the Docker backend
46
+ pip install "styxkit[all]" # + every backend (docker, podman, singularity, graph)
47
+ ```
48
+
49
+ Calling a `use_*()` for a backend you have not installed raises a friendly error
50
+ telling you exactly what to install.
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ import styxkit
56
+
57
+ # Pick a specific runner:
58
+ styxkit.use_docker()
59
+ styxkit.use_singularity(singularity_executable="apptainer")
60
+ styxkit.use_local()
61
+
62
+ # ...or let styxkit detect the best available container runtime,
63
+ # falling back to local when none is found:
64
+ runner = styxkit.use_auto()
65
+
66
+ # Each use_*() registers the runner as the global runner and returns it,
67
+ # so you can configure it without a get_global_runner() round-trip:
68
+ runner = styxkit.use_docker()
69
+ runner.data_dir = "/tmp/styx"
70
+ ```
71
+
72
+ `use_auto()` prefers, in order, the first container backend that is both
73
+ installed and has its executable on `PATH`: Docker, then Podman, then
74
+ Singularity/Apptainer, otherwise the local runner.
75
+
76
+ ## Available helpers
77
+
78
+ - `use_local`, `use_dry` - always available (from `styxdefs`).
79
+ - `use_docker`, `use_podman`, `use_singularity` - lazy, require the matching backend.
80
+ - `use_graph(base=None)` - wraps a runner in a graph recorder; defaults to the
81
+ current global runner.
82
+ - `use_auto()` / `resolve_runner()` - detect and select a runner.
83
+ - The core `styxdefs` symbols (`Runner`, `Execution`, `Metadata`,
84
+ `set_global_runner`, ...) are re-exported for convenience.
85
+
86
+ ## License
87
+
88
+ `styxkit` is released under the MIT License. See the LICENSE file for details.
@@ -0,0 +1,5 @@
1
+ styxkit/__init__.py,sha256=dahIIPrPaxpaCArWV5RmhYIWhUi6kOs3uA9b9uQLFfs,7311
2
+ styxkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ styxkit-0.1.0.dist-info/WHEEL,sha256=wXwAVsgVaOZ_pwDFqQm5Rd6PID-Fc74nkLc8X8gHiDo,81
4
+ styxkit-0.1.0.dist-info/METADATA,sha256=MUHTpIbZus-QYHthnLpgm9Yh4gKRnVYqrWdLlZ8bAq8,3208
5
+ styxkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.19
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any