furu 0.0.1__tar.gz → 0.0.3__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.
- furu-0.0.1/README.md → furu-0.0.3/PKG-INFO +60 -14
- furu-0.0.1/PKG-INFO → furu-0.0.3/README.md +41 -32
- {furu-0.0.1 → furu-0.0.3}/pyproject.toml +3 -9
- {furu-0.0.1 → furu-0.0.3}/src/furu/__init__.py +3 -1
- furu-0.0.3/src/furu/config.py +178 -0
- furu-0.0.3/src/furu/core/__init__.py +4 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/core/furu.py +438 -75
- furu-0.0.1/src/furu/dashboard/frontend/dist/assets/index-CbdDfSOZ.css → furu-0.0.3/src/furu/dashboard/frontend/dist/assets/index-BXAIKNNr.css +1 -1
- furu-0.0.1/src/furu/dashboard/frontend/dist/assets/index-DDv_TYB_.js → furu-0.0.3/src/furu/dashboard/frontend/dist/assets/index-DS3FsqcY.js +3 -3
- {furu-0.0.1/dashboard-frontend → furu-0.0.3/src/furu/dashboard/frontend/dist}/index.html +2 -1
- {furu-0.0.1 → furu-0.0.3}/src/furu/errors.py +47 -5
- {furu-0.0.1 → furu-0.0.3}/src/furu/migration.py +8 -4
- {furu-0.0.1 → furu-0.0.3}/src/furu/serialization/serializer.py +40 -2
- {furu-0.0.1 → furu-0.0.3}/src/furu/storage/metadata.py +17 -5
- {furu-0.0.1 → furu-0.0.3}/src/furu/storage/state.py +115 -3
- furu-0.0.1/.github/workflows/ci.yml +0 -33
- furu-0.0.1/.github/workflows/release.yml +0 -99
- furu-0.0.1/.gitignore +0 -231
- furu-0.0.1/.vscode/settings.json +0 -3
- furu-0.0.1/AGENTS.md +0 -265
- furu-0.0.1/CHANGELOG.md +0 -5
- furu-0.0.1/Makefile +0 -156
- furu-0.0.1/TODO.md +0 -131
- furu-0.0.1/dashboard-frontend/bun.lock +0 -1234
- furu-0.0.1/dashboard-frontend/orval.config.ts +0 -34
- furu-0.0.1/dashboard-frontend/package-lock.json +0 -7628
- furu-0.0.1/dashboard-frontend/package.json +0 -40
- furu-0.0.1/dashboard-frontend/postcss.config.js +0 -9
- furu-0.0.1/dashboard-frontend/src/api.test.ts +0 -314
- furu-0.0.1/dashboard-frontend/src/components/DAGVisualization.tsx +0 -392
- furu-0.0.1/dashboard-frontend/src/components/EmptyState.tsx +0 -27
- furu-0.0.1/dashboard-frontend/src/components/StatsCard.tsx +0 -63
- furu-0.0.1/dashboard-frontend/src/components/StatusBadge.tsx +0 -40
- furu-0.0.1/dashboard-frontend/src/components/ui/badge.tsx +0 -35
- furu-0.0.1/dashboard-frontend/src/components/ui/button.tsx +0 -56
- furu-0.0.1/dashboard-frontend/src/components/ui/card.tsx +0 -70
- furu-0.0.1/dashboard-frontend/src/components/ui/input.tsx +0 -20
- furu-0.0.1/dashboard-frontend/src/components/ui/table.tsx +0 -96
- furu-0.0.1/dashboard-frontend/src/index.css +0 -78
- furu-0.0.1/dashboard-frontend/src/lib/api-client.ts +0 -26
- furu-0.0.1/dashboard-frontend/src/lib/utils.ts +0 -6
- furu-0.0.1/dashboard-frontend/src/main.tsx +0 -52
- furu-0.0.1/dashboard-frontend/src/routes/__root.tsx +0 -68
- furu-0.0.1/dashboard-frontend/src/routes/dag.tsx +0 -113
- furu-0.0.1/dashboard-frontend/src/routes/experiments.tsx +0 -444
- furu-0.0.1/dashboard-frontend/src/routes/experiments_.$namespace.$furu_hash.tsx +0 -804
- furu-0.0.1/dashboard-frontend/src/routes/index.tsx +0 -195
- furu-0.0.1/dashboard-frontend/tailwind.config.js +0 -41
- furu-0.0.1/dashboard-frontend/tsconfig.json +0 -24
- furu-0.0.1/dashboard-frontend/tsconfig.node.json +0 -14
- furu-0.0.1/dashboard-frontend/vite.config.ts +0 -21
- furu-0.0.1/e2e/bun.lock +0 -21
- furu-0.0.1/e2e/generate_data.py +0 -347
- furu-0.0.1/e2e/global-setup.ts +0 -37
- furu-0.0.1/e2e/global-teardown.ts +0 -14
- furu-0.0.1/e2e/package.json +0 -15
- furu-0.0.1/e2e/playwright.config.ts +0 -66
- furu-0.0.1/e2e/tests/api.spec.ts +0 -182
- furu-0.0.1/e2e/tests/experiments.spec.ts +0 -163
- furu-0.0.1/e2e/tests/home.spec.ts +0 -36
- furu-0.0.1/e2e/tests/navigation.spec.ts +0 -435
- furu-0.0.1/examples/README.md +0 -21
- furu-0.0.1/examples/my_project/__init__.py +0 -3
- furu-0.0.1/examples/my_project/pipelines.py +0 -65
- furu-0.0.1/examples/run_logging.py +0 -26
- furu-0.0.1/examples/run_nested.py +0 -26
- furu-0.0.1/examples/run_train.py +0 -26
- furu-0.0.1/src/furu/config.py +0 -98
- furu-0.0.1/src/furu/core/__init__.py +0 -4
- furu-0.0.1/src/furu/dashboard/frontend/dist/favicon.svg +0 -10
- furu-0.0.1/src/furu/dashboard/frontend/dist/index.html +0 -22
- furu-0.0.1/tasks/migration.md +0 -360
- furu-0.0.1/tests/conftest.py +0 -25
- furu-0.0.1/tests/dashboard/__init__.py +0 -4
- furu-0.0.1/tests/dashboard/conftest.py +0 -448
- furu-0.0.1/tests/dashboard/pipelines.py +0 -192
- furu-0.0.1/tests/dashboard/test_api.py +0 -865
- furu-0.0.1/tests/dashboard/test_scanner.py +0 -786
- furu-0.0.1/tests/test_config.py +0 -10
- furu-0.0.1/tests/test_errors.py +0 -83
- furu-0.0.1/tests/test_furu_core.py +0 -158
- furu-0.0.1/tests/test_furu_inheritance.py +0 -44
- furu-0.0.1/tests/test_furu_inheritance_polymorphic.py +0 -118
- furu-0.0.1/tests/test_furu_list.py +0 -30
- furu-0.0.1/tests/test_furu_typing.py +0 -208
- furu-0.0.1/tests/test_logger.py +0 -141
- furu-0.0.1/tests/test_metadata.py +0 -46
- furu-0.0.1/tests/test_migrations.py +0 -848
- furu-0.0.1/tests/test_raw_dir.py +0 -15
- furu-0.0.1/tests/test_serializer.py +0 -71
- furu-0.0.1/tests/test_state_manager.py +0 -464
- furu-0.0.1/tests/test_submitit_path.py +0 -71
- furu-0.0.1/tests/test_tracebacks.py +0 -5
- furu-0.0.1/uv.lock +0 -745
- furu-0.0.1/version.txt +0 -1
- {furu-0.0.1 → furu-0.0.3}/src/furu/adapters/__init__.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/adapters/submitit.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/core/list.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/dashboard/__init__.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/dashboard/__main__.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/dashboard/api/__init__.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/dashboard/api/models.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/dashboard/api/routes.py +0 -0
- {furu-0.0.1/dashboard-frontend/public → furu-0.0.3/src/furu/dashboard/frontend/dist}/favicon.svg +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/dashboard/main.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/dashboard/scanner.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/migrate.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/runtime/__init__.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/runtime/env.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/runtime/logging.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/runtime/tracebacks.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/serialization/__init__.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/serialization/migrations.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/storage/__init__.py +0 -0
- {furu-0.0.1 → furu-0.0.3}/src/furu/storage/migration.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: furu
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Cacheable, nested pipelines for Python. Define computations as configs; furu handles caching, state tracking, and result reuse across runs.
|
|
5
|
+
Author: Herman Brunborg
|
|
6
|
+
Author-email: Herman Brunborg <herman@brunborg.com>
|
|
7
|
+
Requires-Dist: chz>=0.4.0
|
|
8
|
+
Requires-Dist: cloudpickle>=3.1.1
|
|
9
|
+
Requires-Dist: pydantic>=2.12.5
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
+
Requires-Dist: rich>=14.2.0
|
|
12
|
+
Requires-Dist: submitit>=1.5.3
|
|
13
|
+
Requires-Dist: fastapi>=0.109.0 ; extra == 'dashboard'
|
|
14
|
+
Requires-Dist: uvicorn[standard]>=0.27.0 ; extra == 'dashboard'
|
|
15
|
+
Requires-Dist: typer>=0.9.0 ; extra == 'dashboard'
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Provides-Extra: dashboard
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
1
20
|
# furu
|
|
2
21
|
|
|
3
22
|
> **Note:** `v0.0.x` is alpha and may include breaking changes.
|
|
@@ -114,20 +133,25 @@ class TrainTextModel(furu.Furu[str]):
|
|
|
114
133
|
|
|
115
134
|
### Storage Structure
|
|
116
135
|
|
|
136
|
+
Furu uses two roots: `FURU_PATH` for `data/` + `raw/`, and
|
|
137
|
+
`FURU_VERSION_CONTROLLED_PATH` for `artifacts/`. Defaults:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
FURU_PATH=<project>/furu-data
|
|
141
|
+
FURU_VERSION_CONTROLLED_PATH=<project>/furu-data/artifacts
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`<project>` is the nearest directory containing `pyproject.toml` (falling back to
|
|
145
|
+
the git root). This means you can move `FURU_PATH` without relocating artifacts.
|
|
146
|
+
|
|
117
147
|
```
|
|
118
148
|
$FURU_PATH/
|
|
119
|
-
├── data/ #
|
|
120
|
-
│ └── <module>/<Class>/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
│ │ ├── furu.log # Captured logs
|
|
126
|
-
│ │ └── SUCCESS.json # Marker file
|
|
127
|
-
│ └── <your outputs> # Files from _create()
|
|
128
|
-
├── git/ # For version_controlled=True
|
|
129
|
-
│ └── <same structure>
|
|
130
|
-
└── raw/ # Shared directory for large files
|
|
149
|
+
├── data/ # version_controlled=False
|
|
150
|
+
│ └── <module>/<Class>/<hash>/
|
|
151
|
+
└── raw/
|
|
152
|
+
|
|
153
|
+
$FURU_VERSION_CONTROLLED_PATH/ # version_controlled=True
|
|
154
|
+
└── <module>/<Class>/<hash>/
|
|
131
155
|
```
|
|
132
156
|
|
|
133
157
|
## Features
|
|
@@ -241,10 +265,17 @@ For artifacts that should be stored separately (e.g., checked into git):
|
|
|
241
265
|
|
|
242
266
|
```python
|
|
243
267
|
class VersionedConfig(furu.Furu[dict], version_controlled=True):
|
|
244
|
-
# Stored under $
|
|
268
|
+
# Stored under $FURU_VERSION_CONTROLLED_PATH
|
|
269
|
+
# Default: <project>/furu-data/artifacts
|
|
245
270
|
...
|
|
246
271
|
```
|
|
247
272
|
|
|
273
|
+
`<project>` is the nearest directory containing `pyproject.toml`, or the git root
|
|
274
|
+
if `pyproject.toml` is missing.
|
|
275
|
+
|
|
276
|
+
It is typical to keep `furu-data/data/` and `furu-data/raw/` in `.gitignore` while
|
|
277
|
+
committing `furu-data/artifacts/`.
|
|
278
|
+
|
|
248
279
|
## Logging
|
|
249
280
|
|
|
250
281
|
Furu installs stdlib `logging` handlers that capture logs to per-artifact files.
|
|
@@ -305,6 +336,17 @@ except FuruLockNotAcquired:
|
|
|
305
336
|
print("Could not acquire lock")
|
|
306
337
|
```
|
|
307
338
|
|
|
339
|
+
By default, failed artifacts are retried on the next `load_or_create()` call. Set
|
|
340
|
+
`FURU_RETRY_FAILED=0` or pass `retry_failed=False` to keep failures sticky.
|
|
341
|
+
|
|
342
|
+
`FURU_MAX_WAIT_SECS` overrides the per-class `_max_wait_time_sec` (default 600s)
|
|
343
|
+
timeout used when waiting for compute locks before raising `FuruWaitTimeout`.
|
|
344
|
+
|
|
345
|
+
Failures during metadata collection or signal handler setup (before `_create()`
|
|
346
|
+
runs) raise `FuruComputeError` with the original exception attached. These
|
|
347
|
+
failures still mark the attempt as failed and record details in `state.json`
|
|
348
|
+
and `furu.log`.
|
|
349
|
+
|
|
308
350
|
## Submitit Integration
|
|
309
351
|
|
|
310
352
|
Run computations on SLURM clusters via [submitit](https://github.com/facebookincubator/submitit):
|
|
@@ -379,10 +421,14 @@ The `/api/experiments` endpoint supports:
|
|
|
379
421
|
|
|
380
422
|
| Variable | Default | Description |
|
|
381
423
|
|----------|---------|-------------|
|
|
382
|
-
| `FURU_PATH` |
|
|
424
|
+
| `FURU_PATH` | `<project>/furu-data` | Base storage directory for non-versioned artifacts |
|
|
425
|
+
| `FURU_VERSION_CONTROLLED_PATH` | `<project>/furu-data/artifacts` | Override version-controlled storage root |
|
|
383
426
|
| `FURU_LOG_LEVEL` | `INFO` | Console verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
384
427
|
| `FURU_IGNORE_DIFF` | `false` | Skip embedding git diff in metadata |
|
|
428
|
+
| `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) |
|
|
429
|
+
| `FURU_RETRY_FAILED` | `true` | Retry failed artifacts by default (set to `0` to keep failures sticky) |
|
|
385
430
|
| `FURU_POLL_INTERVAL_SECS` | `10` | Polling interval for queued/running jobs |
|
|
431
|
+
| `FURU_MAX_WAIT_SECS` | unset | Override wait timeout (falls back to `_max_wait_time_sec`, default 600s) |
|
|
386
432
|
| `FURU_WAIT_LOG_EVERY_SECS` | `10` | Interval between "waiting" log messages |
|
|
387
433
|
| `FURU_STALE_AFTER_SECS` | `1800` | Consider running jobs stale after this duration |
|
|
388
434
|
| `FURU_LEASE_SECS` | `120` | Compute lock lease duration |
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: furu
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: Cacheable, nested pipelines for Python. Define computations as configs; furu handles caching, state tracking, and result reuse across runs.
|
|
5
|
-
Author-email: Herman Brunborg <herman@brunborg.com>
|
|
6
|
-
Requires-Python: >=3.12
|
|
7
|
-
Requires-Dist: chz>=0.4.0
|
|
8
|
-
Requires-Dist: cloudpickle>=3.1.1
|
|
9
|
-
Requires-Dist: pydantic>=2.12.5
|
|
10
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
-
Requires-Dist: rich>=14.2.0
|
|
12
|
-
Requires-Dist: submitit>=1.5.3
|
|
13
|
-
Provides-Extra: dashboard
|
|
14
|
-
Requires-Dist: fastapi>=0.109.0; extra == 'dashboard'
|
|
15
|
-
Requires-Dist: typer>=0.9.0; extra == 'dashboard'
|
|
16
|
-
Requires-Dist: uvicorn[standard]>=0.27.0; extra == 'dashboard'
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
1
|
# furu
|
|
20
2
|
|
|
21
3
|
> **Note:** `v0.0.x` is alpha and may include breaking changes.
|
|
@@ -132,20 +114,25 @@ class TrainTextModel(furu.Furu[str]):
|
|
|
132
114
|
|
|
133
115
|
### Storage Structure
|
|
134
116
|
|
|
117
|
+
Furu uses two roots: `FURU_PATH` for `data/` + `raw/`, and
|
|
118
|
+
`FURU_VERSION_CONTROLLED_PATH` for `artifacts/`. Defaults:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
FURU_PATH=<project>/furu-data
|
|
122
|
+
FURU_VERSION_CONTROLLED_PATH=<project>/furu-data/artifacts
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`<project>` is the nearest directory containing `pyproject.toml` (falling back to
|
|
126
|
+
the git root). This means you can move `FURU_PATH` without relocating artifacts.
|
|
127
|
+
|
|
135
128
|
```
|
|
136
129
|
$FURU_PATH/
|
|
137
|
-
├── data/ #
|
|
138
|
-
│ └── <module>/<Class>/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
│ │ ├── furu.log # Captured logs
|
|
144
|
-
│ │ └── SUCCESS.json # Marker file
|
|
145
|
-
│ └── <your outputs> # Files from _create()
|
|
146
|
-
├── git/ # For version_controlled=True
|
|
147
|
-
│ └── <same structure>
|
|
148
|
-
└── raw/ # Shared directory for large files
|
|
130
|
+
├── data/ # version_controlled=False
|
|
131
|
+
│ └── <module>/<Class>/<hash>/
|
|
132
|
+
└── raw/
|
|
133
|
+
|
|
134
|
+
$FURU_VERSION_CONTROLLED_PATH/ # version_controlled=True
|
|
135
|
+
└── <module>/<Class>/<hash>/
|
|
149
136
|
```
|
|
150
137
|
|
|
151
138
|
## Features
|
|
@@ -259,10 +246,17 @@ For artifacts that should be stored separately (e.g., checked into git):
|
|
|
259
246
|
|
|
260
247
|
```python
|
|
261
248
|
class VersionedConfig(furu.Furu[dict], version_controlled=True):
|
|
262
|
-
# Stored under $
|
|
249
|
+
# Stored under $FURU_VERSION_CONTROLLED_PATH
|
|
250
|
+
# Default: <project>/furu-data/artifacts
|
|
263
251
|
...
|
|
264
252
|
```
|
|
265
253
|
|
|
254
|
+
`<project>` is the nearest directory containing `pyproject.toml`, or the git root
|
|
255
|
+
if `pyproject.toml` is missing.
|
|
256
|
+
|
|
257
|
+
It is typical to keep `furu-data/data/` and `furu-data/raw/` in `.gitignore` while
|
|
258
|
+
committing `furu-data/artifacts/`.
|
|
259
|
+
|
|
266
260
|
## Logging
|
|
267
261
|
|
|
268
262
|
Furu installs stdlib `logging` handlers that capture logs to per-artifact files.
|
|
@@ -323,6 +317,17 @@ except FuruLockNotAcquired:
|
|
|
323
317
|
print("Could not acquire lock")
|
|
324
318
|
```
|
|
325
319
|
|
|
320
|
+
By default, failed artifacts are retried on the next `load_or_create()` call. Set
|
|
321
|
+
`FURU_RETRY_FAILED=0` or pass `retry_failed=False` to keep failures sticky.
|
|
322
|
+
|
|
323
|
+
`FURU_MAX_WAIT_SECS` overrides the per-class `_max_wait_time_sec` (default 600s)
|
|
324
|
+
timeout used when waiting for compute locks before raising `FuruWaitTimeout`.
|
|
325
|
+
|
|
326
|
+
Failures during metadata collection or signal handler setup (before `_create()`
|
|
327
|
+
runs) raise `FuruComputeError` with the original exception attached. These
|
|
328
|
+
failures still mark the attempt as failed and record details in `state.json`
|
|
329
|
+
and `furu.log`.
|
|
330
|
+
|
|
326
331
|
## Submitit Integration
|
|
327
332
|
|
|
328
333
|
Run computations on SLURM clusters via [submitit](https://github.com/facebookincubator/submitit):
|
|
@@ -397,10 +402,14 @@ The `/api/experiments` endpoint supports:
|
|
|
397
402
|
|
|
398
403
|
| Variable | Default | Description |
|
|
399
404
|
|----------|---------|-------------|
|
|
400
|
-
| `FURU_PATH` |
|
|
405
|
+
| `FURU_PATH` | `<project>/furu-data` | Base storage directory for non-versioned artifacts |
|
|
406
|
+
| `FURU_VERSION_CONTROLLED_PATH` | `<project>/furu-data/artifacts` | Override version-controlled storage root |
|
|
401
407
|
| `FURU_LOG_LEVEL` | `INFO` | Console verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
402
408
|
| `FURU_IGNORE_DIFF` | `false` | Skip embedding git diff in metadata |
|
|
409
|
+
| `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) |
|
|
410
|
+
| `FURU_RETRY_FAILED` | `true` | Retry failed artifacts by default (set to `0` to keep failures sticky) |
|
|
403
411
|
| `FURU_POLL_INTERVAL_SECS` | `10` | Polling interval for queued/running jobs |
|
|
412
|
+
| `FURU_MAX_WAIT_SECS` | unset | Override wait timeout (falls back to `_max_wait_time_sec`, default 600s) |
|
|
404
413
|
| `FURU_WAIT_LOG_EVERY_SECS` | `10` | Interval between "waiting" log messages |
|
|
405
414
|
| `FURU_STALE_AFTER_SECS` | `1800` | Consider running jobs stale after this duration |
|
|
406
415
|
| `FURU_LEASE_SECS` | `120` | Compute lock lease duration |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "furu"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
4
4
|
description = "Cacheable, nested pipelines for Python. Define computations as configs; furu handles caching, state tracking, and result reuse across runs."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -42,14 +42,8 @@ test = [
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
[build-system]
|
|
45
|
-
requires = ["
|
|
46
|
-
build-backend = "
|
|
47
|
-
|
|
48
|
-
[tool.hatch.build]
|
|
49
|
-
artifacts = ["src/furu/dashboard/frontend/dist/**/*"]
|
|
50
|
-
|
|
51
|
-
[tool.hatch.build.targets.wheel]
|
|
52
|
-
packages = ["src/furu"]
|
|
45
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
46
|
+
build-backend = "uv_build"
|
|
53
47
|
|
|
54
48
|
[tool.pytest.ini_options]
|
|
55
49
|
testpaths = ["tests"]
|
|
@@ -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",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FuruConfig:
|
|
7
|
+
"""Central configuration for Furu behavior."""
|
|
8
|
+
|
|
9
|
+
DEFAULT_ROOT_DIR = Path("furu-data")
|
|
10
|
+
VERSION_CONTROLLED_SUBDIR = DEFAULT_ROOT_DIR / "artifacts"
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
def _get_base_root() -> Path:
|
|
14
|
+
env = os.getenv("FURU_PATH")
|
|
15
|
+
if env:
|
|
16
|
+
return Path(env).expanduser().resolve()
|
|
17
|
+
project_root = self._find_project_root(fallback_to_cwd=True)
|
|
18
|
+
return (project_root / self.DEFAULT_ROOT_DIR).resolve()
|
|
19
|
+
|
|
20
|
+
self.base_root = _get_base_root()
|
|
21
|
+
self.version_controlled_root_override = self._get_version_controlled_override()
|
|
22
|
+
self.poll_interval = float(os.getenv("FURU_POLL_INTERVAL_SECS", "10"))
|
|
23
|
+
self.wait_log_every_sec = float(os.getenv("FURU_WAIT_LOG_EVERY_SECS", "10"))
|
|
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
|
|
27
|
+
self.lease_duration_sec = float(os.getenv("FURU_LEASE_SECS", "120"))
|
|
28
|
+
hb = os.getenv("FURU_HEARTBEAT_SECS")
|
|
29
|
+
self.heartbeat_interval_sec = (
|
|
30
|
+
float(hb) if hb is not None else max(1.0, self.lease_duration_sec / 3.0)
|
|
31
|
+
)
|
|
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
|
+
}
|
|
38
|
+
self.ignore_git_diff = os.getenv("FURU_IGNORE_DIFF", "0").lower() in {
|
|
39
|
+
"1",
|
|
40
|
+
"true",
|
|
41
|
+
"yes",
|
|
42
|
+
}
|
|
43
|
+
self.require_git = os.getenv("FURU_REQUIRE_GIT", "1").lower() in {
|
|
44
|
+
"1",
|
|
45
|
+
"true",
|
|
46
|
+
"yes",
|
|
47
|
+
}
|
|
48
|
+
self.require_git_remote = os.getenv("FURU_REQUIRE_GIT_REMOTE", "1").lower() in {
|
|
49
|
+
"1",
|
|
50
|
+
"true",
|
|
51
|
+
"yes",
|
|
52
|
+
}
|
|
53
|
+
always_rerun_items = {
|
|
54
|
+
item.strip()
|
|
55
|
+
for item in os.getenv("FURU_ALWAYS_RERUN", "").split(",")
|
|
56
|
+
if item.strip()
|
|
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
|
|
70
|
+
self.cancelled_is_preempted = os.getenv(
|
|
71
|
+
"FURU_CANCELLED_IS_PREEMPTED", "false"
|
|
72
|
+
).lower() in {"1", "true", "yes"}
|
|
73
|
+
|
|
74
|
+
# Parse FURU_CACHE_METADATA: "never", "forever", or duration like "5m", "1h"
|
|
75
|
+
# Default: "5m" (5 minutes) - balances performance with freshness
|
|
76
|
+
self.cache_metadata_ttl_sec: float | None = self._parse_cache_duration(
|
|
77
|
+
os.getenv("FURU_CACHE_METADATA", "5m")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _parse_cache_duration(value: str) -> float | None:
|
|
82
|
+
"""Parse cache duration string into seconds. Returns None for 'never', float('inf') for 'forever'."""
|
|
83
|
+
value = value.strip().lower()
|
|
84
|
+
if value in {"never", "0", "false", "no"}:
|
|
85
|
+
return None # No caching
|
|
86
|
+
if value in {"forever", "inf", "true", "yes", "1"}:
|
|
87
|
+
return float("inf") # Cache forever
|
|
88
|
+
|
|
89
|
+
# Parse duration like "5m", "1h", "30s"
|
|
90
|
+
import re
|
|
91
|
+
|
|
92
|
+
match = re.match(r"^(\d+(?:\.\d+)?)\s*([smh]?)$", value)
|
|
93
|
+
if not match:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Invalid FURU_CACHE_METADATA value: {value!r}. "
|
|
96
|
+
"Use 'never', 'forever', or duration like '5m', '1h', '30s'"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
num = float(match.group(1))
|
|
100
|
+
unit = match.group(2) or "s"
|
|
101
|
+
multipliers = {"s": 1, "m": 60, "h": 3600}
|
|
102
|
+
return num * multipliers[unit]
|
|
103
|
+
|
|
104
|
+
def get_root(self, version_controlled: bool = False) -> Path:
|
|
105
|
+
"""Get root directory for storage (version_controlled uses its own root)."""
|
|
106
|
+
if version_controlled:
|
|
107
|
+
if self.version_controlled_root_override is not None:
|
|
108
|
+
return self.version_controlled_root_override
|
|
109
|
+
return self._resolve_version_controlled_root()
|
|
110
|
+
return self.base_root / "data"
|
|
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
|
+
|
|
165
|
+
@property
|
|
166
|
+
def raw_dir(self) -> Path:
|
|
167
|
+
return self.base_root / "raw"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
FURU_CONFIG = FuruConfig()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_furu_root(*, version_controlled: bool = False) -> Path:
|
|
174
|
+
return FURU_CONFIG.get_root(version_controlled=version_controlled)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def set_furu_root(path: Path) -> None:
|
|
178
|
+
FURU_CONFIG.base_root = path.resolve()
|