horus-docker 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_docker
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
@@ -0,0 +1,6 @@
1
+ #
2
+ # horus_docker
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
@@ -0,0 +1,228 @@
1
+ #
2
+ # horus_docker
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
7
+ """
8
+ Docker executor implementation for Horus.
9
+ """
10
+
11
+ import asyncio
12
+ import shlex
13
+ from typing import TYPE_CHECKING, ClassVar
14
+
15
+ from horus_builtin.runtime.command import CommandRuntime
16
+ from horus_builtin.runtime.substitution import substitute
17
+ from horus_runtime.core.executor.base import BaseExecutor, RuntimeFilterType
18
+ from horus_runtime.core.task.exceptions import TaskExecutionError
19
+ from horus_runtime.logging import horus_logger
20
+ from pydantic import Field
21
+
22
+ from horus_docker.i18n import tr as _
23
+
24
+ if TYPE_CHECKING:
25
+ from horus_runtime.core.task.base import BaseTask
26
+
27
+
28
+ class DockerExecutor(BaseExecutor):
29
+ """
30
+ Runs the task's command inside a Docker container.
31
+ """
32
+
33
+ kind: str = "docker_executor"
34
+ kind_name: ClassVar[str] = "Docker Executor"
35
+ kind_description: ClassVar[str] = _(
36
+ "Executes a command inside a Docker container on the task target."
37
+ )
38
+
39
+ runtimes: ClassVar[RuntimeFilterType] = (CommandRuntime,)
40
+
41
+ image: str
42
+ """
43
+ Docker image tag (e.g. ``python:3.13-slim``). When :attr:`dockerfile` is
44
+ set this becomes the tag assigned to the locally-built image.
45
+ """
46
+
47
+ env: dict[str, str] = Field(default_factory=dict)
48
+ """
49
+ Environment variables to set inside the container, as ``NAME -> value``.
50
+ """
51
+
52
+ volumes: dict[str, str] = Field(default_factory=dict)
53
+ """
54
+ Bind mounts as ``host_path -> container_path`` (read-write).
55
+ """
56
+
57
+ ports: dict[str, str] = Field(default_factory=dict)
58
+ """
59
+ Port mappings as ``host_port -> container_port``.
60
+ """
61
+
62
+ working_dir: str | None = None
63
+ """
64
+ Working directory inside the container (``-w`` flag).
65
+ """
66
+
67
+ network: str | None = None
68
+ """
69
+ Docker network to attach the container to.
70
+ """
71
+
72
+ entrypoint: str | None = None
73
+ """
74
+ Override for the image's ``ENTRYPOINT`` (``--entrypoint`` flag).
75
+ """
76
+
77
+ user: str | None = None
78
+ """
79
+ User (``name``, ``uid``, or ``uid:gid``) to run the command as.
80
+ """
81
+
82
+ auto_remove: bool = True
83
+ """
84
+ Add ``--rm`` so the container is removed when it exits.
85
+ """
86
+
87
+ dockerfile: str | None = None
88
+ """
89
+ Inline Dockerfile content. Supports ``$id`` / ``${task.attr}``
90
+ substitution for artifacts and task attributes.
91
+ """
92
+
93
+ build_context: str | None = None
94
+ """
95
+ Build context path on the target for ``docker build``. Defaults to
96
+ ``task.working_dir`` when :attr:`dockerfile` is set.
97
+ """
98
+
99
+ def _docker_run_cmd(self, prepared_command: str) -> str:
100
+ """Return the full ``docker run`` CLI command string."""
101
+ parts = ["docker", "run"]
102
+ if self.auto_remove:
103
+ parts.append("--rm")
104
+ for k, v in self.env.items():
105
+ parts += ["-e", shlex.quote(f"{k}={v}")]
106
+ for host, container in self.volumes.items():
107
+ parts += ["-v", shlex.quote(f"{host}:{container}")]
108
+ for host_p, cont_p in self.ports.items():
109
+ parts += ["-p", shlex.quote(f"{host_p}:{cont_p}")]
110
+ if self.working_dir:
111
+ parts += ["-w", shlex.quote(self.working_dir)]
112
+ if self.network:
113
+ parts += ["--network", shlex.quote(self.network)]
114
+ if self.entrypoint is not None:
115
+ parts += ["--entrypoint", shlex.quote(self.entrypoint)]
116
+ if self.user:
117
+ parts += ["-u", shlex.quote(self.user)]
118
+ parts += [
119
+ shlex.quote(self.image),
120
+ "/bin/sh",
121
+ "-c",
122
+ shlex.quote(prepared_command),
123
+ ]
124
+ return " ".join(parts)
125
+
126
+ async def _build_image(self, task: "BaseTask") -> None:
127
+ """
128
+ Render :attr:`dockerfile`, upload it to the target, and build the
129
+ image tagged as :attr:`image`.
130
+ """
131
+ rendered = substitute(self.dockerfile or "", task)
132
+ build_dir = f"{task.working_dir}/.horus_docker"
133
+ dockerfile_path = f"{build_dir}/Dockerfile"
134
+
135
+ await task.target.mkdir(build_dir)
136
+ await task.target.put_file(rendered.encode(), dockerfile_path)
137
+
138
+ context = self.build_context or task.working_dir
139
+ build_cmd = (
140
+ f"docker build"
141
+ f" -t {shlex.quote(self.image)}"
142
+ f" -f {shlex.quote(dockerfile_path)}"
143
+ f" {shlex.quote(str(context))}"
144
+ )
145
+ proc = await task.target.run_command(build_cmd)
146
+ stdout, stderr = await proc.communicate()
147
+ out = stdout.decode(errors="replace").strip() if stdout else ""
148
+ err = stderr.decode(errors="replace").strip() if stderr else ""
149
+ if out:
150
+ horus_logger.log.debug(out)
151
+ if err:
152
+ horus_logger.log.debug(err)
153
+ if proc.returncode != 0:
154
+ raise TaskExecutionError(
155
+ _("docker build failed with exit code %(code)s")
156
+ % {"code": proc.returncode}
157
+ )
158
+
159
+ async def _execute(self, task: "BaseTask") -> None:
160
+ """
161
+ Build the image (if :attr:`dockerfile` is set), run the task's command
162
+ inside a container on the target, then clean up the image.
163
+ """
164
+ if not isinstance(task.runtime, CommandRuntime):
165
+ raise TaskExecutionError(
166
+ _("DockerExecutor only supports CommandRuntime runtimes.")
167
+ )
168
+ prepared_command = await task.runtime.setup_runtime(task)
169
+
170
+ if self.dockerfile:
171
+ await self._build_image(task)
172
+
173
+ run_cmd = self._docker_run_cmd(prepared_command)
174
+ horus_logger.log.debug(
175
+ _(
176
+ "Running task %(task_id)s in Docker image %(image)s: "
177
+ "%(command)s"
178
+ )
179
+ % {
180
+ "task_id": task.id,
181
+ "image": self.image,
182
+ "command": prepared_command,
183
+ }
184
+ )
185
+
186
+ proc = await task.target.run_command(run_cmd, cwd=task.working_dir)
187
+
188
+ try:
189
+ stdout, stderr = await proc.communicate()
190
+ except asyncio.CancelledError:
191
+ proc.kill()
192
+ await proc.wait()
193
+ raise
194
+
195
+ out = stdout.decode(errors="replace").strip() if stdout else ""
196
+ err = stderr.decode(errors="replace").strip() if stderr else ""
197
+ if out:
198
+ horus_logger.log.info(out)
199
+ if err:
200
+ horus_logger.log.warning(err)
201
+
202
+ try:
203
+ if proc.returncode != 0:
204
+ horus_logger.log.error(
205
+ _(
206
+ "Container for task %(task_id)s exited with code "
207
+ "%(code)s. Output: %(out)s"
208
+ )
209
+ % {
210
+ "task_id": task.id,
211
+ "code": proc.returncode,
212
+ "out": (out or err).strip(),
213
+ }
214
+ )
215
+
216
+ raise TaskExecutionError(
217
+ _("Container exited with code %(code)s")
218
+ % {"code": proc.returncode}
219
+ )
220
+ finally:
221
+ if self.dockerfile:
222
+ try:
223
+ rmi = await task.target.run_command(
224
+ f"docker rmi -f {shlex.quote(self.image)}"
225
+ )
226
+ await rmi.wait()
227
+ except Exception:
228
+ pass
horus_docker/i18n.py ADDED
@@ -0,0 +1,22 @@
1
+ #
2
+ # horus_docker
3
+ # Copyright (c) 2026 Temple Compute
4
+ #
5
+ # MIT License
6
+ #
7
+ """
8
+ Localization for horus_docker.
9
+
10
+ Import ``tr`` (aliased as ``_``) in any module that has user-visible strings::
11
+
12
+ from horus_docker.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_docker", Path(__file__).parent / "locale")
@@ -0,0 +1,62 @@
1
+ # English translations for horus_docker.
2
+ # Copyright (C) 2026 Temple Compute
3
+ # This file is distributed under the same license as the horus_docker
4
+ # project.
5
+ # Temple Compute, 2026.
6
+ #
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: horus_docker VERSION\n"
10
+ "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
11
+ "POT-Creation-Date: 2026-05-31 00:04+0200\n"
12
+ "PO-Revision-Date: 2026-05-31 00:07+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_docker/executor/docker.py:41
23
+ msgid "Executes a command inside a Docker container."
24
+ msgstr "Executes a command inside a Docker container."
25
+
26
+ #: src/horus_docker/executor/docker.py:148
27
+ msgid "DockerExecutor only supports CommandRuntime runtimes."
28
+ msgstr "DockerExecutor only supports CommandRuntime runtimes."
29
+
30
+ #: src/horus_docker/executor/docker.py:155
31
+ #, python-format
32
+ msgid "Running task %(task_id)s in Docker image %(image)s: %(command)s"
33
+ msgstr "Running task %(task_id)s in Docker image %(image)s: %(command)s"
34
+
35
+ #: src/horus_docker/executor/docker.py:172
36
+ #, python-format
37
+ msgid ""
38
+ "Logs from task %(task_id)s Docker container:\n"
39
+ "%(logs)s"
40
+ msgstr ""
41
+ "Logs from task %(task_id)s Docker container:\n"
42
+ "%(logs)s"
43
+
44
+ #: src/horus_docker/executor/docker.py:178
45
+ #, python-format
46
+ msgid "Docker execution failed for task %(task_id)s: %(error)s"
47
+ msgstr "Docker execution failed for task %(task_id)s: %(error)s"
48
+
49
+ #: src/horus_docker/executor/docker.py:182
50
+ #, python-format
51
+ msgid "Docker execution failed: %(error)s"
52
+ msgstr "Docker execution failed: %(error)s"
53
+
54
+ #: src/horus_docker/executor/docker.py:188
55
+ #, python-format
56
+ msgid "Container for task %(task_id)s exited with code %(code)s. Logs: %(logs)s"
57
+ msgstr "Container for task %(task_id)s exited with code %(code)s. Logs: %(logs)s"
58
+
59
+ #: src/horus_docker/executor/docker.py:198
60
+ #, python-format
61
+ msgid "Container exited with code %(code)s"
62
+ msgstr "Container exited with code %(code)s"
@@ -0,0 +1,59 @@
1
+ # Translations template for horus_docker.
2
+ # Copyright (C) 2026 Temple Compute
3
+ # This file is distributed under the same license as the horus_docker
4
+ # project.
5
+ # FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
6
+ #
7
+ #, fuzzy
8
+ msgid ""
9
+ msgstr ""
10
+ "Project-Id-Version: horus_docker VERSION\n"
11
+ "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
12
+ "POT-Creation-Date: 2026-05-31 00:04+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_docker/executor/docker.py:41
22
+ msgid "Executes a command inside a Docker container."
23
+ msgstr ""
24
+
25
+ #: src/horus_docker/executor/docker.py:148
26
+ msgid "DockerExecutor only supports CommandRuntime runtimes."
27
+ msgstr ""
28
+
29
+ #: src/horus_docker/executor/docker.py:155
30
+ #, python-format
31
+ msgid "Running task %(task_id)s in Docker image %(image)s: %(command)s"
32
+ msgstr ""
33
+
34
+ #: src/horus_docker/executor/docker.py:172
35
+ #, python-format
36
+ msgid ""
37
+ "Logs from task %(task_id)s Docker container:\n"
38
+ "%(logs)s"
39
+ msgstr ""
40
+
41
+ #: src/horus_docker/executor/docker.py:178
42
+ #, python-format
43
+ msgid "Docker execution failed for task %(task_id)s: %(error)s"
44
+ msgstr ""
45
+
46
+ #: src/horus_docker/executor/docker.py:182
47
+ #, python-format
48
+ msgid "Docker execution failed: %(error)s"
49
+ msgstr ""
50
+
51
+ #: src/horus_docker/executor/docker.py:188
52
+ #, python-format
53
+ msgid "Container for task %(task_id)s exited with code %(code)s. Logs: %(logs)s"
54
+ msgstr ""
55
+
56
+ #: src/horus_docker/executor/docker.py:198
57
+ #, python-format
58
+ msgid "Container exited with code %(code)s"
59
+ msgstr ""
horus_docker/py.typed ADDED
File without changes
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: horus_docker
3
+ Version: 0.0.1
4
+ Summary: A custom plugin for the Horus runtime, providing additional functionality and integrations.
5
+ Classifier: License :: OSI Approved :: MIT License
6
+ Classifier: Programming Language :: Python :: 3
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: horus-runtime>=0.1.3
@@ -0,0 +1,12 @@
1
+ horus_docker/__init__.py,sha256=jqNo4q-TC_OSD0XfvOsEnKgSy246GNyctGK6a95Dtq8,71
2
+ horus_docker/i18n.py,sha256=VkI5_7OcLDkcFNn7iVB0QiWiMyDLNekWFsvke2WCkRQ,473
3
+ horus_docker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ horus_docker/executor/__init__.py,sha256=jqNo4q-TC_OSD0XfvOsEnKgSy246GNyctGK6a95Dtq8,71
5
+ horus_docker/executor/docker.py,sha256=0_Uk8ej6_koltqcwTGqpxgcxxSIZ7XX4ONWduyqXjYI,7275
6
+ horus_docker/locale/messages.pot,sha256=Eb0ZmjnemeiRfcQaB1W1QgPvXWH4bvFED6EE7EZYiD8,1643
7
+ horus_docker/locale/en/LC_MESSAGES/horus_docker.mo,sha256=ZBUGlJBbxIudnS77Gu5S7FpYGFn4fKzy2Wcxv1B8lao,1403
8
+ horus_docker/locale/en/LC_MESSAGES/horus_docker.po,sha256=hb21SXf_ZRBEvwF_vpaJiojFZUlTh6I3oX9ISbYfNYo,2083
9
+ horus_docker-0.0.1.dist-info/METADATA,sha256=uLjRib1kIia2HQM4X7QBAx5RTr4MbUSQ0Dn54h2i4QA,316
10
+ horus_docker-0.0.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
11
+ horus_docker-0.0.1.dist-info/entry_points.txt,sha256=fHxWnpDnTT9IgWhWIZ0y_emEst9KTKYMmFBtgClXmXY,55
12
+ horus_docker-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
+ docker = horus_docker.executor.docker