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.
- horus_singularity/__init__.py +6 -0
- horus_singularity/executor/__init__.py +6 -0
- horus_singularity/executor/singularity.py +190 -0
- horus_singularity/i18n.py +22 -0
- horus_singularity/locale/en/LC_MESSAGES/horus_singularity.mo +0 -0
- horus_singularity/locale/en/LC_MESSAGES/horus_singularity.po +47 -0
- horus_singularity/locale/messages.pot +45 -0
- horus_singularity-0.0.1.dist-info/METADATA +9 -0
- horus_singularity-0.0.1.dist-info/RECORD +12 -0
- horus_singularity-0.0.1.dist-info/WHEEL +4 -0
- horus_singularity-0.0.1.dist-info/entry_points.txt +2 -0
- horus_singularity-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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")
|
|
Binary file
|
|
@@ -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,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.
|