atlas-runner 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.
- atlas_runner-0.1.0/LICENSE +21 -0
- atlas_runner-0.1.0/PKG-INFO +22 -0
- atlas_runner-0.1.0/README.md +115 -0
- atlas_runner-0.1.0/pyproject.toml +52 -0
- atlas_runner-0.1.0/setup.cfg +4 -0
- atlas_runner-0.1.0/src/atlas_runner/__init__.py +27 -0
- atlas_runner-0.1.0/src/atlas_runner/_docker_cli.py +146 -0
- atlas_runner-0.1.0/src/atlas_runner/_version.py +1 -0
- atlas_runner-0.1.0/src/atlas_runner/cli.py +331 -0
- atlas_runner-0.1.0/src/atlas_runner/docker_backend.py +188 -0
- atlas_runner-0.1.0/src/atlas_runner/errors.py +33 -0
- atlas_runner-0.1.0/src/atlas_runner/models.py +72 -0
- atlas_runner-0.1.0/src/atlas_runner/protocol.py +29 -0
- atlas_runner-0.1.0/src/atlas_runner/runner.py +52 -0
- atlas_runner-0.1.0/src/atlas_runner.egg-info/PKG-INFO +22 -0
- atlas_runner-0.1.0/src/atlas_runner.egg-info/SOURCES.txt +24 -0
- atlas_runner-0.1.0/src/atlas_runner.egg-info/dependency_links.txt +1 -0
- atlas_runner-0.1.0/src/atlas_runner.egg-info/entry_points.txt +2 -0
- atlas_runner-0.1.0/src/atlas_runner.egg-info/requires.txt +9 -0
- atlas_runner-0.1.0/src/atlas_runner.egg-info/top_level.txt +1 -0
- atlas_runner-0.1.0/tests/test_cli.py +538 -0
- atlas_runner-0.1.0/tests/test_docker_backend.py +316 -0
- atlas_runner-0.1.0/tests/test_docker_cli.py +282 -0
- atlas_runner-0.1.0/tests/test_errors.py +74 -0
- atlas_runner-0.1.0/tests/test_models.py +178 -0
- atlas_runner-0.1.0/tests/test_runner.py +122 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ATTMOS Inc.
|
|
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.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atlas-runner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A runtime abstraction layer for ATLAS — container orchestration via Docker with a pluggable backend interface.
|
|
5
|
+
Author: ATTMOS Inc.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/ATTMOS/atlas-runner
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
15
|
+
Requires-Dist: typer<1,>=0.9
|
|
16
|
+
Requires-Dist: rich<14,>=13.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# atlas-runner
|
|
2
|
+
|
|
3
|
+
A runtime abstraction layer for ATLAS. Provides a library and CLI that launches and manages workloads using Docker (v0), with a backend interface designed to support Apptainer/Singularity in future releases. Used by atlas-agent for deterministic container execution, lifecycle control, status, logs, and resource enforcement.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python >= 3.11
|
|
8
|
+
- Docker Engine (accessible via `docker` CLI)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install atlas-runner
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For development:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e ".[dev]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
### Library usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from atlas_runner import Runner, RunSpec, create_backend
|
|
28
|
+
|
|
29
|
+
# Create a runner with the Docker backend
|
|
30
|
+
runner = Runner(create_backend("docker"))
|
|
31
|
+
|
|
32
|
+
# Pull an image and run a container
|
|
33
|
+
runner.pull("alpine:latest")
|
|
34
|
+
result = runner.run(RunSpec(
|
|
35
|
+
name="my-task",
|
|
36
|
+
image="alpine:latest",
|
|
37
|
+
command=["echo", "hello"],
|
|
38
|
+
))
|
|
39
|
+
print(result.container_id, result.started_at)
|
|
40
|
+
|
|
41
|
+
# Check status and fetch logs
|
|
42
|
+
status = runner.status("my-task")
|
|
43
|
+
print(status.state, status.exit_code)
|
|
44
|
+
logs = runner.logs("my-task")
|
|
45
|
+
|
|
46
|
+
# Clean up
|
|
47
|
+
runner.destroy("my-task")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### CLI usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Authenticate with a registry
|
|
54
|
+
atlas-runner login registry.io -u user --password-stdin < token.txt
|
|
55
|
+
|
|
56
|
+
# Pull an image
|
|
57
|
+
atlas-runner pull alpine:latest
|
|
58
|
+
|
|
59
|
+
# Run a container with resource limits
|
|
60
|
+
atlas-runner run --name my-task --image alpine:latest \
|
|
61
|
+
--cmd echo --cmd hello \
|
|
62
|
+
--cpu-cores 4 --memory-gb 16
|
|
63
|
+
|
|
64
|
+
# Run with GPU access
|
|
65
|
+
atlas-runner run --name gpu-task --image nvidia/cuda:12.0-base \
|
|
66
|
+
--gpu 0 --gpu 1 \
|
|
67
|
+
--shell "python train.py"
|
|
68
|
+
|
|
69
|
+
# Check status (human-readable or JSON)
|
|
70
|
+
atlas-runner status my-task
|
|
71
|
+
atlas-runner --json status my-task
|
|
72
|
+
|
|
73
|
+
# View logs
|
|
74
|
+
atlas-runner logs my-task
|
|
75
|
+
|
|
76
|
+
# List all containers
|
|
77
|
+
atlas-runner ps
|
|
78
|
+
atlas-runner --json ps
|
|
79
|
+
|
|
80
|
+
# Stop and remove
|
|
81
|
+
atlas-runner destroy my-task
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
| Option | CLI Flag | Description |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Backend | `--backend docker` | Container runtime backend (default: `docker`) |
|
|
89
|
+
| Docker host | `--docker-host tcp://remote:2375` | Docker daemon socket URL |
|
|
90
|
+
| JSON output | `--json` | Output structured JSON instead of human-readable text |
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Install with dev dependencies
|
|
96
|
+
pip install -e ".[dev]"
|
|
97
|
+
|
|
98
|
+
# Run tests
|
|
99
|
+
pytest tests/ -v
|
|
100
|
+
|
|
101
|
+
# Lint and format
|
|
102
|
+
ruff check src/ tests/
|
|
103
|
+
ruff format --check src/ tests/
|
|
104
|
+
|
|
105
|
+
# Type check
|
|
106
|
+
mypy src/
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
See [docs/api.md](docs/api.md) for the full API reference.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "atlas-runner"
|
|
7
|
+
description = "A runtime abstraction layer for ATLAS — container orchestration via Docker with a pluggable backend interface."
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "ATTMOS Inc." }]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"pydantic>=2.5,<3",
|
|
20
|
+
"typer>=0.9,<1",
|
|
21
|
+
"rich>=13.0,<14",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-cov>=4.0",
|
|
28
|
+
"ruff>=0.3",
|
|
29
|
+
"mypy>=1.8",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/ATTMOS/atlas-runner"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
atlas-runner = "atlas_runner.cli:app"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
target-version = "py311"
|
|
43
|
+
src = ["src", "tests"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "I", "UP"]
|
|
47
|
+
|
|
48
|
+
[tool.mypy]
|
|
49
|
+
python_version = "3.11"
|
|
50
|
+
strict = true
|
|
51
|
+
packages = ["atlas_runner"]
|
|
52
|
+
mypy_path = "src"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Atlas Runner — container orchestration via Docker."""
|
|
2
|
+
|
|
3
|
+
from atlas_runner._version import __version__
|
|
4
|
+
from atlas_runner.errors import ErrorCode, RunnerError
|
|
5
|
+
from atlas_runner.models import (
|
|
6
|
+
ContainerState,
|
|
7
|
+
ContainerStatus,
|
|
8
|
+
Mount,
|
|
9
|
+
RunResult,
|
|
10
|
+
RunSpec,
|
|
11
|
+
)
|
|
12
|
+
from atlas_runner.protocol import RunnerBackend
|
|
13
|
+
from atlas_runner.runner import Runner, create_backend
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"ContainerState",
|
|
18
|
+
"ContainerStatus",
|
|
19
|
+
"create_backend",
|
|
20
|
+
"ErrorCode",
|
|
21
|
+
"Mount",
|
|
22
|
+
"Runner",
|
|
23
|
+
"RunnerBackend",
|
|
24
|
+
"RunnerError",
|
|
25
|
+
"RunResult",
|
|
26
|
+
"RunSpec",
|
|
27
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from atlas_runner.errors import ErrorCode, RunnerError
|
|
9
|
+
|
|
10
|
+
_DAEMON_MARKERS = (
|
|
11
|
+
"cannot connect to the docker daemon",
|
|
12
|
+
"is the docker daemon running",
|
|
13
|
+
"connection refused",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DockerCLI:
|
|
18
|
+
"""Low-level wrapper around the ``docker`` CLI.
|
|
19
|
+
|
|
20
|
+
Every Docker operation in atlas-runner goes through this class so that
|
|
21
|
+
``subprocess.run`` is isolated to a single module (clean mock boundary).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, docker_host: str | None = None) -> None:
|
|
25
|
+
self._base: list[str] = ["docker"]
|
|
26
|
+
if docker_host is not None:
|
|
27
|
+
self._base = ["docker", "--host", docker_host]
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
# Core executor
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def _run(
|
|
34
|
+
self,
|
|
35
|
+
args: list[str],
|
|
36
|
+
*,
|
|
37
|
+
check: bool = True,
|
|
38
|
+
capture_output: bool = True,
|
|
39
|
+
input_data: str | None = None,
|
|
40
|
+
timeout: int | None = None,
|
|
41
|
+
) -> subprocess.CompletedProcess[str]:
|
|
42
|
+
cmd = self._base + args
|
|
43
|
+
try:
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
cmd,
|
|
46
|
+
check=False,
|
|
47
|
+
capture_output=capture_output,
|
|
48
|
+
text=True,
|
|
49
|
+
input=input_data,
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
)
|
|
52
|
+
except subprocess.TimeoutExpired as exc:
|
|
53
|
+
raise RunnerError(
|
|
54
|
+
code=ErrorCode.BACKEND_ERROR,
|
|
55
|
+
message=f"Docker command timed out after {timeout}s",
|
|
56
|
+
details={"command": cmd, "timeout": timeout},
|
|
57
|
+
) from exc
|
|
58
|
+
|
|
59
|
+
if check and result.returncode != 0:
|
|
60
|
+
stderr = result.stderr.strip() if result.stderr else ""
|
|
61
|
+
if any(m in stderr.lower() for m in _DAEMON_MARKERS):
|
|
62
|
+
raise RunnerError(
|
|
63
|
+
code=ErrorCode.DOCKER_DAEMON_UNAVAILABLE,
|
|
64
|
+
message="Docker daemon is not available",
|
|
65
|
+
details={"stderr": stderr},
|
|
66
|
+
)
|
|
67
|
+
raise RunnerError(
|
|
68
|
+
code=ErrorCode.BACKEND_ERROR,
|
|
69
|
+
message=f"Docker command failed (exit {result.returncode})",
|
|
70
|
+
details={"command": cmd, "stderr": stderr},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
# Public methods
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def login(
|
|
80
|
+
self,
|
|
81
|
+
registry: str,
|
|
82
|
+
username: str,
|
|
83
|
+
password: str,
|
|
84
|
+
) -> None:
|
|
85
|
+
try:
|
|
86
|
+
self._run(
|
|
87
|
+
["login", "--username", username, "--password-stdin", registry],
|
|
88
|
+
input_data=password,
|
|
89
|
+
)
|
|
90
|
+
except RunnerError as exc:
|
|
91
|
+
if exc.code == ErrorCode.BACKEND_ERROR:
|
|
92
|
+
raise RunnerError(
|
|
93
|
+
code=ErrorCode.LOGIN_FAILED,
|
|
94
|
+
message=f"Login failed for registry: {registry}",
|
|
95
|
+
details=exc.details,
|
|
96
|
+
) from exc
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
def pull(self, image: str) -> None:
|
|
100
|
+
try:
|
|
101
|
+
self._run(["pull", image])
|
|
102
|
+
except RunnerError as exc:
|
|
103
|
+
if exc.code == ErrorCode.BACKEND_ERROR:
|
|
104
|
+
raise RunnerError(
|
|
105
|
+
code=ErrorCode.IMAGE_PULL_FAILED,
|
|
106
|
+
message=f"Failed to pull image: {image}",
|
|
107
|
+
details=exc.details,
|
|
108
|
+
) from exc
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
def run(self, args: list[str]) -> str:
|
|
112
|
+
result = self._run(["run"] + args)
|
|
113
|
+
return result.stdout.strip()
|
|
114
|
+
|
|
115
|
+
def stop(self, name: str) -> None:
|
|
116
|
+
self._run(["stop", name], check=False)
|
|
117
|
+
|
|
118
|
+
def rm(self, name: str) -> None:
|
|
119
|
+
self._run(["rm", name], check=False)
|
|
120
|
+
|
|
121
|
+
def inspect(self, name: str) -> dict[str, Any]:
|
|
122
|
+
result = self._run(["inspect", name])
|
|
123
|
+
data: list[dict[str, Any]] = json.loads(result.stdout)
|
|
124
|
+
return data[0]
|
|
125
|
+
|
|
126
|
+
def ps(self, filters: list[str] | None = None) -> list[dict[str, Any]]:
|
|
127
|
+
args = ["ps", "--format", "{{json .}}", "--no-trunc"]
|
|
128
|
+
for f in filters or []:
|
|
129
|
+
args.extend(["--filter", f])
|
|
130
|
+
result = self._run(args)
|
|
131
|
+
entries: list[dict[str, Any]] = []
|
|
132
|
+
for line in result.stdout.strip().splitlines():
|
|
133
|
+
if line:
|
|
134
|
+
entries.append(json.loads(line))
|
|
135
|
+
return entries
|
|
136
|
+
|
|
137
|
+
def logs(self, name: str) -> str:
|
|
138
|
+
result = self._run(["logs", name])
|
|
139
|
+
return result.stdout
|
|
140
|
+
|
|
141
|
+
def container_exists(self, name: str) -> bool:
|
|
142
|
+
escaped = re.escape(name)
|
|
143
|
+
result = self._run(
|
|
144
|
+
["ps", "-a", "--filter", f"name=^/{escaped}$", "--format", "{{.ID}}"],
|
|
145
|
+
)
|
|
146
|
+
return bool(result.stdout.strip())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|