kvgit 0.4.2__tar.gz → 0.4.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.
- {kvgit-0.4.2 → kvgit-0.4.3}/PKG-INFO +4 -2
- {kvgit-0.4.2 → kvgit-0.4.3}/README.md +3 -1
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/__init__.py +2 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/_codec.py +29 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/_open.py +10 -3
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/cache.py +86 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/errors.py +22 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/refs.py +17 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/repo.py +17 -1
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/versioned/base.py +207 -44
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/versioned/kv.py +163 -60
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/worktree.py +50 -15
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit.egg-info/PKG-INFO +4 -2
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit.egg-info/SOURCES.txt +2 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/pyproject.toml +1 -1
- kvgit-0.4.3/tests/test_commit_on_moved.py +190 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_repo.py +151 -0
- kvgit-0.4.3/tests/test_value_cache.py +222 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_worktree.py +29 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/LICENSE +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/codecs/__init__.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/codecs/_hash.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/codecs/base.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/codecs/numpy.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/codecs/pandas.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/codecs/pickler.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/content_types.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/encoding.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/hamt.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/kv/__init__.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/kv/base.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/kv/composite.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/kv/disk.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/kv/indexeddb.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/kv/memory.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/kv/postgres.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/merges.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/namespaced.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/py.typed +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/versioned/__init__.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/versioned/helpers.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/versioned/keyset.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/versioned/merge.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit/versioned/protocol.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit.egg-info/dependency_links.txt +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit.egg-info/requires.txt +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/kvgit.egg-info/top_level.txt +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/setup.cfg +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_cache.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_content_types.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_delete_branches.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_hamt.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_merge_policy.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_merges.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_namespaced.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_open.py +0 -0
- {kvgit-0.4.2 → kvgit-0.4.3}/tests/test_storage_v4.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kvgit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Versioned key-value store with git-like commit, branch, and merge semantics.
|
|
5
5
|
Author: ashenfad
|
|
6
6
|
License: MIT
|
|
@@ -144,7 +144,9 @@ with Repo(Disk("/tmp/kvgit-demo")) as on_disk:
|
|
|
144
144
|
| `git gc [--prune=now]` | `repo.gc([min_age=0])` |
|
|
145
145
|
|
|
146
146
|
Where it differs: there is no index; several worktrees can hold one
|
|
147
|
-
branch, and their commits race and merge rather than being refused
|
|
147
|
+
branch, and their commits race and merge rather than being refused
|
|
148
|
+
(`commit(on_moved="raise")` refuses instead, raising
|
|
149
|
+
`BranchMovedError`, for data whose keys aren't independent); a
|
|
148
150
|
conflicted merge raises `MergeConflict` and changes nothing (unless a
|
|
149
151
|
merge function such as `text_merge` writes markers into the value);
|
|
150
152
|
commit hashes are not git's, and there are no remotes.
|
|
@@ -90,7 +90,9 @@ with Repo(Disk("/tmp/kvgit-demo")) as on_disk:
|
|
|
90
90
|
| `git gc [--prune=now]` | `repo.gc([min_age=0])` |
|
|
91
91
|
|
|
92
92
|
Where it differs: there is no index; several worktrees can hold one
|
|
93
|
-
branch, and their commits race and merge rather than being refused
|
|
93
|
+
branch, and their commits race and merge rather than being refused
|
|
94
|
+
(`commit(on_moved="raise")` refuses instead, raising
|
|
95
|
+
`BranchMovedError`, for data whose keys aren't independent); a
|
|
94
96
|
conflicted merge raises `MergeConflict` and changes nothing (unless a
|
|
95
97
|
merge function such as `text_merge` writes markers into the value);
|
|
96
98
|
commit hashes are not git's, and there are no remotes.
|
|
@@ -4,6 +4,7 @@ from ._open import open
|
|
|
4
4
|
from .content_types import MergeFn, counter, last_writer_wins, text_merge
|
|
5
5
|
from .errors import (
|
|
6
6
|
BranchExistsError,
|
|
7
|
+
BranchMovedError,
|
|
7
8
|
ConcurrencyError,
|
|
8
9
|
CorruptHeadError,
|
|
9
10
|
GcBusy,
|
|
@@ -40,6 +41,7 @@ from .worktree import Status, Worktree
|
|
|
40
41
|
__all__ = [
|
|
41
42
|
"ROOT_COMMIT",
|
|
42
43
|
"BranchExistsError",
|
|
44
|
+
"BranchMovedError",
|
|
43
45
|
"Branches",
|
|
44
46
|
"CantMark",
|
|
45
47
|
"Commit",
|
|
@@ -61,6 +61,27 @@ class _ChunkReader:
|
|
|
61
61
|
return None
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
class _CountingReader:
|
|
65
|
+
"""A chunk reader that tallies the bytes of the chunks it returns."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, reader: _ChunkReader) -> None:
|
|
68
|
+
self._reader = reader
|
|
69
|
+
self.nbytes = 0
|
|
70
|
+
|
|
71
|
+
def get(self, ref: str) -> bytes:
|
|
72
|
+
raw = self._reader.get(ref)
|
|
73
|
+
self.nbytes += len(raw)
|
|
74
|
+
return raw
|
|
75
|
+
|
|
76
|
+
def get_many(self, refs):
|
|
77
|
+
found = self._reader.get_many(refs)
|
|
78
|
+
self.nbytes += sum(len(v) for v in found.values())
|
|
79
|
+
return found
|
|
80
|
+
|
|
81
|
+
def prefetch(self, refs) -> None:
|
|
82
|
+
self._reader.prefetch(refs)
|
|
83
|
+
|
|
84
|
+
|
|
64
85
|
def _is_chunk_aware(fn) -> bool:
|
|
65
86
|
"""Whether an encoder/decoder takes a required sink/reader argument.
|
|
66
87
|
|
|
@@ -148,6 +169,14 @@ class Codec:
|
|
|
148
169
|
return self.decoder(raw, self._reader)
|
|
149
170
|
return self.decoder(raw)
|
|
150
171
|
|
|
172
|
+
def decode_sized(self, raw: bytes) -> tuple[Any, int]:
|
|
173
|
+
"""A value, and the bytes it was decoded from: its blob plus any
|
|
174
|
+
chunks a chunked decoder read, which the value usually keeps."""
|
|
175
|
+
if self.decoder_chunked:
|
|
176
|
+
reader = _CountingReader(self._reader) # type: ignore[arg-type]
|
|
177
|
+
return self.decoder(raw, reader), len(raw) + reader.nbytes
|
|
178
|
+
return self.decoder(raw), len(raw)
|
|
179
|
+
|
|
151
180
|
def encode_merged(self, value: Any) -> bytes:
|
|
152
181
|
"""Encode a value a merge function produced.
|
|
153
182
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
5
|
from ._codec import CodecSpec
|
|
6
|
-
from .cache import DEFAULT_CACHE_BYTES
|
|
6
|
+
from .cache import DEFAULT_CACHE_BYTES, DEFAULT_VALUE_CACHE_BYTES
|
|
7
7
|
from .kv.base import KVStore
|
|
8
8
|
from .kv.memory import Memory
|
|
9
9
|
from .repo import Repo
|
|
@@ -41,6 +41,7 @@ def open(
|
|
|
41
41
|
branch: str = "main",
|
|
42
42
|
codec: CodecSpec = "pickle",
|
|
43
43
|
cache_bytes: int = DEFAULT_CACHE_BYTES,
|
|
44
|
+
value_cache_bytes: int = DEFAULT_VALUE_CACHE_BYTES,
|
|
44
45
|
) -> Worktree:
|
|
45
46
|
"""Open (or create) a store, and a worktree on ``branch`` of it.
|
|
46
47
|
|
|
@@ -61,8 +62,14 @@ def open(
|
|
|
61
62
|
codec: How values are stored; see :class:`Repo`.
|
|
62
63
|
cache_bytes: Memory for what the store never changes; see
|
|
63
64
|
:class:`Repo`.
|
|
65
|
+
value_cache_bytes: Memory for the values the worktree reads;
|
|
66
|
+
see :class:`Repo`.
|
|
64
67
|
"""
|
|
65
68
|
backend = _make_backend(kind, path=path, db_name=db_name)
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
repo = Repo(
|
|
70
|
+
backend,
|
|
71
|
+
codec=codec,
|
|
72
|
+
cache_bytes=cache_bytes,
|
|
73
|
+
value_cache_bytes=value_cache_bytes,
|
|
68
74
|
)
|
|
75
|
+
return repo.worktree(branch, create=True)
|
|
@@ -19,6 +19,10 @@ commit exists — what ``snapshot(commit=)`` and a tag's ``dangling``
|
|
|
19
19
|
ask — so it is always read, and whatever is reached through a root the
|
|
20
20
|
store still holds is safe to answer from here. Absence is never cached
|
|
21
21
|
either: a key missing now may be written by the next commit.
|
|
22
|
+
|
|
23
|
+
A worktree keeps a second, separate memory: the decoded values it has
|
|
24
|
+
read at its head (:class:`ValueCache`). That one holds blobs, so it is
|
|
25
|
+
per worktree, emptied whenever the head moves, and bounded in bytes.
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
28
|
from __future__ import annotations
|
|
@@ -41,6 +45,9 @@ from .versioned.kv import (
|
|
|
41
45
|
DEFAULT_CACHE_BYTES = 32 * 1024 * 1024
|
|
42
46
|
"""The default budget: tens of thousands of tree nodes."""
|
|
43
47
|
|
|
48
|
+
DEFAULT_VALUE_CACHE_BYTES = 32 * 1024 * 1024
|
|
49
|
+
"""The default budget for each worktree's decoded values."""
|
|
50
|
+
|
|
44
51
|
_CACHEABLE_PREFIXES = (
|
|
45
52
|
Keyset.DEFAULT_PREFIX,
|
|
46
53
|
PARENT_COMMIT.replace("%s", ""),
|
|
@@ -281,3 +288,82 @@ class CachedStore(KVStore):
|
|
|
281
288
|
if GC_LEASE_KEY in writes:
|
|
282
289
|
# This process's own sweep taking or releasing the lease.
|
|
283
290
|
self.cache.saw_lease(writes[GC_LEASE_KEY])
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ValueCache:
|
|
294
|
+
"""A worktree's decoded values of committed keys, least recently
|
|
295
|
+
used first out, bounded in bytes.
|
|
296
|
+
|
|
297
|
+
It holds only what the worktree's head already stores, so anything
|
|
298
|
+
it lets go is read again from the store; pending writes are kept by
|
|
299
|
+
the worktree itself and never pass through here. A value's cost is
|
|
300
|
+
the bytes it was decoded from (its blob, plus any chunks a chunked
|
|
301
|
+
codec read), and a value costing more than a quarter of the budget
|
|
302
|
+
is not held at all, so one large read cannot push out everything
|
|
303
|
+
else and the largest values, the ones a caller may want only a few
|
|
304
|
+
bytes of, are not kept alive by the cache. A budget of ``0`` holds
|
|
305
|
+
nothing.
|
|
306
|
+
|
|
307
|
+
Safe to use from several threads.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def __init__(self, max_bytes: int = DEFAULT_VALUE_CACHE_BYTES) -> None:
|
|
311
|
+
if max_bytes < 0:
|
|
312
|
+
raise ValueError(f"max_bytes must not be negative, got {max_bytes}")
|
|
313
|
+
self.max_bytes = max_bytes
|
|
314
|
+
self._entries: OrderedDict[str, tuple[Any, int]] = OrderedDict()
|
|
315
|
+
self._bytes = 0
|
|
316
|
+
self._lock = threading.Lock()
|
|
317
|
+
|
|
318
|
+
def __repr__(self) -> str:
|
|
319
|
+
return (
|
|
320
|
+
f"ValueCache({len(self._entries)} entries, "
|
|
321
|
+
f"{self._bytes}/{self.max_bytes} bytes)"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def __len__(self) -> int:
|
|
325
|
+
return len(self._entries)
|
|
326
|
+
|
|
327
|
+
def __contains__(self, key: object) -> bool:
|
|
328
|
+
return key in self._entries
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def size_bytes(self) -> int:
|
|
332
|
+
"""What the values held now cost, by the budget's estimate."""
|
|
333
|
+
return self._bytes
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def max_value_bytes(self) -> int:
|
|
337
|
+
"""The largest cost of a value that is held."""
|
|
338
|
+
return self.max_bytes // 4
|
|
339
|
+
|
|
340
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
341
|
+
"""The value held for ``key``, marked as just used; ``default``
|
|
342
|
+
if none is."""
|
|
343
|
+
with self._lock:
|
|
344
|
+
entry = self._entries.get(key)
|
|
345
|
+
if entry is None:
|
|
346
|
+
return default
|
|
347
|
+
self._entries.move_to_end(key)
|
|
348
|
+
return entry[0]
|
|
349
|
+
|
|
350
|
+
def remember(self, key: str, value: Any, nbytes: int) -> None:
|
|
351
|
+
"""Hold ``value``, decoded from ``nbytes`` bytes, evicting the
|
|
352
|
+
least recently used to fit."""
|
|
353
|
+
cost = nbytes + len(key) + _ENTRY_OVERHEAD
|
|
354
|
+
if cost > self.max_value_bytes:
|
|
355
|
+
return
|
|
356
|
+
with self._lock:
|
|
357
|
+
old = self._entries.pop(key, None)
|
|
358
|
+
if old is not None:
|
|
359
|
+
self._bytes -= old[1]
|
|
360
|
+
self._entries[key] = (value, cost)
|
|
361
|
+
self._bytes += cost
|
|
362
|
+
while self._bytes > self.max_bytes:
|
|
363
|
+
_, (_, evicted) = self._entries.popitem(last=False)
|
|
364
|
+
self._bytes -= evicted
|
|
365
|
+
|
|
366
|
+
def clear(self) -> None:
|
|
367
|
+
with self._lock:
|
|
368
|
+
self._entries.clear()
|
|
369
|
+
self._bytes = 0
|
|
@@ -18,6 +18,28 @@ class ConcurrencyError(KvgitError):
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
class BranchMovedError(ConcurrencyError):
|
|
22
|
+
"""Raised by a commit made with ``on_moved="raise"`` that found its
|
|
23
|
+
branch moved: the branch's head is no longer the commit the handle
|
|
24
|
+
is based on. Nothing was published and pending changes are kept;
|
|
25
|
+
refresh and redo the work, or give up.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
branch: The branch.
|
|
29
|
+
base: The commit the handle is based on, which the branch no
|
|
30
|
+
longer names.
|
|
31
|
+
head: The branch's head as the commit found it.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, branch: str, base: str, head: str) -> None:
|
|
35
|
+
self.branch = branch
|
|
36
|
+
self.base = base
|
|
37
|
+
self.head = head
|
|
38
|
+
super().__init__(
|
|
39
|
+
f"Branch '{branch}' moved from {base} to {head}; nothing was committed"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
21
43
|
class MergeConflict(KvgitError):
|
|
22
44
|
"""Raised when a three-way merge encounters unresolvable conflicts.
|
|
23
45
|
|
|
@@ -77,8 +77,19 @@ class Branches(Mapping[str, str]):
|
|
|
77
77
|
``gc``; worktrees still holding it read from their heads and raise
|
|
78
78
|
:class:`UnknownBranchError` on commit.
|
|
79
79
|
|
|
80
|
+
The removal is conditional on the head this call read: if another
|
|
81
|
+
writer moves the branch, or deletes and recreates it, in between,
|
|
82
|
+
nothing is removed and the call raises rather than retrying, so a
|
|
83
|
+
newer head is never deleted on the strength of an older one. A
|
|
84
|
+
branch made again at the same commit is indistinguishable from
|
|
85
|
+
the one read, and is deleted, as with git's
|
|
86
|
+
``update-ref -d <ref> <old-oid>``.
|
|
87
|
+
|
|
80
88
|
Raises:
|
|
81
89
|
UnknownBranchError: if there is no such branch.
|
|
90
|
+
ConcurrencyError: if the branch changed while being deleted.
|
|
91
|
+
ValueError: if ``name`` is empty or in the reserved tag
|
|
92
|
+
namespace.
|
|
82
93
|
"""
|
|
83
94
|
_kv.delete_branch(self._repo._store, name)
|
|
84
95
|
|
|
@@ -136,8 +147,14 @@ class Tags(Mapping[str, str]):
|
|
|
136
147
|
"""Delete a tag. A commit it alone kept alive becomes collectable
|
|
137
148
|
at the next ``gc``.
|
|
138
149
|
|
|
150
|
+
The removal is conditional on the head and info record this call
|
|
151
|
+
read: a tag deleted and made again by another writer in between,
|
|
152
|
+
even at the same commit, keeps its new commit and info, and the
|
|
153
|
+
call raises rather than retrying.
|
|
154
|
+
|
|
139
155
|
Raises:
|
|
140
156
|
UnknownTagError: if there is no such tag.
|
|
157
|
+
ConcurrencyError: if the tag changed while being deleted.
|
|
141
158
|
"""
|
|
142
159
|
_kv.delete_tag(self._repo._store, name)
|
|
143
160
|
|
|
@@ -7,7 +7,12 @@ from dataclasses import dataclass
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from ._codec import Codec, CodecSpec
|
|
10
|
-
from .cache import
|
|
10
|
+
from .cache import (
|
|
11
|
+
DEFAULT_CACHE_BYTES,
|
|
12
|
+
DEFAULT_VALUE_CACHE_BYTES,
|
|
13
|
+
CachedStore,
|
|
14
|
+
ContentCache,
|
|
15
|
+
)
|
|
11
16
|
from .content_types import MergeFn
|
|
12
17
|
from .encoding import loads, safe_loads
|
|
13
18
|
from .errors import (
|
|
@@ -75,6 +80,11 @@ class Repo:
|
|
|
75
80
|
default; ``0`` turns the cache off). A sweep by any process
|
|
76
81
|
empties it before this one builds on what the sweep removed;
|
|
77
82
|
see :class:`~kvgit.cache.ContentCache`.
|
|
83
|
+
value_cache_bytes: Memory for the decoded values each worktree
|
|
84
|
+
reads, held until its head moves (32 MB per worktree by
|
|
85
|
+
default; ``0`` holds none). A value costing more than a
|
|
86
|
+
quarter of it is never held; see
|
|
87
|
+
:class:`~kvgit.cache.ValueCache`.
|
|
78
88
|
|
|
79
89
|
Raises:
|
|
80
90
|
StorageVersionError: if the store is stamped with a layout this
|
|
@@ -91,8 +101,14 @@ class Repo:
|
|
|
91
101
|
default_merge: MergeRule | None = None,
|
|
92
102
|
recover_from_corrupt_head: CorruptHeadRecoverer | None = None,
|
|
93
103
|
cache_bytes: int = DEFAULT_CACHE_BYTES,
|
|
104
|
+
value_cache_bytes: int = DEFAULT_VALUE_CACHE_BYTES,
|
|
94
105
|
) -> None:
|
|
95
106
|
_kv._check_storage_version(backend)
|
|
107
|
+
if value_cache_bytes < 0:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"value_cache_bytes must not be negative, got {value_cache_bytes}"
|
|
110
|
+
)
|
|
111
|
+
self._value_cache_bytes = value_cache_bytes
|
|
96
112
|
self._backend = backend
|
|
97
113
|
# 0 turns the cache off; anything else is a budget, and
|
|
98
114
|
# ContentCache refuses one that is negative.
|