brainlessdb 0.7.2__tar.gz → 0.8.0__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 (27) hide show
  1. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/CHANGELOG.md +7 -0
  2. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/PKG-INFO +1 -1
  3. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/brainlessdb/__init__.py +1 -1
  4. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/brainlessdb/bucket.py +12 -4
  5. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/brainlessdb/collection.py +19 -13
  6. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/pyproject.toml +1 -1
  7. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/mock_nats.py +4 -0
  8. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/.gitignore +0 -0
  9. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/.python-version +0 -0
  10. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/README.md +0 -0
  11. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/brainlessdb/client.py +0 -0
  12. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/brainlessdb/fields.py +0 -0
  13. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/brainlessdb/struct.py +0 -0
  14. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/cliff.toml +0 -0
  15. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/code_style_guide.md +0 -0
  16. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/justfile +0 -0
  17. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/test_results.txt +0 -0
  18. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/__init__.py +0 -0
  19. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/conftest.py +0 -0
  20. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/test_client.py +0 -0
  21. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/test_collection.py +0 -0
  22. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/test_deserialization.py +0 -0
  23. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/test_fields.py +0 -0
  24. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/test_load_errors.py +0 -0
  25. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/test_sync.py +0 -0
  26. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/tests/test_thread_safety.py +0 -0
  27. {brainlessdb-0.7.2 → brainlessdb-0.8.0}/uv.lock +0 -0
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
 
6
+ ## [0.8.0] - 2026-02-09
7
+
8
+ ### 🚀 Features
9
+
10
+ - Replace N+1 bucket.all() with watchall stream and parallelize collection load
11
+
12
+
6
13
  ## [0.7.2] - 2026-02-08
7
14
 
8
15
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainlessdb
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: Typed collections backed by NATS JetStream KV
5
5
  Author-email: "INSOFT s.r.o." <helpdesk@insoft.cz>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  """Brainless DB - Typed collections backed by NATS JetStream KV."""
2
2
 
3
- __version__ = "0.7.2"
3
+ __version__ = "0.8.0"
4
4
 
5
5
  from typing import Any, Optional, TypeVar
6
6
 
@@ -71,10 +71,18 @@ class Bucket:
71
71
 
72
72
  async def all(self) -> dict[str, bytes]:
73
73
  result = {}
74
- for key in await self.keys():
75
- value = await self.get(key)
76
- if value is not None:
77
- result[key] = value
74
+ watcher = await self._kv.watchall(include_history=True)
75
+ try:
76
+ while True:
77
+ entry = await watcher.updates(timeout=5)
78
+ if entry is None:
79
+ break
80
+ op = str(entry.operation) if entry.operation else ""
81
+ is_del = "DEL" in op.upper() or "PURGE" in op.upper()
82
+ if not is_del and entry.value is not None:
83
+ result[entry.key] = entry.value
84
+ finally:
85
+ await watcher.stop()
78
86
  return result
79
87
 
80
88
  async def watch(self, include_history: bool = False) -> AsyncIterator[WatchEntry]:
@@ -197,6 +197,10 @@ class Collection(Generic[T]):
197
197
  self._local_bucket = await self._client.get_bucket(a.local_bucket)
198
198
  return True
199
199
 
200
+ @staticmethod
201
+ async def _load_raw(bucket: Optional[Bucket]) -> dict[str, bytes]:
202
+ return await bucket.all() if bucket else {}
203
+
200
204
  @staticmethod
201
205
  async def _load_bucket(bucket: Optional[Bucket]) -> dict[str, dict]:
202
206
  if not bucket:
@@ -337,20 +341,22 @@ class Collection(Generic[T]):
337
341
  self._loaded = True
338
342
  return 0
339
343
 
340
- # Load from all buckets
341
- config_entries: dict[str, tuple[dict, Any]] = {}
342
- if self._config_bucket:
343
- for uid, raw in (await self._config_bucket.all()).items():
344
- try:
345
- w = msgspec.json.decode(raw, type=ConfigWrapper)
346
- except Exception as ex:
347
- _log.error("Failed to decode config for '%s' in '%s': %s", uid, self.name, ex)
348
- continue
349
- config_entries[uid] = (w.d, w.m)
350
- self._metadata[uid] = w.m
344
+ # Load all buckets in parallel
345
+ config_bytes, state, local = await asyncio.gather(
346
+ self._load_raw(self._config_bucket),
347
+ self._load_bucket(self._state_bucket),
348
+ self._load_bucket(self._local_bucket),
349
+ )
351
350
 
352
- state = await self._load_bucket(self._state_bucket)
353
- local = await self._load_bucket(self._local_bucket)
351
+ config_entries: dict[str, tuple[dict, Any]] = {}
352
+ for uid, raw in config_bytes.items():
353
+ try:
354
+ w = msgspec.json.decode(raw, type=ConfigWrapper)
355
+ except Exception as ex:
356
+ _log.error("Failed to decode config for '%s' in '%s': %s", uid, self.name, ex)
357
+ continue
358
+ config_entries[uid] = (w.d, w.m)
359
+ self._metadata[uid] = w.m
354
360
 
355
361
  # Build entities
356
362
  for uid, (cfg, _) in config_entries.items():
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brainlessdb"
3
- version = "0.7.2"
3
+ version = "0.8.0"
4
4
  description = "Typed collections backed by NATS JetStream KV"
5
5
  authors = [
6
6
  {name = "INSOFT s.r.o.", email = "helpdesk@insoft.cz"}
@@ -50,6 +50,10 @@ class MockKV:
50
50
  async def watchall(self, include_history: bool = False) -> "MockWatcher":
51
51
  q: asyncio.Queue = asyncio.Queue()
52
52
  self._watchers.append(q)
53
+ if include_history:
54
+ for key, value in self._data.items():
55
+ await q.put(MockEntry(key=key, value=value, revision=self._revision))
56
+ await q.put(None)
53
57
  return MockWatcher(q, self._watchers)
54
58
 
55
59
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes