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 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
- return Path("data-furu").resolve()
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
- self.force_recompute = {
53
+ always_rerun_items = {
41
54
  item.strip()
42
- for item in os.getenv("FURU_FORCE_RECOMPUTE", "").split(",")
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 determines subdirectory)."""
105
+ """Get root directory for storage (version_controlled uses its own root)."""
81
106
  if version_controlled:
82
- return self.base_root / "git"
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
@@ -1,4 +1,4 @@
1
- from .furu import Furu
1
+ from .furu import DependencyChzSpec, DependencySpec, Furu
2
2
  from .list import FuruList
3
3
 
4
- __all__ = ["Furu", "FuruList"]
4
+ __all__ = ["DependencyChzSpec", "DependencySpec", "Furu", "FuruList"]