furu 0.0.2__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.
@@ -11,8 +11,8 @@
11
11
  href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap"
12
12
  rel="stylesheet"
13
13
  />
14
- <script type="module" crossorigin src="/assets/index-DDv_TYB_.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-CbdDfSOZ.css">
14
+ <script type="module" crossorigin src="/assets/index-DS3FsqcY.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BXAIKNNr.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
furu/errors.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import traceback
2
+ from collections.abc import Sequence
2
3
  from pathlib import Path
3
4
 
4
5
 
@@ -17,13 +18,25 @@ MISSING = _FuruMissing()
17
18
  class FuruError(Exception):
18
19
  """Base exception for Furu errors."""
19
20
 
20
- pass
21
+ def __init__(self, message: str, *, hints: Sequence[str] | None = None):
22
+ super().__init__(message)
23
+ self.hints = list(hints or [])
24
+
25
+ def _format_hints(self) -> str:
26
+ if not self.hints:
27
+ return ""
28
+ lines = ["", "Hints:"]
29
+ lines.extend([f" - {hint}" for hint in self.hints])
30
+ return "\n".join(lines)
21
31
 
22
32
 
23
33
  class FuruWaitTimeout(FuruError):
24
34
  """Raised when waiting for a result exceeds _max_wait_time_sec."""
25
35
 
26
- pass
36
+ def __str__(self) -> str:
37
+ msg = super().__str__()
38
+ msg += self._format_hints()
39
+ return msg
27
40
 
28
41
 
29
42
  class FuruLockNotAcquired(FuruError):
@@ -40,16 +53,45 @@ class FuruComputeError(FuruError):
40
53
  message: str,
41
54
  state_path: Path,
42
55
  original_error: Exception | None = None,
56
+ *,
57
+ recorded_error_type: str | None = None,
58
+ recorded_error_message: str | None = None,
59
+ recorded_traceback: str | None = None,
60
+ hints: Sequence[str] | None = None,
43
61
  ):
62
+ super().__init__(message, hints=hints)
44
63
  self.state_path = state_path
45
64
  self.original_error = original_error
46
- super().__init__(message)
65
+ self.recorded_error_type = recorded_error_type
66
+ self.recorded_error_message = recorded_error_message
67
+ self.recorded_traceback = recorded_traceback
47
68
 
48
69
  def __str__(self) -> str:
49
70
  msg = super().__str__() # ty: ignore[invalid-super-argument]
71
+ internal_dir = self.state_path.parent
72
+ furu_dir = internal_dir.parent
73
+ log_path = internal_dir / "furu.log"
74
+
75
+ msg += f"\n\nDirectory: {furu_dir}"
76
+ msg += f"\nState file: {self.state_path}"
77
+ msg += f"\nLog file: {log_path}"
78
+
79
+ if self.recorded_error_type or self.recorded_error_message:
80
+ msg += "\n\nRecorded error (from state.json):"
81
+ if self.recorded_error_type:
82
+ msg += f"\n Type: {self.recorded_error_type}"
83
+ if self.recorded_error_message:
84
+ msg += f"\n Message: {self.recorded_error_message}"
85
+
86
+ if self.recorded_traceback:
87
+ msg += f"\n\nRecorded traceback:\n{self.recorded_traceback}"
88
+
50
89
  if self.original_error:
51
90
  msg += f"\n\nOriginal error: {self.original_error}"
52
- if hasattr(self.original_error, "__traceback__"):
91
+ if (
92
+ hasattr(self.original_error, "__traceback__")
93
+ and self.original_error.__traceback__ is not None
94
+ ):
53
95
  tb = "".join(
54
96
  traceback.format_exception(
55
97
  type(self.original_error),
@@ -58,7 +100,7 @@ class FuruComputeError(FuruError):
58
100
  )
59
101
  )
60
102
  msg += f"\n\nTraceback:\n{tb}"
61
- msg += f"\n\nState file: {self.state_path}"
103
+ msg += self._format_hints()
62
104
  return msg
63
105
 
64
106
 
furu/migration.py CHANGED
@@ -507,8 +507,10 @@ def _apply_single_migration(
507
507
  event: dict[str, str | int] = {
508
508
  "type": "migrated",
509
509
  "policy": policy,
510
- "from": f"{candidate.from_ref.namespace}:{candidate.from_ref.furu_hash}",
511
- "to": f"{candidate.to_ref.namespace}:{candidate.to_ref.furu_hash}",
510
+ "from_namespace": candidate.from_ref.namespace,
511
+ "from_hash": candidate.from_ref.furu_hash,
512
+ "to_namespace": candidate.to_ref.namespace,
513
+ "to_hash": candidate.to_ref.furu_hash,
512
514
  }
513
515
  if default_values is not None:
514
516
  event["default_values"] = json.dumps(default_values, sort_keys=True)
@@ -519,8 +521,10 @@ def _apply_single_migration(
519
521
  overwrite_event = {
520
522
  "type": "migration_overwrite",
521
523
  "policy": policy,
522
- "from": f"{candidate.from_ref.namespace}:{candidate.from_ref.furu_hash}",
523
- "to": f"{candidate.to_ref.namespace}:{candidate.to_ref.furu_hash}",
524
+ "from_namespace": candidate.from_ref.namespace,
525
+ "from_hash": candidate.from_ref.furu_hash,
526
+ "to_namespace": candidate.to_ref.namespace,
527
+ "to_hash": candidate.to_ref.furu_hash,
524
528
  "reason": "force_overwrite",
525
529
  }
526
530
  StateManager.append_event(to_dir, overwrite_event)
@@ -6,9 +6,10 @@ import json
6
6
  import pathlib
7
7
  import textwrap
8
8
  from pathlib import Path
9
- from typing import Any
9
+ from typing import Any, Protocol, Sequence, cast, runtime_checkable
10
10
 
11
11
  import chz
12
+ from chz.util import MISSING as CHZ_MISSING, MISSING_TYPE
12
13
 
13
14
  from ..errors import _FuruMissing
14
15
  from pydantic import BaseModel as PydanticBaseModel
@@ -91,13 +92,34 @@ class FuruSerializer:
91
92
  def compute_hash(cls, obj: object, verbose: bool = False) -> str:
92
93
  """Compute deterministic hash of object."""
93
94
 
95
+ @runtime_checkable
96
+ class _DependencyHashProvider(Protocol):
97
+ def _dependency_hashes(self) -> Sequence[str]: ...
98
+
99
+ def _has_required_fields(
100
+ data_class: type[object],
101
+ data: dict[str, JsonValue],
102
+ ) -> bool:
103
+ if not chz.is_chz(data_class):
104
+ return False
105
+ for field in chz.chz_fields(data_class).values():
106
+ name = field.logical_name
107
+ if name in data:
108
+ continue
109
+ if field._default is not CHZ_MISSING:
110
+ continue
111
+ if not isinstance(field._default_factory, MISSING_TYPE):
112
+ continue
113
+ return False
114
+ return True
115
+
94
116
  def canonicalize(item: object) -> JsonValue:
95
117
  if isinstance(item, _FuruMissing):
96
118
  raise ValueError("Cannot hash Furu.MISSING")
97
119
 
98
120
  if chz.is_chz(item):
99
121
  fields = chz.chz_fields(item)
100
- return {
122
+ result = {
101
123
  "__class__": cls.get_classname(item),
102
124
  **{
103
125
  name: canonicalize(getattr(item, name))
@@ -105,8 +127,24 @@ class FuruSerializer:
105
127
  if not name.startswith("_")
106
128
  },
107
129
  }
130
+ if isinstance(item, _DependencyHashProvider):
131
+ dependency_hashes = list(item._dependency_hashes())
132
+ if dependency_hashes:
133
+ result["__dependencies__"] = dependency_hashes
134
+ return result
108
135
 
109
136
  if isinstance(item, dict):
137
+ if cls.CLASS_MARKER in item:
138
+ config = cast(dict[str, JsonValue], item)
139
+ module_path, _, class_name = item[cls.CLASS_MARKER].rpartition(".")
140
+ module = importlib.import_module(module_path)
141
+ data_class = getattr(module, class_name, None)
142
+ if (
143
+ data_class is not None
144
+ and hasattr(data_class, "_dependency_hashes")
145
+ and _has_required_fields(data_class, config)
146
+ ):
147
+ return canonicalize(cls.from_dict(config))
110
148
  filtered = item
111
149
  if cls.CLASS_MARKER in item:
112
150
  filtered = {
furu/storage/metadata.py CHANGED
@@ -124,7 +124,7 @@ class MetadataManager:
124
124
  try:
125
125
  head = cls.run_git_command(["rev-parse", "HEAD"])
126
126
  branch = cls.run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
127
- except subprocess.CalledProcessError:
127
+ except (subprocess.CalledProcessError, FileNotFoundError):
128
128
  return GitInfo(
129
129
  git_commit="<no-git>",
130
130
  git_branch="<no-git>",
@@ -133,15 +133,27 @@ class MetadataManager:
133
133
  git_submodules={},
134
134
  )
135
135
  else:
136
- head = cls.run_git_command(["rev-parse", "HEAD"])
137
- branch = cls.run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
136
+ try:
137
+ head = cls.run_git_command(["rev-parse", "HEAD"])
138
+ branch = cls.run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
139
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
140
+ raise RuntimeError(
141
+ "Failed to read git commit/branch for provenance. "
142
+ "If this is expected, set FURU_REQUIRE_GIT=0."
143
+ ) from e
138
144
 
139
145
  if FURU_CONFIG.require_git_remote:
140
- remote = cls.run_git_command(["remote", "get-url", "origin"])
146
+ try:
147
+ remote = cls.run_git_command(["remote", "get-url", "origin"])
148
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
149
+ raise RuntimeError(
150
+ "Git remote 'origin' is required for provenance but was not found. "
151
+ "Set FURU_REQUIRE_GIT_REMOTE=0 to allow missing origin."
152
+ ) from e
141
153
  else:
142
154
  try:
143
155
  remote = cls.run_git_command(["remote", "get-url", "origin"])
144
- except subprocess.CalledProcessError:
156
+ except (subprocess.CalledProcessError, FileNotFoundError):
145
157
  remote = None
146
158
 
147
159
  if ignore_diff:
furu/storage/state.py CHANGED
@@ -977,6 +977,7 @@ def compute_lock(
977
977
  poll_interval_sec: float = 10.0,
978
978
  wait_log_every_sec: float = 10.0,
979
979
  reconcile_fn: Callable[[Path], None] | None = None,
980
+ allow_failed: bool = False,
980
981
  ) -> Generator[ComputeLockContext, None, None]:
981
982
  """
982
983
  Context manager that atomically acquires lock + records attempt + starts heartbeat.
@@ -1000,6 +1001,7 @@ def compute_lock(
1000
1001
  poll_interval_sec: Interval between lock acquisition attempts
1001
1002
  wait_log_every_sec: Interval between "waiting for lock" log messages
1002
1003
  reconcile_fn: Optional function to call to reconcile stale attempts
1004
+ allow_failed: Allow recomputation even if state is failed
1003
1005
 
1004
1006
  Yields:
1005
1007
  ComputeLockContext with attempt_id and stop_heartbeat callable
@@ -1008,6 +1010,7 @@ def compute_lock(
1008
1010
  FuruLockNotAcquired: If lock cannot be acquired (after waiting)
1009
1011
  FuruWaitTimeout: If max_wait_time_sec is exceeded
1010
1012
  """
1013
+
1011
1014
  def _format_wait_duration(seconds: float) -> str:
1012
1015
  if seconds < 60.0:
1013
1016
  return f"{seconds:.1f}s"
@@ -1020,6 +1023,21 @@ def compute_lock(
1020
1023
  days = hours / 24.0
1021
1024
  return f"{days:.1f}d"
1022
1025
 
1026
+ def _format_owner(attempt: _StateAttempt) -> str:
1027
+ owner = attempt.owner
1028
+ parts: list[str] = []
1029
+ if attempt.id:
1030
+ parts.append(f"attempt {attempt.id}")
1031
+ if owner.host:
1032
+ parts.append(f"host {owner.host}")
1033
+ if owner.pid is not None:
1034
+ parts.append(f"pid {owner.pid}")
1035
+ if owner.user:
1036
+ parts.append(f"user {owner.user}")
1037
+ if not parts:
1038
+ return "owner unknown"
1039
+ return ", ".join(parts)
1040
+
1023
1041
  def _describe_wait(attempt: _StateAttempt, waited_sec: float) -> str:
1024
1042
  label = "last heartbeat"
1025
1043
  timestamp = attempt.heartbeat_at
@@ -1034,7 +1052,7 @@ def compute_lock(
1034
1052
  return (
1035
1053
  "waited "
1036
1054
  f"{_format_wait_duration(waited_sec)}, {label} {timestamp_info}, "
1037
- f"status {attempt.status}, backend {attempt.backend}"
1055
+ f"status {attempt.status}, backend {attempt.backend}, {_format_owner(attempt)}"
1038
1056
  )
1039
1057
 
1040
1058
  lock_path = StateManager.get_lock_path(directory, StateManager.COMPUTE_LOCK)
@@ -1054,8 +1072,26 @@ def compute_lock(
1054
1072
  if max_wait_time_sec is not None:
1055
1073
  elapsed = time.time() - start_time
1056
1074
  if elapsed > max_wait_time_sec:
1075
+ state = StateManager.read_state(directory)
1076
+ attempt = state.attempt
1077
+ attempt_info = "no active attempt"
1078
+ if isinstance(attempt, (_StateAttemptQueued, _StateAttemptRunning)):
1079
+ attempt_info = _describe_wait(attempt, elapsed)
1080
+ message = (
1081
+ f"Timed out waiting for compute lock after {elapsed:.1f}s."
1082
+ f"\nDirectory: {directory}"
1083
+ f"\nLock file: {lock_path}"
1084
+ f"\nDetails: {attempt_info}"
1085
+ )
1057
1086
  raise FuruWaitTimeout(
1058
- f"Timed out waiting for compute lock after {elapsed:.1f}s"
1087
+ message,
1088
+ hints=[
1089
+ "Increase max wait: set FURU_MAX_WAIT_SECS (or override Furu._max_wait_time_sec).",
1090
+ "Change poll cadence: set FURU_POLL_INTERVAL_SECS.",
1091
+ "Change wait logging cadence: set FURU_WAIT_LOG_EVERY_SECS.",
1092
+ "If locks look stale too quickly/slowly: tune FURU_LEASE_SECS and FURU_HEARTBEAT_SECS.",
1093
+ "For more logs: set FURU_LOG_LEVEL=DEBUG.",
1094
+ ],
1059
1095
  )
1060
1096
 
1061
1097
  lock_fd = StateManager.try_lock(lock_path)
@@ -1066,9 +1102,11 @@ def compute_lock(
1066
1102
  raise FuruLockNotAcquired(
1067
1103
  "Cannot acquire lock: experiment already succeeded"
1068
1104
  )
1069
- if isinstance(state.result, _StateResultFailed):
1105
+ if isinstance(state.result, _StateResultFailed) and not allow_failed:
1070
1106
  StateManager.release_lock(lock_fd, lock_path)
1071
- raise FuruLockNotAcquired("Cannot acquire lock: experiment already failed")
1107
+ raise FuruLockNotAcquired(
1108
+ "Cannot acquire lock: experiment already failed"
1109
+ )
1072
1110
  attempt = state.attempt
1073
1111
  if (
1074
1112
  isinstance(attempt, (_StateAttemptQueued, _StateAttemptRunning))
@@ -1083,7 +1121,7 @@ def compute_lock(
1083
1121
  raise FuruLockNotAcquired(
1084
1122
  "Cannot acquire lock: experiment already succeeded"
1085
1123
  )
1086
- if isinstance(state.result, _StateResultFailed):
1124
+ if isinstance(state.result, _StateResultFailed) and not allow_failed:
1087
1125
  raise FuruLockNotAcquired(
1088
1126
  "Cannot acquire lock: experiment already failed"
1089
1127
  )
@@ -1117,7 +1155,7 @@ def compute_lock(
1117
1155
  raise FuruLockNotAcquired(
1118
1156
  "Cannot acquire lock: experiment already succeeded"
1119
1157
  )
1120
- if isinstance(state.result, _StateResultFailed):
1158
+ if isinstance(state.result, _StateResultFailed) and not allow_failed:
1121
1159
  raise FuruLockNotAcquired("Cannot acquire lock: experiment already failed")
1122
1160
 
1123
1161
  # If no active attempt but lock exists, it's orphaned - clean it up
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: furu
3
- Version: 0.0.2
3
+ Version: 0.0.3
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>
@@ -336,6 +336,17 @@ except FuruLockNotAcquired:
336
336
  print("Could not acquire lock")
337
337
  ```
338
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
+
339
350
  ## Submitit Integration
340
351
 
341
352
  Run computations on SLURM clusters via [submitit](https://github.com/facebookincubator/submitit):
@@ -415,7 +426,9 @@ The `/api/experiments` endpoint supports:
415
426
  | `FURU_LOG_LEVEL` | `INFO` | Console verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
416
427
  | `FURU_IGNORE_DIFF` | `false` | Skip embedding git diff in metadata |
417
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) |
418
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) |
419
432
  | `FURU_WAIT_LOG_EVERY_SECS` | `10` | Interval between "waiting" log messages |
420
433
  | `FURU_STALE_AFTER_SECS` | `1800` | Consider running jobs stale after this duration |
421
434
  | `FURU_LEASE_SECS` | `120` | Compute lock lease duration |
@@ -1,36 +1,36 @@
1
- furu/__init__.py,sha256=fhSViHOJ9W-64swuaBFdZOfq0ZMuSj6LSiX2ZfcjhD8,1736
1
+ furu/__init__.py,sha256=c0rtDRCWRafo0gB4x7qOMVL8ZXtxHOrPnJIs_CwrWlY,1818
2
2
  furu/adapters/__init__.py,sha256=onLzEj9hccPK15g8a8va2T19nqQXoxb9rQlJIjKSKnE,69
3
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
4
+ furu/config.py,sha256=UvSkUDNh0iuMKyl0OelKO5i7FAdkHnqnfbTFXaIaXvY,6886
5
+ furu/core/__init__.py,sha256=6hH7i6r627c0FZn6eQVsSG7LD4QmTta6iQw0AiPQPTM,156
6
+ furu/core/furu.py,sha256=Uz5vVo161Duvl94hwn7u2WH9MaDFQFqlxowzHGigkkY,51592
7
7
  furu/core/list.py,sha256=hwwlvqaKB1grPBGKXc15scF1RCqDvWc0AoDbhKlN4W0,3625
8
8
  furu/dashboard/__init__.py,sha256=zNVddterfpjQtcpihIl3TRJdgdjOHYR0uO0cOSaGABg,172
9
9
  furu/dashboard/__main__.py,sha256=cNs65IMl4kwZFpxa9xLXmFSy4-M5D1X1ZBfTDxW11vo,144
10
10
  furu/dashboard/api/__init__.py,sha256=9-WyWOt-VQJJBIsdW29D-7JvR-BivJd9G_SRaRptCz0,80
11
11
  furu/dashboard/api/models.py,sha256=SCu-kLJyW7dwSKswdgQNS3wQuj25ORs0pHkvX9xBbo4,4767
12
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
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
15
  furu/dashboard/frontend/dist/favicon.svg,sha256=3TSLHNZITFe3JTPoYHZnDgiGsJxIzf39v97l2A1Hodo,369
16
- furu/dashboard/frontend/dist/index.html,sha256=o3XhvegC9rBpUiWNfXdCHqf_tg2795nob1NI0nBpFS4,810
16
+ furu/dashboard/frontend/dist/index.html,sha256=d9a8ZFKZ5uDtN3urqVNmS8LWMBhOC0eW7X0noT0RcYQ,810
17
17
  furu/dashboard/main.py,sha256=8JYc79gbJ9MjvIRdGDuAcR2Mme9kyY4ryZb11ZZ4uVA,4069
18
18
  furu/dashboard/scanner.py,sha256=qXCvkvFByBc09TUdth5Js67rS8zpRBlRkVQ9dJ7YbdE,34696
19
- furu/errors.py,sha256=d1Kp5O9cVoQwXmQeZC-35u7xldw_c3ryYXrbVfv-Lws,2001
19
+ furu/errors.py,sha256=tWKLOtkP5uYDuqozeImCN7WzjFforPj1WImW0AWc4Vk,3684
20
20
  furu/migrate.py,sha256=x_Uh7oXAv40L5ZAHJhdnw-o7ct56rWUSZLbHHfRObeY,1313
21
- furu/migration.py,sha256=A91dng1XRn1N_xJrmBhh-OvU22GlseqOh6PmVhNZh3w,31307
21
+ furu/migration.py,sha256=R2-tARMx4VKryiqJ7WHia_dPVxRbTqofPpCFVE9zQ8U,31411
22
22
  furu/runtime/__init__.py,sha256=fQqE7wUuWunLD73Vm3lss7BFSij3UVxXOKQXBAOS8zw,504
23
23
  furu/runtime/env.py,sha256=o1phhoTDhOnhALr3Ozf1ldrdvk2ClyEvBWbebHM6BXg,160
24
24
  furu/runtime/logging.py,sha256=JkuTFtbv6dYk088P6_Bga46bnKSDt-ElAqmiY86hMys,9773
25
25
  furu/runtime/tracebacks.py,sha256=PGCuOq8QkWSoun791gjUXM8frOP2wWV8IBlqaA4nuGE,1631
26
26
  furu/serialization/__init__.py,sha256=L7oHuIbxdSh7GCY3thMQnDwlt_ERH-TMy0YKEAZLrPs,341
27
27
  furu/serialization/migrations.py,sha256=HD5g8JCBdH3Y0rHJYc4Ug1IXBVcUDxLE7nfiXZnXcUE,7772
28
- furu/serialization/serializer.py,sha256=THWqHzpSwXj3Nj3PZ3JhwlWJ8sgvVyGrwBEDB_EWuAE,8355
28
+ furu/serialization/serializer.py,sha256=_nfUaAOy_KHegvfXlpPh4rCuvkzalJva75OvDg5nXiI,10114
29
29
  furu/storage/__init__.py,sha256=cLLL-GPpSu9C72Mdk5S6TGu3g-SnBfEuxzfpx5ZJPtw,616
30
- furu/storage/metadata.py,sha256=u4F4V1dDZtsiniO5xDCy8YxJZxGnreriYnJ1fOvQ2Bg,9232
30
+ furu/storage/metadata.py,sha256=MH6w5hs-2rwHD6G9erMPM5pE3hm0h5Pk_G3Z6eyyGB0,9899
31
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,,
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,,
File without changes