furu 0.0.1__py3-none-any.whl → 0.0.3__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.
- furu/__init__.py +3 -1
- furu/config.py +85 -5
- furu/core/__init__.py +2 -2
- furu/core/furu.py +438 -75
- furu/dashboard/frontend/dist/assets/{index-CbdDfSOZ.css → index-BXAIKNNr.css} +1 -1
- furu/dashboard/frontend/dist/assets/{index-DDv_TYB_.js → index-DS3FsqcY.js} +3 -3
- furu/dashboard/frontend/dist/index.html +2 -2
- furu/errors.py +47 -5
- furu/migration.py +8 -4
- furu/serialization/serializer.py +40 -2
- furu/storage/metadata.py +17 -5
- furu/storage/state.py +115 -3
- {furu-0.0.1.dist-info → furu-0.0.3.dist-info}/METADATA +48 -20
- {furu-0.0.1.dist-info → furu-0.0.3.dist-info}/RECORD +19 -19
- {furu-0.0.1.dist-info → furu-0.0.3.dist-info}/WHEEL +1 -1
- {furu-0.0.1.dist-info → furu-0.0.3.dist-info}/entry_points.txt +1 -0
furu/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ __version__ = version("furu")
|
|
|
13
13
|
|
|
14
14
|
from .config import FURU_CONFIG, FuruConfig, get_furu_root, set_furu_root
|
|
15
15
|
from .adapters import SubmititAdapter
|
|
16
|
-
from .core import Furu, FuruList
|
|
16
|
+
from .core import DependencyChzSpec, DependencySpec, Furu, FuruList
|
|
17
17
|
from .errors import (
|
|
18
18
|
FuruComputeError,
|
|
19
19
|
FuruError,
|
|
@@ -56,6 +56,8 @@ __all__ = [
|
|
|
56
56
|
"FuruMigrationRequired",
|
|
57
57
|
"FuruSerializer",
|
|
58
58
|
"FuruWaitTimeout",
|
|
59
|
+
"DependencyChzSpec",
|
|
60
|
+
"DependencySpec",
|
|
59
61
|
"MISSING",
|
|
60
62
|
"migrate",
|
|
61
63
|
"NamespacePair",
|
furu/config.py
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from importlib import import_module
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class FuruConfig:
|
|
6
7
|
"""Central configuration for Furu behavior."""
|
|
7
8
|
|
|
9
|
+
DEFAULT_ROOT_DIR = Path("furu-data")
|
|
10
|
+
VERSION_CONTROLLED_SUBDIR = DEFAULT_ROOT_DIR / "artifacts"
|
|
11
|
+
|
|
8
12
|
def __init__(self):
|
|
9
13
|
def _get_base_root() -> Path:
|
|
10
14
|
env = os.getenv("FURU_PATH")
|
|
11
15
|
if env:
|
|
12
16
|
return Path(env).expanduser().resolve()
|
|
13
|
-
|
|
17
|
+
project_root = self._find_project_root(fallback_to_cwd=True)
|
|
18
|
+
return (project_root / self.DEFAULT_ROOT_DIR).resolve()
|
|
14
19
|
|
|
15
20
|
self.base_root = _get_base_root()
|
|
21
|
+
self.version_controlled_root_override = self._get_version_controlled_override()
|
|
16
22
|
self.poll_interval = float(os.getenv("FURU_POLL_INTERVAL_SECS", "10"))
|
|
17
23
|
self.wait_log_every_sec = float(os.getenv("FURU_WAIT_LOG_EVERY_SECS", "10"))
|
|
18
24
|
self.stale_timeout = float(os.getenv("FURU_STALE_AFTER_SECS", str(30 * 60)))
|
|
25
|
+
max_wait_env = os.getenv("FURU_MAX_WAIT_SECS")
|
|
26
|
+
self.max_wait_time_sec = float(max_wait_env) if max_wait_env else None
|
|
19
27
|
self.lease_duration_sec = float(os.getenv("FURU_LEASE_SECS", "120"))
|
|
20
28
|
hb = os.getenv("FURU_HEARTBEAT_SECS")
|
|
21
29
|
self.heartbeat_interval_sec = (
|
|
22
30
|
float(hb) if hb is not None else max(1.0, self.lease_duration_sec / 3.0)
|
|
23
31
|
)
|
|
24
32
|
self.max_requeues = int(os.getenv("FURU_PREEMPT_MAX", "5"))
|
|
33
|
+
self.retry_failed = os.getenv("FURU_RETRY_FAILED", "1").lower() in {
|
|
34
|
+
"1",
|
|
35
|
+
"true",
|
|
36
|
+
"yes",
|
|
37
|
+
}
|
|
25
38
|
self.ignore_git_diff = os.getenv("FURU_IGNORE_DIFF", "0").lower() in {
|
|
26
39
|
"1",
|
|
27
40
|
"true",
|
|
@@ -37,11 +50,23 @@ class FuruConfig:
|
|
|
37
50
|
"true",
|
|
38
51
|
"yes",
|
|
39
52
|
}
|
|
40
|
-
|
|
53
|
+
always_rerun_items = {
|
|
41
54
|
item.strip()
|
|
42
|
-
for item in os.getenv("
|
|
55
|
+
for item in os.getenv("FURU_ALWAYS_RERUN", "").split(",")
|
|
43
56
|
if item.strip()
|
|
44
57
|
}
|
|
58
|
+
all_entries = {item for item in always_rerun_items if item.lower() == "all"}
|
|
59
|
+
if all_entries and len(always_rerun_items) > len(all_entries):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"FURU_ALWAYS_RERUN cannot combine 'ALL' with specific entries"
|
|
62
|
+
)
|
|
63
|
+
self.always_rerun_all = bool(all_entries)
|
|
64
|
+
if self.always_rerun_all:
|
|
65
|
+
always_rerun_items = {
|
|
66
|
+
item for item in always_rerun_items if item.lower() != "all"
|
|
67
|
+
}
|
|
68
|
+
self._require_namespaces_exist(always_rerun_items)
|
|
69
|
+
self.always_rerun = always_rerun_items
|
|
45
70
|
self.cancelled_is_preempted = os.getenv(
|
|
46
71
|
"FURU_CANCELLED_IS_PREEMPTED", "false"
|
|
47
72
|
).lower() in {"1", "true", "yes"}
|
|
@@ -77,11 +102,66 @@ class FuruConfig:
|
|
|
77
102
|
return num * multipliers[unit]
|
|
78
103
|
|
|
79
104
|
def get_root(self, version_controlled: bool = False) -> Path:
|
|
80
|
-
"""Get root directory for storage (version_controlled
|
|
105
|
+
"""Get root directory for storage (version_controlled uses its own root)."""
|
|
81
106
|
if version_controlled:
|
|
82
|
-
|
|
107
|
+
if self.version_controlled_root_override is not None:
|
|
108
|
+
return self.version_controlled_root_override
|
|
109
|
+
return self._resolve_version_controlled_root()
|
|
83
110
|
return self.base_root / "data"
|
|
84
111
|
|
|
112
|
+
@classmethod
|
|
113
|
+
def _get_version_controlled_override(cls) -> Path | None:
|
|
114
|
+
env = os.getenv("FURU_VERSION_CONTROLLED_PATH")
|
|
115
|
+
if env:
|
|
116
|
+
return Path(env).expanduser().resolve()
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def _resolve_version_controlled_root(cls) -> Path:
|
|
121
|
+
project_root = cls._find_project_root()
|
|
122
|
+
return (project_root / cls.VERSION_CONTROLLED_SUBDIR).resolve()
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _find_project_root(
|
|
126
|
+
start: Path | None = None, *, fallback_to_cwd: bool = False
|
|
127
|
+
) -> Path:
|
|
128
|
+
base = (start or Path.cwd()).resolve()
|
|
129
|
+
git_root: Path | None = None
|
|
130
|
+
for path in (base, *base.parents):
|
|
131
|
+
if (path / "pyproject.toml").is_file():
|
|
132
|
+
return path
|
|
133
|
+
if git_root is None and (path / ".git").exists():
|
|
134
|
+
git_root = path
|
|
135
|
+
if git_root is not None:
|
|
136
|
+
return git_root
|
|
137
|
+
if fallback_to_cwd:
|
|
138
|
+
return base
|
|
139
|
+
raise ValueError(
|
|
140
|
+
"Cannot locate pyproject.toml or .git to determine version-controlled root. "
|
|
141
|
+
"Set FURU_VERSION_CONTROLLED_PATH to override."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _require_namespaces_exist(namespaces: set[str]) -> None:
|
|
146
|
+
if not namespaces:
|
|
147
|
+
return
|
|
148
|
+
missing_sentinel = object()
|
|
149
|
+
for namespace in namespaces:
|
|
150
|
+
module_name, _, qualname = namespace.rpartition(".")
|
|
151
|
+
if not module_name or not qualname:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"FURU_ALWAYS_RERUN entries must be 'module.QualifiedName', "
|
|
154
|
+
f"got {namespace!r}"
|
|
155
|
+
)
|
|
156
|
+
target: object = import_module(module_name)
|
|
157
|
+
for attr in qualname.split("."):
|
|
158
|
+
value = getattr(target, attr, missing_sentinel)
|
|
159
|
+
if value is missing_sentinel:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"FURU_ALWAYS_RERUN entry does not exist: {namespace!r}"
|
|
162
|
+
)
|
|
163
|
+
target = value
|
|
164
|
+
|
|
85
165
|
@property
|
|
86
166
|
def raw_dir(self) -> Path:
|
|
87
167
|
return self.base_root / "raw"
|
furu/core/__init__.py
CHANGED