omnirun 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.
omnirun/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """omnirun — run jobs from your repo anywhere."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from omnirun.backends.base import Backend, BackendError, make_backend, register
2
+
3
+ __all__ = ["Backend", "BackendError", "make_backend", "register"]
@@ -0,0 +1,118 @@
1
+ """Backend protocol and registry.
2
+
3
+ A backend turns a JobSpec into a running job somewhere and answers questions
4
+ about it afterwards. Implementations register with @register("type-name") and
5
+ are constructed from their BackendConfig section by config.load_backends().
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from collections.abc import Iterator
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any, Callable, TypeVar
14
+
15
+ from omnirun.models import JobHandle, JobSpec, Offer, ResourceSpec, StatusReport
16
+
17
+ if TYPE_CHECKING:
18
+ from omnirun.config import BackendConfig
19
+
20
+
21
+ class BackendError(RuntimeError):
22
+ """Raised for backend-level failures with a user-actionable message."""
23
+
24
+
25
+ class Backend(ABC):
26
+ """One configured execution target (a cluster, a machine, a provider account).
27
+
28
+ Rules for implementations:
29
+ - probe() must be fast (called speculatively with a ~10s budget), must not
30
+ mutate anything, and must NEVER raise: on error return a single not-fit
31
+ Offer carrying the error in unfit_reasons.
32
+ - submit() may take long (repo push, provisioning). It must either return a
33
+ working JobHandle or raise BackendError after cleaning up anything billable.
34
+ - status()/logs()/cancel()/pull_outputs() take the handle produced by submit
35
+ and must tolerate being called from a fresh process (no in-memory state).
36
+ """
37
+
38
+ #: registry type name, set by @register
39
+ type_name: str = ""
40
+
41
+ def __init__(self, name: str, config: "BackendConfig") -> None:
42
+ self.name = name # config key, e.g. "uni"
43
+ self.config = config
44
+
45
+ @abstractmethod
46
+ def probe(self, res: ResourceSpec) -> list[Offer]: ...
47
+
48
+ @abstractmethod
49
+ def submit(self, spec: JobSpec, offer: Offer) -> JobHandle: ...
50
+
51
+ @abstractmethod
52
+ def status(self, handle: JobHandle) -> StatusReport: ...
53
+
54
+ @abstractmethod
55
+ def logs(self, handle: JobHandle, follow: bool = False) -> Iterator[str]:
56
+ """Yield log lines (stdout+stderr merged). follow=True tails until the
57
+ job reaches a terminal state."""
58
+ ...
59
+
60
+ @abstractmethod
61
+ def cancel(self, handle: JobHandle) -> None: ...
62
+
63
+ @abstractmethod
64
+ def pull_outputs(self, handle: JobHandle, dest: Path) -> list[Path]:
65
+ """Copy the job's collected outputs into dest; return copied paths."""
66
+ ...
67
+
68
+ def gc(self, handle: JobHandle) -> None:
69
+ """Release everything the job holds remotely (worktrees, instances).
70
+
71
+ Called by `omnirun gc` for terminal jobs. Default: nothing to do.
72
+ Backends with billable resources MUST override."""
73
+
74
+ def check(self) -> str:
75
+ """Connectivity/config sanity check for `omnirun backends check`.
76
+
77
+ Return a short human 'ok: ...' description or raise BackendError."""
78
+ return "ok"
79
+
80
+
81
+ _REGISTRY: dict[str, type[Backend]] = {}
82
+
83
+ _B = TypeVar("_B", bound=Backend)
84
+
85
+
86
+ def register(type_name: str) -> Callable[[type[_B]], type[_B]]:
87
+ def deco(cls: type[_B]) -> type[_B]:
88
+ cls.type_name = type_name
89
+ _REGISTRY[type_name] = cls
90
+ return cls
91
+
92
+ return deco
93
+
94
+
95
+ def backend_class(type_name: str) -> type[Backend]:
96
+ # Import concrete backend modules lazily so optional deps (kaggle, colab)
97
+ # don't break unrelated commands.
98
+ if type_name not in _REGISTRY:
99
+ import importlib
100
+
101
+ mod = {
102
+ "local": "omnirun.backends.local",
103
+ "ssh": "omnirun.backends.ssh",
104
+ "slurm": "omnirun.backends.slurm",
105
+ "kaggle": "omnirun.backends.kaggle",
106
+ "colab": "omnirun.backends.colab",
107
+ "runpod": "omnirun.backends.runpod",
108
+ "vast": "omnirun.backends.vast",
109
+ "thunder": "omnirun.backends.thunder",
110
+ }.get(type_name)
111
+ if mod is None:
112
+ raise BackendError(f"unknown backend type {type_name!r}")
113
+ importlib.import_module(mod)
114
+ return _REGISTRY[type_name]
115
+
116
+
117
+ def make_backend(name: str, config: Any) -> Backend:
118
+ return backend_class(config.type)(name, config)