furu 0.0.3__py3-none-any.whl → 0.0.5__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.3
3
+ Version: 0.0.5
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,8 +391,8 @@ except FuruLockNotAcquired:
336
391
  print("Could not acquire lock")
337
392
  ```
338
393
 
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.
394
+ By default, failed artifacts are retried on the next `get()` call. Set
395
+ `FURU_RETRY_FAILED=0` to keep failures sticky.
341
396
 
342
397
  `FURU_MAX_WAIT_SECS` overrides the per-class `_max_wait_time_sec` (default 600s)
343
398
  timeout used when waiting for compute locks before raising `FuruWaitTimeout`.
@@ -349,27 +404,8 @@ and `furu.log`.
349
404
 
350
405
  ## Submitit Integration
351
406
 
352
- Run computations on SLURM clusters via [submitit](https://github.com/facebookincubator/submitit):
353
-
354
- ```python
355
- import submitit
356
- import furu
357
-
358
- executor = submitit.AutoExecutor(folder="submitit_logs")
359
- executor.update_parameters(
360
- timeout_min=60,
361
- slurm_partition="gpu",
362
- gpus_per_node=1,
363
- )
364
-
365
- # Submit job and return immediately
366
- job = my_furu_obj.load_or_create(executor=executor)
367
-
368
- # Job ID is tracked in .furu/state.json
369
- print(job.job_id)
370
- ```
371
-
372
- 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.
373
409
 
374
410
  ## Dashboard
375
411
 
@@ -423,21 +459,34 @@ The `/api/experiments` endpoint supports:
423
459
  |----------|---------|-------------|
424
460
  | `FURU_PATH` | `<project>/furu-data` | Base storage directory for non-versioned artifacts |
425
461
  | `FURU_VERSION_CONTROLLED_PATH` | `<project>/furu-data/artifacts` | Override version-controlled storage root |
462
+ | `FURU_SUBMITIT_PATH` | `<FURU_PATH>/submitit` | Override submitit logs root |
426
463
  | `FURU_LOG_LEVEL` | `INFO` | Console verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
427
- | `FURU_IGNORE_DIFF` | `false` | Skip embedding git diff in metadata |
464
+ | `FURU_RICH_UNCAUGHT_TRACEBACKS` | `true` | Use Rich for exception formatting (set `0` to disable) |
465
+ | `FURU_RECORD_GIT` | `cached` | Git provenance capture: `ignore` skips git metadata, `cached` records once per process, `uncached` records every time |
466
+ | `FURU_ALLOW_NO_GIT_ORIGIN` | `false` | Allow missing git `origin` when recording git metadata (invalid with `FURU_RECORD_GIT=ignore`) |
428
467
  | `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
468
  | `FURU_RETRY_FAILED` | `true` | Retry failed artifacts by default (set to `0` to keep failures sticky) |
469
+ | `FURU_MAX_COMPUTE_RETRIES` | `3` | Maximum compute retries per node after the first failure |
430
470
  | `FURU_POLL_INTERVAL_SECS` | `10` | Polling interval for queued/running jobs |
431
471
  | `FURU_MAX_WAIT_SECS` | unset | Override wait timeout (falls back to `_max_wait_time_sec`, default 600s) |
432
472
  | `FURU_WAIT_LOG_EVERY_SECS` | `10` | Interval between "waiting" log messages |
433
473
  | `FURU_STALE_AFTER_SECS` | `1800` | Consider running jobs stale after this duration |
434
474
  | `FURU_LEASE_SECS` | `120` | Compute lock lease duration |
435
- | `FURU_HEARTBEAT_SECS` | `lease/3` | Heartbeat interval for running jobs |
475
+ | `FURU_HEARTBEAT_SECS` | `lease/3` | Heartbeat interval for running jobs (min 1s) |
436
476
  | `FURU_PREEMPT_MAX` | `5` | Maximum submitit requeues on preemption |
437
477
  | `FURU_CANCELLED_IS_PREEMPTED` | `false` | Treat SLURM CANCELLED as preempted |
438
- | `FURU_RICH_UNCAUGHT_TRACEBACKS` | `true` | Use Rich for exception formatting |
478
+ | `SLURM_JOB_ID` | unset | Read-only; set by Slurm to record job id and enable submitit context |
439
479
 
440
- Local `.env` files are loaded automatically if `python-dotenv` is installed.
480
+ Local `.env` files are not loaded automatically. Call `furu.load_env()` when you
481
+ want to load `.env` values (requires `python-dotenv`).
482
+
483
+ ### Test and CI Environment Variables
484
+
485
+ | Variable | Default | Description |
486
+ |----------|---------|-------------|
487
+ | `FURU_DASHBOARD_DEV_DATA_DIR` | unset | Override data dir for `make dashboard-dev` (defaults to a temp dir) |
488
+ | `FURU_E2E_DATA_DIR` | unset | Required for Playwright e2e runs; used as the data root and to set `FURU_PATH` |
489
+ | `CI` | unset | Enables CI-friendly Playwright settings (retries, single worker, traces, screenshots, video) |
441
490
 
442
491
  ### Programmatic Configuration
443
492
 
@@ -450,7 +499,7 @@ furu.set_furu_root(Path("/my/storage"))
450
499
  root = furu.get_furu_root()
451
500
 
452
501
  # Access config directly
453
- furu.FURU_CONFIG.ignore_git_diff = True
502
+ furu.FURU_CONFIG.record_git = "uncached"
454
503
  furu.FURU_CONFIG.poll_interval = 5.0
455
504
  ```
456
505
 
@@ -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=UGnH8QAKMUgrGMGNkfBgLXideXEpDlozUSsX9iNN8Lw,6844
5
+ furu/core/__init__.py,sha256=6hH7i6r627c0FZn6eQVsSG7LD4QmTta6iQw0AiPQPTM,156
6
+ furu/core/furu.py,sha256=tGUtHVAgSV_oKeW5hlSH5h6OvZG1h4BDBltpjFyJByQ,61375
7
+ furu/core/list.py,sha256=QaGSh8NFg1K2WFncM8duOYQ6KLZ6EW2pRLArN_e5Juw,3662
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-BjyrY-Zz.js,sha256=fItsQ--Dzobq5KdUcuqDi4txM2-NNqx8JET5Lwkwf7U,544515
15
+ furu/dashboard/frontend/dist/favicon.svg,sha256=3TSLHNZITFe3JTPoYHZnDgiGsJxIzf39v97l2A1Hodo,369
16
+ furu/dashboard/frontend/dist/index.html,sha256=Ig-j0qgTXBSge0GN7PaM7mcLnuRhRMQmkTZjU1wmTXY,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=SXUH9PfcCAeHSZYrTP1YNjl2fV7vqzpZXZQzmtIcVMg,7137
23
+ furu/execution/paths.py,sha256=0MfQk5Kh7bxvJiWvG40TJe7RF5Q5Na6uvi6qV0OT3Vc,460
24
+ furu/execution/plan.py,sha256=bEnzFlBVN3vGKb_0a03sOEtJYRkLSJkIaNzGnxjZTo4,9928
25
+ furu/execution/plan_utils.py,sha256=TAQqlPeJfOdH2MT-X7g3j1Se_0e4oKvG0tJaWC1kM40,381
26
+ furu/execution/slurm_dag.py,sha256=xh9EUGdPZaAH3UfcRqo6MsKYBIV-UW3_7owY8kLOwz4,9392
27
+ furu/execution/slurm_pool.py,sha256=ft76Gp-HgFWWjGvDclUChLOjY1rvhhfkP5mxhK3ViQk,30395
28
+ furu/execution/slurm_spec.py,sha256=DG8BF4FCga2ZXsqGUvfNibk6II40JcShVZ4jTwxTdec,977
29
+ furu/execution/submitit_factory.py,sha256=B2vkDtmscuAX0sBaj9V5pNlgOtkkV35yJ1fZ7A-DSvU,1119
30
+ furu/migrate.py,sha256=x_Uh7oXAv40L5ZAHJhdnw-o7ct56rWUSZLbHHfRObeY,1313
31
+ furu/migration.py,sha256=EYWULuH8lEVvESthO2qEF95WJTo1Uj6d4L6VU2zmWpw,31350
32
+ furu/runtime/__init__.py,sha256=fQqE7wUuWunLD73Vm3lss7BFSij3UVxXOKQXBAOS8zw,504
33
+ furu/runtime/env.py,sha256=lb-LWl-1EM_CP8sy0z3HAY20NXQ-v3QdOgte1i0HYVA,214
34
+ furu/runtime/logging.py,sha256=Xni1hWyH21bKc6D2owBZzThsj6q8yQOBD9zUrDS4jtI,10760
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=fJ_0G0vWRl9vNb7IigjXd__aokTok2ZHowmttoXjTsM,9581
41
+ furu/storage/migration.py,sha256=FNExLdPu1ekKZR2XJkAgags9U8pV2FfkKAECSXkSra8,2585
42
+ furu/storage/state.py,sha256=kcIfAwdKWT8Q2ElbC5qofQC6noS_k6eNSPkNAdYXoaY,43707
43
+ furu-0.0.5.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
44
+ furu-0.0.5.dist-info/entry_points.txt,sha256=hZkjtFzNlb33Zk-aUfLMRj-XgVDxdT82-JXG9d4bu2E,60
45
+ furu-0.0.5.dist-info/METADATA,sha256=1ugdjmF6ECLzzO21v123_GdyMmSb6tni6avp6YJRbG4,17101
46
+ furu-0.0.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.27
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,36 +0,0 @@
1
- furu/__init__.py,sha256=c0rtDRCWRafo0gB4x7qOMVL8ZXtxHOrPnJIs_CwrWlY,1818
2
- furu/adapters/__init__.py,sha256=onLzEj9hccPK15g8a8va2T19nqQXoxb9rQlJIjKSKnE,69
3
- furu/adapters/submitit.py,sha256=OuCP0pEkO1kI4WLcSUvMqXwVCCy-8uwUE7v1qvkLZnU,6214
4
- furu/config.py,sha256=UvSkUDNh0iuMKyl0OelKO5i7FAdkHnqnfbTFXaIaXvY,6886
5
- furu/core/__init__.py,sha256=6hH7i6r627c0FZn6eQVsSG7LD4QmTta6iQw0AiPQPTM,156
6
- furu/core/furu.py,sha256=Uz5vVo161Duvl94hwn7u2WH9MaDFQFqlxowzHGigkkY,51592
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-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=8JYc79gbJ9MjvIRdGDuAcR2Mme9kyY4ryZb11ZZ4uVA,4069
18
- furu/dashboard/scanner.py,sha256=qXCvkvFByBc09TUdth5Js67rS8zpRBlRkVQ9dJ7YbdE,34696
19
- furu/errors.py,sha256=tWKLOtkP5uYDuqozeImCN7WzjFforPj1WImW0AWc4Vk,3684
20
- furu/migrate.py,sha256=x_Uh7oXAv40L5ZAHJhdnw-o7ct56rWUSZLbHHfRObeY,1313
21
- furu/migration.py,sha256=R2-tARMx4VKryiqJ7WHia_dPVxRbTqofPpCFVE9zQ8U,31411
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=_nfUaAOy_KHegvfXlpPh4rCuvkzalJva75OvDg5nXiI,10114
29
- furu/storage/__init__.py,sha256=cLLL-GPpSu9C72Mdk5S6TGu3g-SnBfEuxzfpx5ZJPtw,616
30
- furu/storage/metadata.py,sha256=MH6w5hs-2rwHD6G9erMPM5pE3hm0h5Pk_G3Z6eyyGB0,9899
31
- furu/storage/migration.py,sha256=Ars9aYwvhXpIBDf6L9ojGjp_l656-RfdtEAFKN0sZZY,2640
32
- furu/storage/state.py,sha256=rAzR0XJS3OvwGMATlppxNQwX1FrSIffUTkptSwOjBcs,42627
33
- furu-0.0.3.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
34
- furu-0.0.3.dist-info/entry_points.txt,sha256=hZkjtFzNlb33Zk-aUfLMRj-XgVDxdT82-JXG9d4bu2E,60
35
- furu-0.0.3.dist-info/METADATA,sha256=NY6H_CMvm2-wc21GdRpMWxa5cK4HMxMwylTDVaZy2aY,14615
36
- furu-0.0.3.dist-info/RECORD,,