kvgit 0.3.0__tar.gz → 0.3.2__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.
Files changed (47) hide show
  1. {kvgit-0.3.0 → kvgit-0.3.2}/PKG-INFO +3 -7
  2. {kvgit-0.3.0 → kvgit-0.3.2}/README.md +2 -2
  3. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/__init__.py +2 -8
  4. kvgit-0.3.2/kvgit/kv/composite.py +191 -0
  5. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/kv/disk.py +7 -0
  6. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/staged.py +1 -2
  7. kvgit-0.3.2/kvgit/store.py +152 -0
  8. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/versioned/__init__.py +0 -7
  9. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/versioned/kv.py +171 -138
  10. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/versioned/merge.py +2 -3
  11. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/versioned/protocol.py +1 -2
  12. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit.egg-info/PKG-INFO +3 -7
  13. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit.egg-info/SOURCES.txt +1 -1
  14. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit.egg-info/requires.txt +0 -5
  15. {kvgit-0.3.0 → kvgit-0.3.2}/pyproject.toml +3 -5
  16. kvgit-0.3.2/tests/test_delete_branches.py +209 -0
  17. kvgit-0.3.0/kvgit/kv/composite.py +0 -137
  18. kvgit-0.3.0/kvgit/store.py +0 -87
  19. kvgit-0.3.0/kvgit/versioned/gp.py +0 -360
  20. {kvgit-0.3.0 → kvgit-0.3.2}/LICENSE +0 -0
  21. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/codecs/__init__.py +0 -0
  22. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/codecs/_hash.py +0 -0
  23. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/codecs/base.py +0 -0
  24. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/codecs/numpy.py +0 -0
  25. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/codecs/pandas.py +0 -0
  26. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/codecs/pickler.py +0 -0
  27. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/content_types.py +0 -0
  28. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/encoding.py +0 -0
  29. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/errors.py +0 -0
  30. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/hamt.py +0 -0
  31. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/kv/__init__.py +0 -0
  32. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/kv/base.py +0 -0
  33. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/kv/indexeddb.py +0 -0
  34. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/kv/memory.py +0 -0
  35. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/namespaced.py +0 -0
  36. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/py.typed +0 -0
  37. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/versioned/base.py +0 -0
  38. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/versioned/helpers.py +0 -0
  39. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit/versioned/keyset.py +0 -0
  40. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit.egg-info/dependency_links.txt +0 -0
  41. {kvgit-0.3.0 → kvgit-0.3.2}/kvgit.egg-info/top_level.txt +0 -0
  42. {kvgit-0.3.0 → kvgit-0.3.2}/setup.cfg +0 -0
  43. {kvgit-0.3.0 → kvgit-0.3.2}/tests/test_content_types.py +0 -0
  44. {kvgit-0.3.0 → kvgit-0.3.2}/tests/test_hamt.py +0 -0
  45. {kvgit-0.3.0 → kvgit-0.3.2}/tests/test_namespaced.py +0 -0
  46. {kvgit-0.3.0 → kvgit-0.3.2}/tests/test_staged.py +0 -0
  47. {kvgit-0.3.0 → kvgit-0.3.2}/tests/test_store_factory.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kvgit
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Versioned key-value store with git-like commit, branch, and merge semantics.
5
5
  Author: ashenfad
6
6
  License: MIT
@@ -27,8 +27,6 @@ Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Provides-Extra: disk
29
29
  Requires-Dist: diskcache; extra == "disk"
30
- Provides-Extra: git
31
- Requires-Dist: gitpython>=3.1; extra == "git"
32
30
  Provides-Extra: numpy
33
31
  Requires-Dist: numpy>=1.24; extra == "numpy"
34
32
  Provides-Extra: pandas
@@ -39,13 +37,11 @@ Requires-Dist: numpy>=1.24; extra == "scientific"
39
37
  Requires-Dist: pandas>=2.0; extra == "scientific"
40
38
  Provides-Extra: all
41
39
  Requires-Dist: diskcache; extra == "all"
42
- Requires-Dist: gitpython>=3.1; extra == "all"
43
40
  Requires-Dist: numpy>=1.24; extra == "all"
44
41
  Requires-Dist: pandas>=2.0; extra == "all"
45
42
  Provides-Extra: dev
46
43
  Requires-Dist: pytest; extra == "dev"
47
44
  Requires-Dist: diskcache; extra == "dev"
48
- Requires-Dist: gitpython>=3.1; extra == "dev"
49
45
  Requires-Dist: ruff; extra == "dev"
50
46
  Requires-Dist: numpy>=1.24; extra == "dev"
51
47
  Requires-Dist: pandas>=2.0; extra == "dev"
@@ -61,7 +57,7 @@ Git-style versioning for your data. Commits, branches, and merges -- backed by a
61
57
  | **Commits** | Immutable, content-addressable snapshots with rollback |
62
58
  | **Branches** | Cheap forks with CAS-based optimistic concurrency |
63
59
  | **Three-way merge** | Auto-merges non-overlapping changes; pluggable merge fns for conflicts |
64
- | **Pluggable backends** | In-memory, disk (diskcache), git (GitPython), IndexedDB (Pyodide/browser), or bring your own `KVStore` |
60
+ | **Pluggable backends** | In-memory, disk (diskcache), IndexedDB (Pyodide/browser), or bring your own `KVStore` |
65
61
  | **Chunked codecs** | Optional content-addressed dedup for large numpy arrays and pandas DataFrames -- equal buffers stored once across keys, commits, and branches |
66
62
 
67
63
  ## Install
@@ -69,7 +65,6 @@ Git-style versioning for your data. Commits, branches, and merges -- backed by a
69
65
  ```bash
70
66
  pip install kvgit # in-memory only
71
67
  pip install kvgit[disk] # adds disk backend via diskcache
72
- pip install kvgit[git] # adds git backend via GitPython (requires git on PATH)
73
68
  pip install kvgit[scientific] # adds chunked codecs for numpy / pandas
74
69
  # IndexedDB backend is available automatically in Pyodide (browser) environments
75
70
  ```
@@ -131,3 +126,4 @@ See [`docs/`](docs/) for detailed documentation:
131
126
 
132
127
  - [Quick Start](docs/quick-start.md) -- common patterns with runnable examples
133
128
  - [API Reference](docs/api.md) -- full reference for all classes, methods, and types
129
+ - [Browser persistence (Pyodide)](docs/pyodide.md) -- choosing between the IndexedDB and OPFS-mounted-disk backends, plus the syncfs flush requirement and recommended host-side patterns
@@ -8,7 +8,7 @@ Git-style versioning for your data. Commits, branches, and merges -- backed by a
8
8
  | **Commits** | Immutable, content-addressable snapshots with rollback |
9
9
  | **Branches** | Cheap forks with CAS-based optimistic concurrency |
10
10
  | **Three-way merge** | Auto-merges non-overlapping changes; pluggable merge fns for conflicts |
11
- | **Pluggable backends** | In-memory, disk (diskcache), git (GitPython), IndexedDB (Pyodide/browser), or bring your own `KVStore` |
11
+ | **Pluggable backends** | In-memory, disk (diskcache), IndexedDB (Pyodide/browser), or bring your own `KVStore` |
12
12
  | **Chunked codecs** | Optional content-addressed dedup for large numpy arrays and pandas DataFrames -- equal buffers stored once across keys, commits, and branches |
13
13
 
14
14
  ## Install
@@ -16,7 +16,6 @@ Git-style versioning for your data. Commits, branches, and merges -- backed by a
16
16
  ```bash
17
17
  pip install kvgit # in-memory only
18
18
  pip install kvgit[disk] # adds disk backend via diskcache
19
- pip install kvgit[git] # adds git backend via GitPython (requires git on PATH)
20
19
  pip install kvgit[scientific] # adds chunked codecs for numpy / pandas
21
20
  # IndexedDB backend is available automatically in Pyodide (browser) environments
22
21
  ```
@@ -78,3 +77,4 @@ See [`docs/`](docs/) for detailed documentation:
78
77
 
79
78
  - [Quick Start](docs/quick-start.md) -- common patterns with runnable examples
80
79
  - [API Reference](docs/api.md) -- full reference for all classes, methods, and types
80
+ - [Browser persistence (Pyodide)](docs/pyodide.md) -- choosing between the IndexedDB and OPFS-mounted-disk backends, plus the syncfs flush requirement and recommended host-side patterns
@@ -5,7 +5,7 @@ from .errors import ConcurrencyError, MergeConflict
5
5
  from .namespaced import Namespaced
6
6
  from .versioned.protocol import MergeResult, Versioned
7
7
  from .staged import Staged
8
- from .store import store
8
+ from .store import delete_branches, store
9
9
  from .versioned.kv import VersionedKV
10
10
 
11
11
  __all__ = [
@@ -18,13 +18,7 @@ __all__ = [
18
18
  "Versioned",
19
19
  "VersionedKV",
20
20
  "counter",
21
+ "delete_branches",
21
22
  "last_writer_wins",
22
23
  "store",
23
24
  ]
24
-
25
- try:
26
- from .versioned.gp import VersionedGP
27
-
28
- __all__ += ["VersionedGP"]
29
- except ImportError:
30
- pass
@@ -0,0 +1,191 @@
1
+ """N-tier composite cache over multiple KV stores."""
2
+
3
+ import logging
4
+ from collections.abc import Iterable, Mapping
5
+
6
+ from .base import KVStore
7
+
8
+ logger = logging.getLogger("kvgit.kv.composite")
9
+
10
+ # Exceptions we treat as programming bugs (a misconfigured tier, a
11
+ # protocol mismatch) rather than operational tier unavailability.
12
+ # These are re-raised so they surface instead of being silently masked
13
+ # by the cache-fallback machinery. Everything else under Exception
14
+ # (OSError, ConnectionError, Pyodide JsException, ...) is logged and
15
+ # treated as "tier unavailable, try next".
16
+ _BUG_EXCEPTIONS = (TypeError, AttributeError, AssertionError)
17
+
18
+
19
+ def _is_bug(exc: BaseException) -> bool:
20
+ return isinstance(exc, _BUG_EXCEPTIONS)
21
+
22
+
23
+ class Composite(KVStore):
24
+ """N-tier cache composing any number of KV stores.
25
+
26
+ On get: check L1, L2, ..., Ln in order. On hit at tier i,
27
+ populate L1..L(i-1) and return.
28
+
29
+ On set: write to all tiers (most durable first).
30
+
31
+ On cas: delegate to Ln (authoritative), update caches on success.
32
+
33
+ Tier failures (``OSError``, network errors, etc.) are logged at
34
+ WARNING and the next tier is tried; programming-error exceptions
35
+ (``TypeError``, ``AttributeError``, ``AssertionError``) propagate
36
+ so they aren't silently swallowed.
37
+
38
+ Args:
39
+ stores: List of KV stores ordered fastest -> most durable.
40
+ """
41
+
42
+ def __init__(self, stores: list[KVStore]) -> None:
43
+ if not stores:
44
+ raise ValueError("Composite requires at least one store")
45
+ self._stores = stores
46
+
47
+ def _populate_caches(self, upto: int, items: Mapping[str, bytes]) -> None:
48
+ """Best-effort write to faster tiers after a slow-tier hit."""
49
+ for j in range(upto):
50
+ try:
51
+ self._stores[j].set_many(items)
52
+ except Exception as e:
53
+ if _is_bug(e):
54
+ raise
55
+ logger.warning("Composite cache populate failed at tier %d: %s", j, e)
56
+
57
+ def get(self, key: str) -> bytes | None:
58
+ for i, store in enumerate(self._stores):
59
+ try:
60
+ value = store.get(key)
61
+ except Exception as e:
62
+ if _is_bug(e):
63
+ raise
64
+ logger.warning("Composite get failed at tier %d for %r: %s", i, key, e)
65
+ continue
66
+ if value is not None:
67
+ if i > 0:
68
+ self._populate_caches(i, {key: value})
69
+ return value
70
+ return None
71
+
72
+ def get_many(self, *args) -> Mapping[str, bytes]:
73
+ result: dict[str, bytes] = {}
74
+ remaining = set(self._normalize_keys(args))
75
+ for i, store in enumerate(self._stores):
76
+ if not remaining:
77
+ break
78
+ try:
79
+ # Delegate to the tier's bulk get — backends with high
80
+ # per-call latency (Disk, IndexedDB) collapse N round-trips
81
+ # into one. The protocol guarantees only existing keys
82
+ # appear in the result.
83
+ tier_values = store.get_many(remaining)
84
+ except Exception as e:
85
+ if _is_bug(e):
86
+ raise
87
+ logger.warning("Composite get_many failed at tier %d: %s", i, e)
88
+ continue
89
+ if tier_values and i > 0:
90
+ self._populate_caches(i, tier_values)
91
+ result.update(tier_values)
92
+ remaining -= tier_values.keys()
93
+ return result
94
+
95
+ def __contains__(self, key: str) -> bool:
96
+ for i, store in enumerate(self._stores):
97
+ try:
98
+ if key in store:
99
+ return True
100
+ except Exception as e:
101
+ if _is_bug(e):
102
+ raise
103
+ logger.warning(
104
+ "Composite contains failed at tier %d for %r: %s", i, key, e
105
+ )
106
+ continue
107
+ return False
108
+
109
+ def keys(self) -> Iterable[str]:
110
+ return self._stores[-1].keys()
111
+
112
+ def items(self) -> Iterable[tuple[str, bytes]]:
113
+ return self._stores[-1].items()
114
+
115
+ def set(self, key: str, value: bytes) -> None:
116
+ # Authoritative tier first; failures here propagate (durability
117
+ # is the contract of set()). Cache-tier failures are logged.
118
+ self._stores[-1].set(key, value)
119
+ for i, store in enumerate(self._stores[:-1]):
120
+ try:
121
+ store.set(key, value)
122
+ except Exception as e:
123
+ if _is_bug(e):
124
+ raise
125
+ logger.warning("Composite set failed at tier %d for %r: %s", i, key, e)
126
+
127
+ def set_many(
128
+ self,
129
+ items: Mapping[str, bytes] | None = None,
130
+ /,
131
+ **kwargs: bytes,
132
+ ) -> None:
133
+ items = self._normalize_items(items, kwargs)
134
+ self._stores[-1].set_many(items)
135
+ for i, store in enumerate(self._stores[:-1]):
136
+ try:
137
+ store.set_many(items)
138
+ except Exception as e:
139
+ if _is_bug(e):
140
+ raise
141
+ logger.warning("Composite set_many failed at tier %d: %s", i, e)
142
+
143
+ def remove(self, key: str) -> None:
144
+ self._stores[-1].remove(key)
145
+ for i, store in enumerate(self._stores[:-1]):
146
+ try:
147
+ store.remove(key)
148
+ except Exception as e:
149
+ if _is_bug(e):
150
+ raise
151
+ logger.warning(
152
+ "Composite remove failed at tier %d for %r: %s", i, key, e
153
+ )
154
+
155
+ def remove_many(self, *args) -> None:
156
+ keys = list(self._normalize_keys(args))
157
+ self._stores[-1].remove_many(keys)
158
+ for i, store in enumerate(self._stores[:-1]):
159
+ try:
160
+ store.remove_many(keys)
161
+ except Exception as e:
162
+ if _is_bug(e):
163
+ raise
164
+ logger.warning("Composite remove_many failed at tier %d: %s", i, e)
165
+
166
+ def clear(self) -> None:
167
+ self._stores[-1].clear()
168
+ for i, store in enumerate(self._stores[:-1]):
169
+ try:
170
+ store.clear()
171
+ except Exception as e:
172
+ if _is_bug(e):
173
+ raise
174
+ logger.warning("Composite clear failed at tier %d: %s", i, e)
175
+
176
+ def cas(self, key: str, value: bytes, expected: bytes | None) -> bool:
177
+ success = self._stores[-1].cas(key, value, expected)
178
+ if success:
179
+ for i, store in enumerate(self._stores[:-1]):
180
+ try:
181
+ store.set(key, value)
182
+ except Exception as e:
183
+ if _is_bug(e):
184
+ raise
185
+ logger.warning(
186
+ "Composite cas cache-update failed at tier %d for %r: %s",
187
+ i,
188
+ key,
189
+ e,
190
+ )
191
+ return success
@@ -87,3 +87,10 @@ class Disk(KVStore):
87
87
 
88
88
  def clear(self) -> None:
89
89
  self.store.clear()
90
+
91
+ def close(self) -> None:
92
+ # diskcache holds SQLite connections (one per thread) open until
93
+ # closed. Admin teardown (kvgit.delete_branches) constructs a
94
+ # backend just to sweep and must release the handle so the next
95
+ # opener on the same directory isn't blocked.
96
+ self.store.close()
@@ -126,8 +126,7 @@ class Staged(MutableMapping[str, Any]):
126
126
  "chunked encoder/decoder requires a VersionedKV-backed Staged "
127
127
  f"(got {type(versioned).__name__}). "
128
128
  "Use kvgit.store(kind='memory'/'disk'/'indexeddb') or build a "
129
- "VersionedKV directly. The git backend (VersionedGP) does not "
130
- "support chunked codecs."
129
+ "VersionedKV directly."
131
130
  )
132
131
  self._chunk_reader = (
133
132
  _ChunkReader(versioned.store) if self._decoder_chunked else None
@@ -0,0 +1,152 @@
1
+ """Store factory function."""
2
+
3
+ import pickle
4
+ from collections.abc import Iterable
5
+ from typing import Any, Callable, Literal
6
+
7
+ from .kv.base import KVStore
8
+ from .kv.memory import Memory
9
+ from .staged import Staged
10
+ from .versioned.kv import BRANCH_HEAD, BRANCH_HEAD_PREV, VersionedKV, clean_orphans
11
+
12
+
13
+ def _make_backend(
14
+ kind: Literal["memory", "disk", "indexeddb"],
15
+ *,
16
+ path: str | None,
17
+ db_name: str,
18
+ ) -> KVStore:
19
+ """Construct a raw ``KVStore`` backend by kind.
20
+
21
+ Shared by :func:`store` and :func:`delete_branches` so both open the
22
+ exact same backends from the same parameters.
23
+ """
24
+ if kind == "memory":
25
+ return Memory()
26
+ elif kind == "disk":
27
+ if path is None:
28
+ raise ValueError("path is required when kind='disk'")
29
+ from .kv.disk import Disk
30
+
31
+ return Disk(path)
32
+ elif kind == "indexeddb":
33
+ from .kv.indexeddb import IndexedDB
34
+
35
+ return IndexedDB(db_name=db_name)
36
+ else:
37
+ raise ValueError(f"Unknown kind: {kind!r}")
38
+
39
+
40
+ def store(
41
+ kind: Literal["memory", "disk", "indexeddb"] = "memory",
42
+ *,
43
+ path: str | None = None,
44
+ db_name: str = "kvgit",
45
+ branch: str = "main",
46
+ encoder: Callable[..., bytes] = pickle.dumps,
47
+ decoder: Callable[..., Any] = pickle.loads,
48
+ codecs: str | None = None,
49
+ ) -> Staged:
50
+ """Create a Staged store with sensible defaults.
51
+
52
+ Args:
53
+ kind: ``"memory"`` (default), ``"disk"``, or ``"indexeddb"``.
54
+ path: Required when ``kind="disk"``. Directory path for the
55
+ disk backend.
56
+ db_name: IndexedDB database name (default ``"kvgit"``).
57
+ Only used when ``kind="indexeddb"``.
58
+ branch: Branch name (default ``"main"``).
59
+ encoder: Value encoder (default ``pickle.dumps``).
60
+ decoder: Value decoder (default ``pickle.loads``).
61
+ codecs: Optional named codec preset. Currently supported:
62
+ ``"scientific"`` — numpy/pandas chunked codecs (requires
63
+ numpy; install with ``pip install kvgit[scientific]``).
64
+ Mutually exclusive with explicit ``encoder`` / ``decoder``.
65
+
66
+ Returns:
67
+ A ``Staged`` store instance.
68
+
69
+ Raises:
70
+ ValueError: if ``codecs`` is given alongside non-default
71
+ ``encoder`` / ``decoder``, or if ``codecs`` names an
72
+ unknown preset.
73
+ ImportError: if a codec preset's optional dependency is not
74
+ installed.
75
+ """
76
+ if codecs is not None:
77
+ if encoder is not pickle.dumps or decoder is not pickle.loads:
78
+ raise ValueError(
79
+ "codecs= is mutually exclusive with explicit encoder/decoder; "
80
+ "pass one or the other"
81
+ )
82
+ from .codecs import _resolve_named
83
+
84
+ encoder, decoder = _resolve_named(codecs)
85
+
86
+ backend = _make_backend(kind, path=path, db_name=db_name)
87
+
88
+ return Staged(VersionedKV(backend, branch=branch), encoder=encoder, decoder=decoder)
89
+
90
+
91
+ def delete_branches(
92
+ names: str | Iterable[str],
93
+ *,
94
+ kind: Literal["memory", "disk", "indexeddb"] = "disk",
95
+ path: str | None = None,
96
+ db_name: str = "kvgit",
97
+ min_age: float = 3600,
98
+ ) -> None:
99
+ """Delete branches with no branch anchor, then sweep orphans.
100
+
101
+ :meth:`VersionedKV.delete_branch` refuses to delete the branch its
102
+ handle is anchored on — and there is no way to open a handle
103
+ *without* a branch. When the branch you want gone is the store's
104
+ only branch, there is nothing safe to anchor on. This admin entry
105
+ point sidesteps the whole problem: it opens the backend directly
106
+ (no ``VersionedKV``, so no current branch) and edits the branch
107
+ keys itself.
108
+
109
+ For each name, ``__branch_head__<name>`` and its
110
+ ``__branch_head_prev__<name>`` recovery backup are removed — the
111
+ backup too, so a later same-named branch can't "recover" the
112
+ deleted state through ``_resolve_head``'s fallback. A name with no
113
+ head is simply skipped: this is teardown, and idempotency beats a
114
+ ``ValueError`` here. Deleting every branch is legal — the store
115
+ just mints a fresh empty ``main`` on next open.
116
+
117
+ One :func:`clean_orphans` sweep runs after all removals, so commits
118
+ that only the deleted branches referenced are reclaimed. All heads
119
+ (and prev-heads) are gone before the sweep, so it can't mistake a
120
+ doomed branch for a live one. ``min_age`` defaults to
121
+ ``clean_orphans``'s one-hour guard (matching ``delete_branch``), so
122
+ an in-flight commit from a concurrent writer on another branch is
123
+ not swept out from under it; an admin who knows the store is quiet
124
+ can pass ``0`` to reclaim young commits immediately.
125
+
126
+ Args:
127
+ names: Branch names to delete — one name or an iterable of
128
+ them. Missing names are no-ops. (A bare string is treated
129
+ as ONE name, not iterated per character.)
130
+ kind: ``"disk"`` (default), ``"memory"``, or ``"indexeddb"``.
131
+ path: Required when ``kind="disk"``. Store directory.
132
+ db_name: IndexedDB database name (``kind="indexeddb"`` only).
133
+ min_age: Passed to :func:`clean_orphans` — commits younger than
134
+ this many seconds survive the sweep.
135
+ """
136
+ # A bare string is a plausible one-branch call, and iterating it
137
+ # would "delete" each character as a branch name — a silent no-op
138
+ # at best. Treat it as a single name, like nontainer's
139
+ # delete_workspace does one layer up.
140
+ doomed = {names} if isinstance(names, str) else set(names)
141
+ if not doomed:
142
+ return # nothing asked: skip the backend open and the sweep
143
+ backend = _make_backend(kind, path=path, db_name=db_name)
144
+ try:
145
+ for name in doomed:
146
+ backend.remove(BRANCH_HEAD % name)
147
+ backend.remove(BRANCH_HEAD_PREV % name)
148
+ clean_orphans(backend, min_age=min_age)
149
+ finally:
150
+ close = getattr(backend, "close", None)
151
+ if callable(close):
152
+ close()
@@ -10,10 +10,3 @@ __all__ = [
10
10
  "Versioned",
11
11
  "VersionedKV",
12
12
  ]
13
-
14
- try:
15
- from .gp import VersionedGP
16
-
17
- __all__ += ["VersionedGP"]
18
- except ImportError:
19
- pass