horus-singularity 0.0.1__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.
@@ -0,0 +1,6 @@
1
+ #
2
+ # horus_singularity
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
@@ -0,0 +1,6 @@
1
+ #
2
+ # horus_singularity
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
@@ -0,0 +1,190 @@
1
+ #
2
+ # horus_singularity
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
7
+ """
8
+ Singularity/Apptainer executor implementation for Horus.
9
+ """
10
+
11
+ import asyncio
12
+ import shlex
13
+ from typing import TYPE_CHECKING, Any, ClassVar
14
+
15
+ from horus_builtin.runtime.command import CommandRuntime
16
+ from horus_runtime.core.executor.base import BaseExecutor, RuntimeFilterType
17
+ from horus_runtime.core.task.exceptions import TaskExecutionError
18
+ from horus_runtime.logging import horus_logger
19
+ from pydantic import Field, PrivateAttr
20
+
21
+ from horus_singularity.i18n import tr as _
22
+
23
+ if TYPE_CHECKING:
24
+ from horus_runtime.core.task.base import BaseTask
25
+
26
+
27
+ class SingularityExecutor(BaseExecutor):
28
+ """
29
+ Runs the task's command inside a Singularity/Apptainer container.
30
+ """
31
+
32
+ kind: str = "singularity"
33
+ kind_name: ClassVar[str] = "Singularity Executor"
34
+ kind_description: ClassVar[str] = _(
35
+ "Executes a command inside a Singularity/Apptainer container on the "
36
+ "task target."
37
+ )
38
+
39
+ runtimes: ClassVar[RuntimeFilterType] = (CommandRuntime,)
40
+
41
+ image: str
42
+ """
43
+ Path to the ``.sif`` image file on the target (e.g.
44
+ ``/opt/images/boltz.sif``). Unlike Docker there is no build step: the
45
+ image must already exist on the target.
46
+ """
47
+
48
+ exe: str = "singularity"
49
+ """
50
+ Container CLI to invoke — ``singularity`` or ``apptainer``.
51
+ """
52
+
53
+ binds: dict[str, str] = Field(default_factory=dict)
54
+ """
55
+ Bind mounts as ``host_path -> container_path`` (``--bind`` flag).
56
+ """
57
+
58
+ nv: bool = False
59
+ """
60
+ Add ``--nv`` so the NVIDIA driver stack is exposed inside the container.
61
+ """
62
+
63
+ env: dict[str, str] = Field(default_factory=dict)
64
+ """
65
+ Environment variables to set inside the container, as ``NAME -> value``.
66
+ """
67
+
68
+ working_dir: str | None = None
69
+ """
70
+ Working directory inside the container (``--pwd`` flag).
71
+ """
72
+
73
+ extra_args: list[str] = Field(default_factory=list)
74
+ """
75
+ Extra flags passed verbatim to ``<exe> exec`` before the image path.
76
+ """
77
+
78
+ _proc: Any = PrivateAttr(default=None)
79
+ """
80
+ Reference to the process started in :meth:`_execute` so
81
+ :meth:`cancel_execution` can kill it.
82
+ """
83
+
84
+ def _singularity_exec_cmd(
85
+ self, prepared_command: str, task: "BaseTask | None" = None
86
+ ) -> str:
87
+ """Return the full ``singularity exec`` CLI command string."""
88
+ # ponytail: auto-mount artifact parent dirs; explicit binds win
89
+ auto_mounts: dict[str, str] = {}
90
+ if task is not None:
91
+ for artifact in (*task.inputs, *task.outputs):
92
+ host_dir = str(artifact.path.parent)
93
+ auto_mounts[host_dir] = host_dir
94
+ merged_binds = {**auto_mounts, **self.binds}
95
+
96
+ parts = [shlex.quote(self.exe), "exec"]
97
+ if self.nv:
98
+ parts.append("--nv")
99
+ for k, v in self.env.items():
100
+ parts += ["--env", shlex.quote(f"{k}={v}")]
101
+ for host, container in merged_binds.items():
102
+ parts += ["--bind", shlex.quote(f"{host}:{container}")]
103
+ if self.working_dir:
104
+ parts += ["--pwd", shlex.quote(self.working_dir)]
105
+ parts += [shlex.quote(arg) for arg in self.extra_args]
106
+ parts += [
107
+ shlex.quote(self.image),
108
+ "/bin/sh",
109
+ "-c",
110
+ shlex.quote(prepared_command),
111
+ ]
112
+ return " ".join(parts)
113
+
114
+ async def _execute(self, task: "BaseTask") -> None:
115
+ """
116
+ Run the task's command inside a Singularity/Apptainer container on
117
+ the target.
118
+ """
119
+ if not isinstance(task.runtime, CommandRuntime):
120
+ raise TaskExecutionError(
121
+ _("SingularityExecutor only supports CommandRuntime runtimes.")
122
+ )
123
+ prepared_command = await task.runtime.setup_runtime(task)
124
+
125
+ exec_cmd = self._singularity_exec_cmd(prepared_command, task)
126
+ horus_logger.log.debug(
127
+ _(
128
+ "Running task %(task_id)s in Singularity image %(image)s: "
129
+ "%(command)s"
130
+ )
131
+ % {
132
+ "task_id": task.id,
133
+ "image": self.image,
134
+ "command": prepared_command,
135
+ }
136
+ )
137
+
138
+ proc = await task.target.run_command(exec_cmd, cwd=task.working_dir)
139
+ self._proc = proc
140
+
141
+ try:
142
+ stdout, stderr = await proc.communicate()
143
+ except asyncio.CancelledError:
144
+ proc.kill()
145
+ await proc.wait()
146
+ raise
147
+
148
+ out = stdout.decode(errors="replace").strip() if stdout else ""
149
+ err = stderr.decode(errors="replace").strip() if stderr else ""
150
+ if out:
151
+ horus_logger.log.info(out)
152
+ if err:
153
+ horus_logger.log.warning(err)
154
+
155
+ try:
156
+ if proc.returncode != 0:
157
+ horus_logger.log.error(
158
+ _(
159
+ "Container for task %(task_id)s exited with code "
160
+ "%(code)s. Output: %(out)s"
161
+ )
162
+ % {
163
+ "task_id": task.id,
164
+ "code": proc.returncode,
165
+ "out": (out or err).strip(),
166
+ }
167
+ )
168
+
169
+ raise TaskExecutionError(
170
+ _("Container exited with code %(code)s")
171
+ % {"code": proc.returncode}
172
+ )
173
+ finally:
174
+ self._proc = None
175
+
176
+ async def cancel_execution(self) -> None:
177
+ """Kill the running container process so it is not orphaned.
178
+
179
+ Singularity containers have no daemon-side name to stop, so the
180
+ process started in :meth:`_execute` is killed directly. If nothing
181
+ is running (e.g. the task finished before the cancel arrived) this
182
+ is a safe no-op.
183
+ """
184
+ proc = self._proc
185
+ if proc is None:
186
+ return
187
+ self._proc = None # clear before kill — idempotent
188
+ if proc.returncode is None:
189
+ proc.kill()
190
+ await proc.wait()
@@ -0,0 +1,22 @@
1
+ #
2
+ # horus_singularity
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
7
+ """
8
+ Localization for horus_singularity.
9
+
10
+ Import ``tr`` (aliased as ``_``) in any module that has user-visible strings::
11
+
12
+ from horus_singularity.i18n import tr as _
13
+
14
+ _("Something happened.")
15
+ _("%(n)s item processed", "%(n)s items processed", n=count)
16
+ """
17
+
18
+ from pathlib import Path
19
+
20
+ from horus_runtime.i18n import make_translator
21
+
22
+ tr = make_translator("horus_singularity", Path(__file__).parent / "locale")
@@ -0,0 +1,47 @@
1
+ # English translations for horus_singularity.
2
+ # Copyright (C) 2026 Temple Compute
3
+ # This file is distributed under the same license as the horus_singularity
4
+ # project.
5
+ # Temple Compute, 2026.
6
+ #
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: horus_singularity VERSION\n"
10
+ "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
11
+ "POT-Creation-Date: 2026-07-22 23:17+0200\n"
12
+ "PO-Revision-Date: 2026-07-22 23:32+0200\n"
13
+ "Last-Translator: Temple Compute\n"
14
+ "Language: en\n"
15
+ "Language-Team: en <LL@li.org>\n"
16
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
17
+ "MIME-Version: 1.0\n"
18
+ "Content-Type: text/plain; charset=utf-8\n"
19
+ "Content-Transfer-Encoding: 8bit\n"
20
+ "Generated-By: Babel 2.18.0\n"
21
+
22
+ #: src/horus_singularity/executor/singularity.py:35
23
+ msgid ""
24
+ "Executes a command inside a Singularity/Apptainer container on the task "
25
+ "target."
26
+ msgstr ""
27
+ "Executes a command inside a Singularity/Apptainer container on the task "
28
+ "target."
29
+
30
+ #: src/horus_singularity/executor/singularity.py:121
31
+ msgid "SingularityExecutor only supports CommandRuntime runtimes."
32
+ msgstr "SingularityExecutor only supports CommandRuntime runtimes."
33
+
34
+ #: src/horus_singularity/executor/singularity.py:128
35
+ #, python-format
36
+ msgid "Running task %(task_id)s in Singularity image %(image)s: %(command)s"
37
+ msgstr "Running task %(task_id)s in Singularity image %(image)s: %(command)s"
38
+
39
+ #: src/horus_singularity/executor/singularity.py:159
40
+ #, python-format
41
+ msgid "Container for task %(task_id)s exited with code %(code)s. Output: %(out)s"
42
+ msgstr "Container for task %(task_id)s exited with code %(code)s. Output: %(out)s"
43
+
44
+ #: src/horus_singularity/executor/singularity.py:170
45
+ #, python-format
46
+ msgid "Container exited with code %(code)s"
47
+ msgstr "Container exited with code %(code)s"
@@ -0,0 +1,45 @@
1
+ # Translations template for horus_singularity.
2
+ # Copyright (C) 2026 Temple Compute
3
+ # This file is distributed under the same license as the horus_singularity
4
+ # project.
5
+ # FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
6
+ #
7
+ #, fuzzy
8
+ msgid ""
9
+ msgstr ""
10
+ "Project-Id-Version: horus_singularity VERSION\n"
11
+ "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
12
+ "POT-Creation-Date: 2026-07-22 23:17+0200\n"
13
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
14
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
15
+ "Language-Team: LANGUAGE <LL@li.org>\n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=utf-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Generated-By: Babel 2.18.0\n"
20
+
21
+ #: src/horus_singularity/executor/singularity.py:35
22
+ msgid ""
23
+ "Executes a command inside a Singularity/Apptainer container on the task "
24
+ "target."
25
+ msgstr ""
26
+
27
+ #: src/horus_singularity/executor/singularity.py:121
28
+ msgid "SingularityExecutor only supports CommandRuntime runtimes."
29
+ msgstr ""
30
+
31
+ #: src/horus_singularity/executor/singularity.py:128
32
+ #, python-format
33
+ msgid "Running task %(task_id)s in Singularity image %(image)s: %(command)s"
34
+ msgstr ""
35
+
36
+ #: src/horus_singularity/executor/singularity.py:159
37
+ #, python-format
38
+ msgid "Container for task %(task_id)s exited with code %(code)s. Output: %(out)s"
39
+ msgstr ""
40
+
41
+ #: src/horus_singularity/executor/singularity.py:170
42
+ #, python-format
43
+ msgid "Container exited with code %(code)s"
44
+ msgstr ""
45
+
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: horus_singularity
3
+ Version: 0.0.1
4
+ Summary: A Horus runtime plugin that runs task commands inside Singularity/Apptainer containers.
5
+ License-File: LICENSE
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Python :: 3
8
+ Requires-Python: >=3.13
9
+ Requires-Dist: horus-runtime>=0.2.0
@@ -0,0 +1,12 @@
1
+ horus_singularity/__init__.py,sha256=F3A65u__Zvq08OpMNe7mBjx6POUhQn2O1zX1T192Dt4,76
2
+ horus_singularity/i18n.py,sha256=AkCLpS_gr5zKRL-7v44Cdwm9PfgBtFoU833tzdo75Cw,493
3
+ horus_singularity/executor/__init__.py,sha256=F3A65u__Zvq08OpMNe7mBjx6POUhQn2O1zX1T192Dt4,76
4
+ horus_singularity/executor/singularity.py,sha256=kTZipZSvAWJnW3vIfVhLADE2DjdqzgaogrEmwTt_WrQ,6044
5
+ horus_singularity/locale/messages.pot,sha256=kOSubLenV0qtWAVX6hf0HmVS1UToqL-cMhWBaj1dZGQ,1371
6
+ horus_singularity/locale/en/LC_MESSAGES/horus_singularity.mo,sha256=S50hvFrSf8Ubqu4LMhqTdZcKwihBwqcJMGp9EbVR11I,1160
7
+ horus_singularity/locale/en/LC_MESSAGES/horus_singularity.po,sha256=15zPLythKao2_iF-JBXjFFJX6mMohvQo0C7JP49uVBs,1712
8
+ horus_singularity-0.0.1.dist-info/METADATA,sha256=AoJqkmDkOFORW0oOQrGoqqvMvPAV_JHxERTPKLZ0Vm0,339
9
+ horus_singularity-0.0.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
10
+ horus_singularity-0.0.1.dist-info/entry_points.txt,sha256=9Sk4Qz_nvtRgbpfHunnwDvdCt-SwvVwZQOCwwZH9yU8,70
11
+ horus_singularity-0.0.1.dist-info/licenses/LICENSE,sha256=6YWukf1Wwk94ltA2Xcjpy8rBuXnA9Yc8-51OPO2DMYo,1071
12
+ horus_singularity-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [horus.executor]
2
+ singularity = horus_singularity.executor.singularity
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Temple Compute
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.