furu 0.0.2__py3-none-any.whl → 0.0.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: furu
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Cacheable, nested pipelines for Python. Define computations as configs; furu handles caching, state tracking, and result reuse across runs.
5
5
  Author: Herman Brunborg
6
6
  Author-email: Herman Brunborg <herman@brunborg.com>
@@ -44,7 +44,7 @@ The `[dashboard]` extra includes the web dashboard. Omit it for the core library
44
44
  1. Subclass `furu.Furu[T]`
45
45
  2. Implement `_create(self) -> T` (compute and write to `self.furu_dir`)
46
46
  3. Implement `_load(self) -> T` (load from `self.furu_dir`)
47
- 4. Call `load_or_create()`
47
+ 4. Call `get()`
48
48
 
49
49
  ```python
50
50
  # my_project/pipelines.py
@@ -75,10 +75,10 @@ class TrainModel(furu.Furu[Path]):
75
75
  from my_project.pipelines import TrainModel
76
76
 
77
77
  # First call: runs _create(), caches result
78
- artifact = TrainModel(lr=3e-4, steps=5000).load_or_create()
78
+ artifact = TrainModel(lr=3e-4, steps=5000).get()
79
79
 
80
80
  # Second call with same config: loads from cache via _load()
81
- artifact = TrainModel(lr=3e-4, steps=5000).load_or_create()
81
+ artifact = TrainModel(lr=3e-4, steps=5000).get()
82
82
  ```
83
83
 
84
84
  > **Tip:** Define Furu classes in importable modules (not `__main__`); the artifact namespace is derived from the class's module + qualified name.
@@ -96,7 +96,7 @@ Each `Furu` instance maps deterministically to a directory based on its config:
96
96
  - **namespace**: Derived from the class's module + qualified name (e.g., `my_project.pipelines/TrainModel`)
97
97
  - **hash**: Computed from the object's config values using Blake2s
98
98
 
99
- When you call `load_or_create()`:
99
+ When you call `get()`:
100
100
  1. If no cached result exists → run `_create()`, save state as "success"
101
101
  2. If cached result exists → run `_load()` to retrieve it
102
102
  3. If another process is running → wait for it to finish, then load
@@ -123,7 +123,7 @@ class TrainTextModel(furu.Furu[str]):
123
123
  dataset: Dataset = furu.chz.field(default_factory=Dataset)
124
124
 
125
125
  def _create(self) -> str:
126
- data = self.dataset.load_or_create() # Triggers Dataset cache
126
+ data = self.dataset.get() # Triggers Dataset cache
127
127
  (self.furu_dir / "model.txt").write_text(f"trained on:\n{data}")
128
128
  return "trained"
129
129
 
@@ -131,6 +131,58 @@ class TrainTextModel(furu.Furu[str]):
131
131
  return (self.furu_dir / "model.txt").read_text()
132
132
  ```
133
133
 
134
+ ### Executors (Local + Slurm)
135
+
136
+ Use the execution helpers for batch runs and cluster scheduling:
137
+
138
+ ```python
139
+ from furu.execution import run_local
140
+
141
+ run_local(
142
+ [TrainModel(lr=3e-4, steps=5000), TrainModel(lr=1e-3, steps=2000)],
143
+ max_workers=8,
144
+ window_size="bfs",
145
+ )
146
+ ```
147
+
148
+ ```python
149
+ from furu.execution import SlurmSpec, submit_slurm_dag
150
+
151
+ specs = {
152
+ "default": SlurmSpec(partition="cpu", cpus=8, mem_gb=32, time_min=120),
153
+ "gpu": SlurmSpec(partition="gpu", gpus=1, cpus=8, mem_gb=64, time_min=720),
154
+ }
155
+
156
+ submit_slurm_dag([TrainModel(lr=3e-4, steps=5000)], specs=specs)
157
+ ```
158
+
159
+ ```python
160
+ from furu.execution import run_slurm_pool
161
+
162
+ run_slurm_pool(
163
+ [TrainModel(lr=3e-4, steps=5000)],
164
+ specs=specs,
165
+ max_workers_total=50,
166
+ window_size="bfs",
167
+ )
168
+ ```
169
+
170
+ Submitit logs are stored under `<FURU_PATH>/submitit` by default. Override with
171
+ `FURU_SUBMITIT_PATH` when you want a different logs root.
172
+
173
+ ### Breaking Changes and Executor Semantics
174
+
175
+ - `load_or_create()` is removed; use `get()` exclusively.
176
+ - `get()` no longer accepts per-call `retry_failed` overrides. Configure retries via
177
+ `FURU_RETRY_FAILED` or `FURU_CONFIG.retry_failed`.
178
+ - Executor runs (`run_local`, `run_slurm_pool`, `submit_slurm_dag`) fail fast if a
179
+ dependency is FAILED while `retry_failed` is disabled; with retries enabled, failed
180
+ compute nodes are retried (bounded by `FURU_MAX_COMPUTE_RETRIES` retries).
181
+ - Pool protocol/queue failures (invalid payloads, spec mismatch, missing artifacts) are
182
+ fatal even when `retry_failed` is enabled; only compute failures are retried.
183
+ - `FURU_ALWAYS_RERUN` causes matching nodes to recompute once per executor run, but
184
+ repeated references in the same run reuse that result.
185
+
134
186
  ### Storage Structure
135
187
 
136
188
  Furu uses two roots: `FURU_PATH` for `data/` + `raw/`, and
@@ -176,7 +228,7 @@ class MyExperiments(furu.FuruList[TrainModel]):
176
228
 
177
229
  # Iterate over all experiments
178
230
  for exp in MyExperiments:
179
- exp.load_or_create()
231
+ exp.get()
180
232
 
181
233
  # Access by name
182
234
  exp = MyExperiments.by_name("baseline")
@@ -191,14 +243,17 @@ for name, exp in MyExperiments.items():
191
243
 
192
244
  ### Custom Validation
193
245
 
194
- Override `_validate()` to add custom cache invalidation logic:
246
+ Override `_validate()` to add custom cache invalidation logic. Return False or
247
+ raise `furu.FuruValidationError` to force re-computation. In executor planning,
248
+ any other exception is logged and treated as invalid (no crash); in interactive
249
+ `exists()` calls, exceptions still surface:
195
250
 
196
251
  ```python
197
252
  class ModelWithValidation(furu.Furu[Path]):
198
253
  checkpoint_name: str = "model.pt"
199
254
 
200
255
  def _validate(self) -> bool:
201
- # Return False to force re-computation
256
+ # Return False (or raise FuruValidationError) to force re-computation
202
257
  ckpt = self.furu_dir / self.checkpoint_name
203
258
  return ckpt.exists() and ckpt.stat().st_size > 0
204
259
 
@@ -220,7 +275,7 @@ if obj.exists():
220
275
 
221
276
  # Get metadata without triggering computation
222
277
  metadata = obj.get_metadata()
223
- print(f"Hash: {obj._furu_hash}")
278
+ print(f"Hash: {obj.furu_hash}")
224
279
  print(f"Dir: {obj.furu_dir}")
225
280
  ```
226
281
 
@@ -251,7 +306,7 @@ class LargeDataProcessor(furu.Furu[Path]):
251
306
  def _create(self) -> Path:
252
307
  # self.raw_dir is shared across all configs
253
308
  # Create a subfolder for isolation if needed
254
- my_raw = self.raw_dir / self._furu_hash
309
+ my_raw = self.raw_dir / self.furu_hash
255
310
  my_raw.mkdir(exist_ok=True)
256
311
 
257
312
  large_file = my_raw / "huge_dataset.bin"
@@ -303,8 +358,8 @@ HHMMSS file.py:line message
303
358
 
304
359
  Furu emits status messages like:
305
360
  ```
306
- load_or_create TrainModel abc123def (missing->create)
307
- load_or_create TrainModel abc123def (success->load)
361
+ get TrainModel abc123def (missing->create)
362
+ get TrainModel abc123def (success->load)
308
363
  ```
309
364
 
310
365
  ### Explicit Setup
@@ -325,7 +380,7 @@ logger = furu.get_logger()
325
380
  from furu import FuruComputeError, FuruWaitTimeout, FuruLockNotAcquired
326
381
 
327
382
  try:
328
- result = obj.load_or_create()
383
+ result = obj.get()
329
384
  except FuruComputeError as e:
330
385
  print(f"Computation failed: {e}")
331
386
  print(f"State file: {e.state_path}")
@@ -336,29 +391,21 @@ except FuruLockNotAcquired:
336
391
  print("Could not acquire lock")
337
392
  ```
338
393
 
339
- ## Submitit Integration
340
-
341
- Run computations on SLURM clusters via [submitit](https://github.com/facebookincubator/submitit):
394
+ By default, failed artifacts are retried on the next `get()` call. Set
395
+ `FURU_RETRY_FAILED=0` to keep failures sticky.
342
396
 
343
- ```python
344
- import submitit
345
- import furu
397
+ `FURU_MAX_WAIT_SECS` overrides the per-class `_max_wait_time_sec` (default 600s)
398
+ timeout used when waiting for compute locks before raising `FuruWaitTimeout`.
346
399
 
347
- executor = submitit.AutoExecutor(folder="submitit_logs")
348
- executor.update_parameters(
349
- timeout_min=60,
350
- slurm_partition="gpu",
351
- gpus_per_node=1,
352
- )
400
+ Failures during metadata collection or signal handler setup (before `_create()`
401
+ runs) raise `FuruComputeError` with the original exception attached. These
402
+ failures still mark the attempt as failed and record details in `state.json`
403
+ and `furu.log`.
353
404
 
354
- # Submit job and return immediately
355
- job = my_furu_obj.load_or_create(executor=executor)
356
-
357
- # Job ID is tracked in .furu/state.json
358
- print(job.job_id)
359
- ```
405
+ ## Submitit Integration
360
406
 
361
- Furu handles preemption, requeuing, and state tracking automatically.
407
+ Furu includes a `SubmititAdapter` for integrating submitit executors with the
408
+ state system. Executor helpers in `furu.execution` handle submission workflows.
362
409
 
363
410
  ## Dashboard
364
411
 
@@ -415,7 +462,10 @@ The `/api/experiments` endpoint supports:
415
462
  | `FURU_LOG_LEVEL` | `INFO` | Console verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
416
463
  | `FURU_IGNORE_DIFF` | `false` | Skip embedding git diff in metadata |
417
464
  | `FURU_ALWAYS_RERUN` | `""` | Comma-separated class qualnames to always rerun (use `ALL` to bypass cache globally; cannot combine with other entries; entries must be importable) |
465
+ | `FURU_RETRY_FAILED` | `true` | Retry failed artifacts by default (set to `0` to keep failures sticky) |
466
+ | `FURU_MAX_COMPUTE_RETRIES` | `3` | Maximum compute retries per node after the first failure |
418
467
  | `FURU_POLL_INTERVAL_SECS` | `10` | Polling interval for queued/running jobs |
468
+ | `FURU_MAX_WAIT_SECS` | unset | Override wait timeout (falls back to `_max_wait_time_sec`, default 600s) |
419
469
  | `FURU_WAIT_LOG_EVERY_SECS` | `10` | Interval between "waiting" log messages |
420
470
  | `FURU_STALE_AFTER_SECS` | `1800` | Consider running jobs stale after this duration |
421
471
  | `FURU_LEASE_SECS` | `120` | Compute lock lease duration |
@@ -0,0 +1,46 @@
1
+ furu/__init__.py,sha256=Z8VssTuQm2nH7bgB8SQc8pXsNGc-H1QGHFffKzNzqk8,2018
2
+ furu/adapters/__init__.py,sha256=onLzEj9hccPK15g8a8va2T19nqQXoxb9rQlJIjKSKnE,69
3
+ furu/adapters/submitit.py,sha256=FV3XEUSQuS5vIyzkW-Iuqtf8SRL-fsokPG67u7tMF5I,7276
4
+ furu/config.py,sha256=1nlJff4KNrWDvLhmnuLrsc7FJIxFLFhz3eOXZ8-ngX4,7349
5
+ furu/core/__init__.py,sha256=6hH7i6r627c0FZn6eQVsSG7LD4QmTta6iQw0AiPQPTM,156
6
+ furu/core/furu.py,sha256=Cy2cOnM5vsQoSk9nIVYj2Fx017wOQFPbxhnvYQsh7nI,58881
7
+ furu/core/list.py,sha256=xSuBT35p1anJ2fKQPxb-3cRTONUamFjfzkreVaI9Jo4,3614
8
+ furu/dashboard/__init__.py,sha256=ziAordJfkbbXNIM7iA9O7vR2gsCq34AInYiMYOCfWOc,362
9
+ furu/dashboard/__main__.py,sha256=cNs65IMl4kwZFpxa9xLXmFSy4-M5D1X1ZBfTDxW11vo,144
10
+ furu/dashboard/api/__init__.py,sha256=9-WyWOt-VQJJBIsdW29D-7JvR-BivJd9G_SRaRptCz0,80
11
+ furu/dashboard/api/models.py,sha256=SCu-kLJyW7dwSKswdgQNS3wQuj25ORs0pHkvX9xBbo4,4767
12
+ furu/dashboard/api/routes.py,sha256=iZez0khIUvbgfeSoy1BJvmoEEbgUrdSQA8SN8iAIkM8,4813
13
+ furu/dashboard/frontend/dist/assets/index-BXAIKNNr.css,sha256=qhsN0Td3mM-GAR8mZ0CtocynABLKa1ncl9ioDrTKOIQ,34768
14
+ furu/dashboard/frontend/dist/assets/index-DS3FsqcY.js,sha256=nfrKjhWThPtL8n5iTd9_1W-bsyMGwg2O8Iq2jkjj9Lg,544699
15
+ furu/dashboard/frontend/dist/favicon.svg,sha256=3TSLHNZITFe3JTPoYHZnDgiGsJxIzf39v97l2A1Hodo,369
16
+ furu/dashboard/frontend/dist/index.html,sha256=d9a8ZFKZ5uDtN3urqVNmS8LWMBhOC0eW7X0noT0RcYQ,810
17
+ furu/dashboard/main.py,sha256=gj9Cdj2qyaSCEkmfNHUMQXlXv6GpWTQ9IZEi7WzlCSo,4463
18
+ furu/dashboard/scanner.py,sha256=qXCvkvFByBc09TUdth5Js67rS8zpRBlRkVQ9dJ7YbdE,34696
19
+ furu/errors.py,sha256=FFbV4M0-ipVGizv5ee80L-NZFVjaRjy8i19mClr6R0g,3959
20
+ furu/execution/__init__.py,sha256=ixVw1Shvg2ulS597OYYeGgSSTwv25j_McuQdDXIiEL8,625
21
+ furu/execution/context.py,sha256=0tAbM0azqEus8hknf_A9-Zs9Sq99bnUkFyV4RO4ZMRU,666
22
+ furu/execution/local.py,sha256=TkKrRdmaQrN7i7Sxe87eHibRJOnz5OxU0Oj8qL_xP4I,7059
23
+ furu/execution/paths.py,sha256=0MfQk5Kh7bxvJiWvG40TJe7RF5Q5Na6uvi6qV0OT3Vc,460
24
+ furu/execution/plan.py,sha256=fM7CkXm_M0lL3vqdiNnWzbvMJAoSYKDBAnC82Af_rYM,6860
25
+ furu/execution/plan_utils.py,sha256=TAQqlPeJfOdH2MT-X7g3j1Se_0e4oKvG0tJaWC1kM40,381
26
+ furu/execution/slurm_dag.py,sha256=FOJcPKmIzRyrbJIq7heqGjKN0EFRMyOcV-yP7Ci87Qs,9360
27
+ furu/execution/slurm_pool.py,sha256=bi90fzZXAnoWHSPQba8Z3tk4_QMaqikWxCCzRfvDMvk,30400
28
+ furu/execution/slurm_spec.py,sha256=A1VX5K6aG8Ricg4fhnkz3Alkw_fx1bx53D0p4Ms3FqA,979
29
+ furu/execution/submitit_factory.py,sha256=B2vkDtmscuAX0sBaj9V5pNlgOtkkV35yJ1fZ7A-DSvU,1119
30
+ furu/migrate.py,sha256=x_Uh7oXAv40L5ZAHJhdnw-o7ct56rWUSZLbHHfRObeY,1313
31
+ furu/migration.py,sha256=R2-tARMx4VKryiqJ7WHia_dPVxRbTqofPpCFVE9zQ8U,31411
32
+ furu/runtime/__init__.py,sha256=fQqE7wUuWunLD73Vm3lss7BFSij3UVxXOKQXBAOS8zw,504
33
+ furu/runtime/env.py,sha256=o1phhoTDhOnhALr3Ozf1ldrdvk2ClyEvBWbebHM6BXg,160
34
+ furu/runtime/logging.py,sha256=WS3mB8VqMYUxPPI0yv1K-LnzVBj84Mnu1Qf9P2hCUUE,9652
35
+ furu/runtime/tracebacks.py,sha256=PGCuOq8QkWSoun791gjUXM8frOP2wWV8IBlqaA4nuGE,1631
36
+ furu/serialization/__init__.py,sha256=L7oHuIbxdSh7GCY3thMQnDwlt_ERH-TMy0YKEAZLrPs,341
37
+ furu/serialization/migrations.py,sha256=HD5g8JCBdH3Y0rHJYc4Ug1IXBVcUDxLE7nfiXZnXcUE,7772
38
+ furu/serialization/serializer.py,sha256=_nfUaAOy_KHegvfXlpPh4rCuvkzalJva75OvDg5nXiI,10114
39
+ furu/storage/__init__.py,sha256=cLLL-GPpSu9C72Mdk5S6TGu3g-SnBfEuxzfpx5ZJPtw,616
40
+ furu/storage/metadata.py,sha256=MH6w5hs-2rwHD6G9erMPM5pE3hm0h5Pk_G3Z6eyyGB0,9899
41
+ furu/storage/migration.py,sha256=Ars9aYwvhXpIBDf6L9ojGjp_l656-RfdtEAFKN0sZZY,2640
42
+ furu/storage/state.py,sha256=SFonqragT2eMCZbBKIvcA4JVe78rVmDRvo4Ky2IcNgc,43632
43
+ furu-0.0.4.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
44
+ furu-0.0.4.dist-info/entry_points.txt,sha256=hZkjtFzNlb33Zk-aUfLMRj-XgVDxdT82-JXG9d4bu2E,60
45
+ furu-0.0.4.dist-info/METADATA,sha256=fdUBvn-vEnVim9V5hAamE1sFuaKzWdwWPI17VU2Vyfc,16162
46
+ furu-0.0.4.dist-info/RECORD,,
@@ -1,36 +0,0 @@
1
- furu/__init__.py,sha256=fhSViHOJ9W-64swuaBFdZOfq0ZMuSj6LSiX2ZfcjhD8,1736
2
- furu/adapters/__init__.py,sha256=onLzEj9hccPK15g8a8va2T19nqQXoxb9rQlJIjKSKnE,69
3
- furu/adapters/submitit.py,sha256=OuCP0pEkO1kI4WLcSUvMqXwVCCy-8uwUE7v1qvkLZnU,6214
4
- furu/config.py,sha256=C9mYQLgP4ciPmONCpQUu2YVV8adscCkfLsiyjXZVcpQ,6636
5
- furu/core/__init__.py,sha256=gzFMgaAYnffofQksR6E1NegiwBF99h0ysn_QeD5wIhw,82
6
- furu/core/furu.py,sha256=7swlMfGXBB_jmGABgMSl28v_qiE8Ot4vuDSos42cweQ,39085
7
- furu/core/list.py,sha256=hwwlvqaKB1grPBGKXc15scF1RCqDvWc0AoDbhKlN4W0,3625
8
- furu/dashboard/__init__.py,sha256=zNVddterfpjQtcpihIl3TRJdgdjOHYR0uO0cOSaGABg,172
9
- furu/dashboard/__main__.py,sha256=cNs65IMl4kwZFpxa9xLXmFSy4-M5D1X1ZBfTDxW11vo,144
10
- furu/dashboard/api/__init__.py,sha256=9-WyWOt-VQJJBIsdW29D-7JvR-BivJd9G_SRaRptCz0,80
11
- furu/dashboard/api/models.py,sha256=SCu-kLJyW7dwSKswdgQNS3wQuj25ORs0pHkvX9xBbo4,4767
12
- furu/dashboard/api/routes.py,sha256=iZez0khIUvbgfeSoy1BJvmoEEbgUrdSQA8SN8iAIkM8,4813
13
- furu/dashboard/frontend/dist/assets/index-CbdDfSOZ.css,sha256=k3kxCuCqyxKgIv4M9itoAImMU8NMzkzAdTNQ4v_4fMU,34612
14
- furu/dashboard/frontend/dist/assets/index-DDv_TYB_.js,sha256=FH0uqY7P7vm3rikvDaJ504FZh0Z97nCkVcIglK-ElAY,543928
15
- furu/dashboard/frontend/dist/favicon.svg,sha256=3TSLHNZITFe3JTPoYHZnDgiGsJxIzf39v97l2A1Hodo,369
16
- furu/dashboard/frontend/dist/index.html,sha256=o3XhvegC9rBpUiWNfXdCHqf_tg2795nob1NI0nBpFS4,810
17
- furu/dashboard/main.py,sha256=8JYc79gbJ9MjvIRdGDuAcR2Mme9kyY4ryZb11ZZ4uVA,4069
18
- furu/dashboard/scanner.py,sha256=qXCvkvFByBc09TUdth5Js67rS8zpRBlRkVQ9dJ7YbdE,34696
19
- furu/errors.py,sha256=d1Kp5O9cVoQwXmQeZC-35u7xldw_c3ryYXrbVfv-Lws,2001
20
- furu/migrate.py,sha256=x_Uh7oXAv40L5ZAHJhdnw-o7ct56rWUSZLbHHfRObeY,1313
21
- furu/migration.py,sha256=A91dng1XRn1N_xJrmBhh-OvU22GlseqOh6PmVhNZh3w,31307
22
- furu/runtime/__init__.py,sha256=fQqE7wUuWunLD73Vm3lss7BFSij3UVxXOKQXBAOS8zw,504
23
- furu/runtime/env.py,sha256=o1phhoTDhOnhALr3Ozf1ldrdvk2ClyEvBWbebHM6BXg,160
24
- furu/runtime/logging.py,sha256=JkuTFtbv6dYk088P6_Bga46bnKSDt-ElAqmiY86hMys,9773
25
- furu/runtime/tracebacks.py,sha256=PGCuOq8QkWSoun791gjUXM8frOP2wWV8IBlqaA4nuGE,1631
26
- furu/serialization/__init__.py,sha256=L7oHuIbxdSh7GCY3thMQnDwlt_ERH-TMy0YKEAZLrPs,341
27
- furu/serialization/migrations.py,sha256=HD5g8JCBdH3Y0rHJYc4Ug1IXBVcUDxLE7nfiXZnXcUE,7772
28
- furu/serialization/serializer.py,sha256=THWqHzpSwXj3Nj3PZ3JhwlWJ8sgvVyGrwBEDB_EWuAE,8355
29
- furu/storage/__init__.py,sha256=cLLL-GPpSu9C72Mdk5S6TGu3g-SnBfEuxzfpx5ZJPtw,616
30
- furu/storage/metadata.py,sha256=u4F4V1dDZtsiniO5xDCy8YxJZxGnreriYnJ1fOvQ2Bg,9232
31
- furu/storage/migration.py,sha256=Ars9aYwvhXpIBDf6L9ojGjp_l656-RfdtEAFKN0sZZY,2640
32
- furu/storage/state.py,sha256=q8wWJnGMWzx56PfsRMAMRB62p5vVw-iZ5rnUPfw2-js,40878
33
- furu-0.0.2.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
34
- furu-0.0.2.dist-info/entry_points.txt,sha256=hZkjtFzNlb33Zk-aUfLMRj-XgVDxdT82-JXG9d4bu2E,60
35
- furu-0.0.2.dist-info/METADATA,sha256=8Zvp5E5XHn11a8YedVhAOi9KAvH9LwKvxiW4Jn7-Hsg,13833
36
- furu-0.0.2.dist-info/RECORD,,
File without changes