brainlessdb 0.6.0__tar.gz → 0.7.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 (27) hide show
  1. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/CHANGELOG.md +25 -0
  2. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/PKG-INFO +20 -20
  3. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/README.md +19 -19
  4. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/brainlessdb/__init__.py +3 -3
  5. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/brainlessdb/bucket.py +4 -0
  6. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/brainlessdb/collection.py +25 -11
  7. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/brainlessdb/struct.py +3 -3
  8. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/justfile +1 -1
  9. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/pyproject.toml +1 -1
  10. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/test_results.txt +40 -40
  11. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/conftest.py +6 -6
  12. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/test_collection.py +96 -0
  13. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/test_deserialization.py +15 -15
  14. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/test_fields.py +6 -6
  15. brainlessdb-0.7.2/tests/test_load_errors.py +274 -0
  16. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/uv.lock +1 -1
  17. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/.gitignore +0 -0
  18. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/.python-version +0 -0
  19. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/brainlessdb/client.py +0 -0
  20. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/brainlessdb/fields.py +0 -0
  21. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/cliff.toml +0 -0
  22. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/code_style_guide.md +0 -0
  23. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/__init__.py +0 -0
  24. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/mock_nats.py +0 -0
  25. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/test_client.py +0 -0
  26. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/test_sync.py +0 -0
  27. {brainlessdb-0.6.0 → brainlessdb-0.7.2}/tests/test_thread_safety.py +0 -0
@@ -3,6 +3,31 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
 
6
+ ## [0.7.2] - 2026-02-08
7
+
8
+ ### 🐛 Bug Fixes
9
+
10
+ - Commit pyproject.toml and brainlessdb/__init__.py on just release
11
+
12
+
13
+ ## [0.7.1] - 2026-02-08
14
+
15
+ ### Fchore
16
+
17
+ - Renamed BrainlessStruct to BrainlessBucket and NestedStruct to BrainlessStruct
18
+
19
+
20
+ ## [0.7.0] - 2026-02-06
21
+
22
+ ### 🚀 Features
23
+
24
+ - When deserialization fails log error and move on
25
+
26
+ ### 🐛 Bug Fixes
27
+
28
+ - Preserve None fields in config serialization to prevent ValidationError on reload
29
+
30
+
6
31
  ## [0.6.0] - 2026-02-05
7
32
 
8
33
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainlessdb
3
- Version: 0.6.0
3
+ Version: 0.7.2
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
@@ -18,9 +18,9 @@ Typed collections backed by NATS JetStream KV. In-memory with background sync.
18
18
  ```python
19
19
  from typing import Annotated
20
20
  from msgspec import Meta
21
- from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessStruct
21
+ from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessBucket
22
22
 
23
- class UserV1(BrainlessStruct):
23
+ class UserV1(BrainlessBucket):
24
24
  id: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX})]
25
25
  name: Annotated[str, Meta()] = ""
26
26
 
@@ -46,24 +46,24 @@ await db.stop()
46
46
 
47
47
  | Class | Use for | Has UUID | Stored in |
48
48
  |-------|---------|----------|-----------|
49
- | `BrainlessStruct` | Main entities (User, Order, etc.) | Yes | Own bucket(s) |
50
- | `NestedStruct` | Nested data, app-local data | No | Inside parent entity |
49
+ | `BrainlessBucket` | Main entities (User, Order, etc.) | Yes | Own bucket(s) |
50
+ | `BrainlessStruct` | Nested data, app-local data | No | Inside parent entity |
51
51
 
52
52
  ```python
53
- from brainlessdb import BrainlessStruct, NestedStruct
53
+ from brainlessdb import BrainlessBucket, BrainlessStruct
54
54
 
55
- # App-local data - NestedStruct (no UUID, not an entity)
56
- class UcsLocal(NestedStruct):
55
+ # App-local data - BrainlessStruct (no UUID, not an entity)
56
+ class UcsLocal(BrainlessStruct):
57
57
  sid: int = 0
58
58
  pointer: int = 0
59
59
 
60
- # Nested data - NestedStruct
61
- class Address(NestedStruct):
60
+ # Nested data - BrainlessStruct
61
+ class Address(BrainlessStruct):
62
62
  street: str = ""
63
63
  city: str = ""
64
64
 
65
- # Main entity - BrainlessStruct (has UUID, stored in NATS)
66
- class UserV1(BrainlessStruct):
65
+ # Main entity - BrainlessBucket (has UUID, stored in NATS)
66
+ class UserV1(BrainlessBucket):
67
67
  id: Annotated[int, Meta()]
68
68
  address: Optional[Address] = None # nested struct
69
69
  _: Optional[UcsLocal] = None # app-local struct
@@ -87,7 +87,7 @@ await brainlessdb.stop()
87
87
  Persistent data synced across all instances:
88
88
 
89
89
  ```python
90
- class UserV1(BrainlessStruct):
90
+ class UserV1(BrainlessBucket):
91
91
  id: Annotated[int, Meta()]
92
92
  name: Annotated[str, Meta()] = ""
93
93
  ```
@@ -96,7 +96,7 @@ class UserV1(BrainlessStruct):
96
96
  Ephemeral data (separate bucket, faster sync):
97
97
 
98
98
  ```python
99
- class UserV1(BrainlessStruct):
99
+ class UserV1(BrainlessBucket):
100
100
  id: Annotated[int, Meta()]
101
101
  status: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.STATE})] = 0
102
102
  ```
@@ -105,7 +105,7 @@ class UserV1(BrainlessStruct):
105
105
  Fast O(1) lookups:
106
106
 
107
107
  ```python
108
- class UserV1(BrainlessStruct):
108
+ class UserV1(BrainlessBucket):
109
109
  id: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX})]
110
110
  ```
111
111
 
@@ -113,7 +113,7 @@ class UserV1(BrainlessStruct):
113
113
  Enforces uniqueness constraint (also auto-indexed):
114
114
 
115
115
  ```python
116
- class UserV1(BrainlessStruct):
116
+ class UserV1(BrainlessBucket):
117
117
  email: Annotated[Optional[str], Meta(extra={"brainlessdb_flags": BrainlessDBFeat.UNIQUE})] = None
118
118
  ```
119
119
 
@@ -127,16 +127,16 @@ counter: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX |
127
127
  Data private to each namespace:
128
128
 
129
129
  ```python
130
- from brainlessdb import BrainlessStruct, NestedStruct
130
+ from brainlessdb import BrainlessBucket, BrainlessStruct
131
131
 
132
- class UcsLocal(NestedStruct):
132
+ class UcsLocal(BrainlessStruct):
133
133
  sid: int = 0
134
134
 
135
- class AriLocal(NestedStruct):
135
+ class AriLocal(BrainlessStruct):
136
136
  channel_id: str = ""
137
137
 
138
138
  # Entity with app-local field
139
- class UserV1(BrainlessStruct):
139
+ class UserV1(BrainlessBucket):
140
140
  id: Annotated[int, Meta()]
141
141
  _: Union[UcsLocal, AriLocal, None] = None
142
142
  ```
@@ -7,9 +7,9 @@ Typed collections backed by NATS JetStream KV. In-memory with background sync.
7
7
  ```python
8
8
  from typing import Annotated
9
9
  from msgspec import Meta
10
- from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessStruct
10
+ from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessBucket
11
11
 
12
- class UserV1(BrainlessStruct):
12
+ class UserV1(BrainlessBucket):
13
13
  id: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX})]
14
14
  name: Annotated[str, Meta()] = ""
15
15
 
@@ -35,24 +35,24 @@ await db.stop()
35
35
 
36
36
  | Class | Use for | Has UUID | Stored in |
37
37
  |-------|---------|----------|-----------|
38
- | `BrainlessStruct` | Main entities (User, Order, etc.) | Yes | Own bucket(s) |
39
- | `NestedStruct` | Nested data, app-local data | No | Inside parent entity |
38
+ | `BrainlessBucket` | Main entities (User, Order, etc.) | Yes | Own bucket(s) |
39
+ | `BrainlessStruct` | Nested data, app-local data | No | Inside parent entity |
40
40
 
41
41
  ```python
42
- from brainlessdb import BrainlessStruct, NestedStruct
42
+ from brainlessdb import BrainlessBucket, BrainlessStruct
43
43
 
44
- # App-local data - NestedStruct (no UUID, not an entity)
45
- class UcsLocal(NestedStruct):
44
+ # App-local data - BrainlessStruct (no UUID, not an entity)
45
+ class UcsLocal(BrainlessStruct):
46
46
  sid: int = 0
47
47
  pointer: int = 0
48
48
 
49
- # Nested data - NestedStruct
50
- class Address(NestedStruct):
49
+ # Nested data - BrainlessStruct
50
+ class Address(BrainlessStruct):
51
51
  street: str = ""
52
52
  city: str = ""
53
53
 
54
- # Main entity - BrainlessStruct (has UUID, stored in NATS)
55
- class UserV1(BrainlessStruct):
54
+ # Main entity - BrainlessBucket (has UUID, stored in NATS)
55
+ class UserV1(BrainlessBucket):
56
56
  id: Annotated[int, Meta()]
57
57
  address: Optional[Address] = None # nested struct
58
58
  _: Optional[UcsLocal] = None # app-local struct
@@ -76,7 +76,7 @@ await brainlessdb.stop()
76
76
  Persistent data synced across all instances:
77
77
 
78
78
  ```python
79
- class UserV1(BrainlessStruct):
79
+ class UserV1(BrainlessBucket):
80
80
  id: Annotated[int, Meta()]
81
81
  name: Annotated[str, Meta()] = ""
82
82
  ```
@@ -85,7 +85,7 @@ class UserV1(BrainlessStruct):
85
85
  Ephemeral data (separate bucket, faster sync):
86
86
 
87
87
  ```python
88
- class UserV1(BrainlessStruct):
88
+ class UserV1(BrainlessBucket):
89
89
  id: Annotated[int, Meta()]
90
90
  status: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.STATE})] = 0
91
91
  ```
@@ -94,7 +94,7 @@ class UserV1(BrainlessStruct):
94
94
  Fast O(1) lookups:
95
95
 
96
96
  ```python
97
- class UserV1(BrainlessStruct):
97
+ class UserV1(BrainlessBucket):
98
98
  id: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX})]
99
99
  ```
100
100
 
@@ -102,7 +102,7 @@ class UserV1(BrainlessStruct):
102
102
  Enforces uniqueness constraint (also auto-indexed):
103
103
 
104
104
  ```python
105
- class UserV1(BrainlessStruct):
105
+ class UserV1(BrainlessBucket):
106
106
  email: Annotated[Optional[str], Meta(extra={"brainlessdb_flags": BrainlessDBFeat.UNIQUE})] = None
107
107
  ```
108
108
 
@@ -116,16 +116,16 @@ counter: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX |
116
116
  Data private to each namespace:
117
117
 
118
118
  ```python
119
- from brainlessdb import BrainlessStruct, NestedStruct
119
+ from brainlessdb import BrainlessBucket, BrainlessStruct
120
120
 
121
- class UcsLocal(NestedStruct):
121
+ class UcsLocal(BrainlessStruct):
122
122
  sid: int = 0
123
123
 
124
- class AriLocal(NestedStruct):
124
+ class AriLocal(BrainlessStruct):
125
125
  channel_id: str = ""
126
126
 
127
127
  # Entity with app-local field
128
- class UserV1(BrainlessStruct):
128
+ class UserV1(BrainlessBucket):
129
129
  id: Annotated[int, Meta()]
130
130
  _: Union[UcsLocal, AriLocal, None] = None
131
131
  ```
@@ -1,19 +1,19 @@
1
1
  """Brainless DB - Typed collections backed by NATS JetStream KV."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.2"
4
4
 
5
5
  from typing import Any, Optional, TypeVar
6
6
 
7
7
  from .client import BrainlessDB
8
8
  from .collection import Collection, HasUUID
9
- from .struct import BrainlessDBFeat, BrainlessStruct, NestedStruct, UniqueConstraintError
9
+ from .struct import BrainlessDBFeat, BrainlessBucket, BrainlessStruct, UniqueConstraintError
10
10
 
11
11
  __all__ = [
12
12
  "__version__",
13
13
  "BrainlessDB",
14
14
  "BrainlessDBFeat",
15
+ "BrainlessBucket",
15
16
  "BrainlessStruct",
16
- "NestedStruct",
17
17
  "Collection",
18
18
  "HasUUID",
19
19
  "UniqueConstraintError",
@@ -28,6 +28,10 @@ class Bucket:
28
28
  def __init__(self, kv: Any):
29
29
  self._kv = kv
30
30
 
31
+ @property
32
+ def name(self) -> str:
33
+ return self._kv.name
34
+
31
35
  @classmethod
32
36
  async def create(cls, js: Any, name: str) -> "Bucket":
33
37
  """Create or get existing KV bucket."""
@@ -11,7 +11,7 @@ import msgspec
11
11
  from .bucket import Bucket
12
12
  from .fields import analyze_fields
13
13
  from .struct import (
14
- BrainlessStruct,
14
+ BrainlessBucket,
15
15
  ConfigWrapper,
16
16
  UniqueConstraintError,
17
17
  _register,
@@ -45,8 +45,8 @@ class Collection(Generic[T]):
45
45
  """Collection of structs backed by NATS KV buckets."""
46
46
 
47
47
  def __init__(self, client: "BrainlessDB", cls: type[T]) -> None:
48
- if BrainlessStruct not in cls.__bases__:
49
- raise TypeError(f"{cls.__name__} must directly inherit from BrainlessStruct")
48
+ if BrainlessBucket not in cls.__bases__:
49
+ raise TypeError(f"{cls.__name__} must directly inherit from BrainlessBucket")
50
50
 
51
51
  self._client = client
52
52
  self._cls = cls
@@ -156,8 +156,7 @@ class Collection(Generic[T]):
156
156
  data = {}
157
157
  for f in self._analysis.config_fields:
158
158
  val = getattr(obj, f, None)
159
- if val is not None:
160
- data[f] = msgspec.to_builtins(val) if hasattr(val, "__struct_fields__") else val
159
+ data[f] = msgspec.to_builtins(val) if hasattr(val, "__struct_fields__") else val
161
160
  return data
162
161
 
163
162
  def _to_state(self, obj: T) -> dict[str, Any]:
@@ -202,7 +201,13 @@ class Collection(Generic[T]):
202
201
  async def _load_bucket(bucket: Optional[Bucket]) -> dict[str, dict]:
203
202
  if not bucket:
204
203
  return {}
205
- return {k: msgspec.json.decode(v) for k, v in (await bucket.all()).items()}
204
+ result: dict[str, dict] = {}
205
+ for k, v in (await bucket.all()).items():
206
+ try:
207
+ result[k] = msgspec.json.decode(v)
208
+ except Exception as ex:
209
+ _log.error("Failed to decode key '%s' from bucket '%s': %s", k, bucket.name, ex)
210
+ return result
206
211
 
207
212
  # === CRUD ===
208
213
 
@@ -336,7 +341,11 @@ class Collection(Generic[T]):
336
341
  config_entries: dict[str, tuple[dict, Any]] = {}
337
342
  if self._config_bucket:
338
343
  for uid, raw in (await self._config_bucket.all()).items():
339
- w = msgspec.json.decode(raw, type=ConfigWrapper)
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
340
349
  config_entries[uid] = (w.d, w.m)
341
350
  self._metadata[uid] = w.m
342
351
 
@@ -345,15 +354,20 @@ class Collection(Generic[T]):
345
354
 
346
355
  # Build entities
347
356
  for uid, (cfg, _) in config_entries.items():
348
- obj = self._from_parts(uid, cfg, state.get(uid), local.get(uid))
357
+ try:
358
+ obj = self._from_parts(uid, cfg, state.get(uid), local.get(uid))
359
+ except Exception as ex:
360
+ _log.error("Failed to build entity '%s' in '%s': %s", uid, self.name, ex)
361
+ continue
349
362
  self._entities[uid] = obj
350
363
  self._index_add(obj)
351
364
  _register(obj.uuid, self)
352
365
 
353
366
  self._loaded = True
354
- if config_entries:
355
- _log.info("Loaded %d entities into '%s'", len(config_entries), self.name)
356
- return len(config_entries)
367
+ count = len(self._entities)
368
+ if count:
369
+ _log.info("Loaded %d entities into '%s'", count, self.name)
370
+ return count
357
371
 
358
372
  async def flush(self) -> int:
359
373
  connected = await self._ensure_buckets()
@@ -38,11 +38,11 @@ def _unregister(uuid: UUID) -> None:
38
38
  _registry.pop(uuid, None)
39
39
 
40
40
 
41
- class NestedStruct(Struct, tag=True, tag_field="TYPE"):
41
+ class BrainlessStruct(Struct, tag=True, tag_field="TYPE"):
42
42
  """Base struct for nested/embedded types."""
43
43
 
44
44
 
45
- class BrainlessStruct(Struct, kw_only=True, tag=True, tag_field="TYPE"):
45
+ class BrainlessBucket(Struct, kw_only=True, tag=True, tag_field="TYPE"):
46
46
  """Base struct for brainlessdb entities."""
47
47
 
48
48
  _uuid: UUID = field(default_factory=uuid1)
@@ -57,7 +57,7 @@ class BrainlessStruct(Struct, kw_only=True, tag=True, tag_field="TYPE"):
57
57
  if collection:
58
58
  collection.add(self)
59
59
 
60
- def update(self, data: dict) -> "BrainlessStruct":
60
+ def update(self, data: dict) -> "BrainlessBucket":
61
61
  """Update fields from dict, supports nested structs."""
62
62
  for key, value in data.items():
63
63
  if not hasattr(self, key):
@@ -6,7 +6,7 @@ create-changelog:
6
6
  release: create-changelog
7
7
  awk -i inplace '{ sub(/^version = "[0-9]+\.[0-9]+\.[0-9]+"/, "version = \"{{next_version}}\"") }; { print }' pyproject.toml
8
8
  awk -i inplace '{ sub(/^__version__ = "[0-9]+\.[0-9]+\.[0-9]+"/, "__version__ = \"{{next_version}}\"") }; { print }' brainlessdb/__init__.py
9
- git add --ignore-errors CHANGELOG.md
9
+ git add --ignore-errors CHANGELOG.md pyproject.toml uv.lock brainlessdb/__init__.py
10
10
  git commit -m {{next_version}}
11
11
  git tag -a {{next_version}} -m {{next_version}}
12
12
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brainlessdb"
3
- version = "0.6.0"
3
+ version = "0.7.2"
4
4
  description = "Typed collections backed by NATS JetStream KV"
5
5
  authors = [
6
6
  {name = "INSOFT s.r.o.", email = "helpdesk@insoft.cz"}
@@ -425,14 +425,14 @@ self = <tests.test_deserialization.TestRealWorldUserPattern object at 0x720273f1
425
425
  """Test Optional[Annotated[int, Meta(...)]] pattern like queue_reason."""
426
426
  from enum import IntEnum
427
427
  import msgspec
428
- from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessStruct
428
+ from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessBucket
429
429
  from brainlessdb.struct import ConfigWrapper
430
-
430
+
431
431
  class Status(IntEnum):
432
432
  OFFLINE = 0
433
433
  ONLINE = 1
434
-
435
- class UserLike(BrainlessStruct):
434
+
435
+ class UserLike(BrainlessBucket):
436
436
  id: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX})] = 0
437
437
  group_id: int = 0
438
438
  # Pattern from queue_reason - Optional[Annotated[int, ...]]
@@ -440,7 +440,7 @@ self = <tests.test_deserialization.TestRealWorldUserPattern object at 0x720273f1
440
440
  queue_status_changed: Optional[Annotated[int, Meta(description="")]] = None
441
441
  # State field with enum
442
442
  status: Annotated[Status, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.STATE})] = Status.OFFLINE
443
-
443
+
444
444
  db = BrainlessDB(namespace="test")
445
445
  > coll = await db.collection(UserLike)
446
446
  E TypeError: object Collection can't be used in 'await' expression
@@ -454,17 +454,17 @@ self = <tests.test_deserialization.TestRealWorldUserPattern object at 0x720273f1
454
454
  async def test_nested_struct_pattern(self):
455
455
  """Test nested struct like AgentChannelsV1."""
456
456
  import msgspec
457
- from brainlessdb import BrainlessDB, BrainlessStruct
457
+ from brainlessdb import BrainlessDB, BrainlessBucket
458
458
  from brainlessdb.struct import ConfigWrapper
459
-
459
+
460
460
  class Channels(Struct):
461
461
  voice: int = 0
462
462
  chat: int = 0
463
-
464
- class UserWithChannels(BrainlessStruct):
463
+
464
+ class UserWithChannels(BrainlessBucket):
465
465
  id: int = 0
466
466
  channels: Channels = field(default_factory=Channels)
467
-
467
+
468
468
  db = BrainlessDB(namespace="test")
469
469
  > coll = await db.collection(UserWithChannels)
470
470
  E TypeError: object Collection can't be used in 'await' expression
@@ -478,17 +478,17 @@ self = <tests.test_deserialization.TestRealWorldUserPattern object at 0x720273f1
478
478
  async def test_vcard_list_pattern(self):
479
479
  """Test list of nested structs like vcard: list[VCardV1]."""
480
480
  import msgspec
481
- from brainlessdb import BrainlessDB, BrainlessStruct
481
+ from brainlessdb import BrainlessDB, BrainlessBucket
482
482
  from brainlessdb.struct import ConfigWrapper
483
-
483
+
484
484
  class VCard(Struct):
485
485
  name: str = ""
486
486
  value: str = ""
487
-
488
- class UserWithVCard(BrainlessStruct):
487
+
488
+ class UserWithVCard(BrainlessBucket):
489
489
  id: int = 0
490
490
  vcard: Optional[list[VCard]] = None
491
-
491
+
492
492
  db = BrainlessDB(namespace="test")
493
493
  > coll = await db.collection(UserWithVCard)
494
494
  E TypeError: object Collection can't be used in 'await' expression
@@ -501,18 +501,18 @@ self = <tests.test_deserialization.TestLocalClassDetection object at 0x720273f19
501
501
  @pytest.mark.asyncio
502
502
  async def test_optional_annotated_union_pattern(self):
503
503
  """Test Optional[Annotated[Union[A, B], Meta()]] pattern from UserV2."""
504
- from brainlessdb import BrainlessDB, BrainlessStruct
505
-
504
+ from brainlessdb import BrainlessDB, BrainlessBucket
505
+
506
506
  class AriLocal(Struct):
507
507
  counter: int = 0
508
-
508
+
509
509
  class UcsLocal(Struct):
510
510
  counter: int = 0
511
-
512
- class UserWithUnionLocal(BrainlessStruct):
511
+
512
+ class UserWithUnionLocal(BrainlessBucket):
513
513
  id: int = 0
514
514
  _: Optional[Annotated[Union[UcsLocal, AriLocal], Meta()]] = None
515
-
515
+
516
516
  # Test with "ari" namespace - should find AriLocal
517
517
  db_ari = BrainlessDB(namespace="ari")
518
518
  > coll_ari = await db_ari.collection(UserWithUnionLocal)
@@ -526,15 +526,15 @@ self = <tests.test_deserialization.TestLocalClassDetection object at 0x720273f19
526
526
  @pytest.mark.asyncio
527
527
  async def test_optional_single_class_pattern(self):
528
528
  """Test Optional[LocalClass] pattern."""
529
- from brainlessdb import BrainlessDB, BrainlessStruct
530
-
529
+ from brainlessdb import BrainlessDB, BrainlessBucket
530
+
531
531
  class TestLocal(Struct):
532
532
  value: int = 0
533
-
534
- class UserWithLocal(BrainlessStruct):
533
+
534
+ class UserWithLocal(BrainlessBucket):
535
535
  id: int = 0
536
536
  _: Optional[TestLocal] = None
537
-
537
+
538
538
  db = BrainlessDB(namespace="test")
539
539
  > coll = await db.collection(UserWithLocal)
540
540
  E TypeError: object Collection can't be used in 'await' expression
@@ -547,18 +547,18 @@ self = <tests.test_deserialization.TestLocalClassDetection object at 0x720273f12
547
547
  @pytest.mark.asyncio
548
548
  async def test_union_without_optional_pattern(self):
549
549
  """Test Union[A, B] (without Optional) pattern."""
550
- from brainlessdb import BrainlessDB, BrainlessStruct
551
-
550
+ from brainlessdb import BrainlessDB, BrainlessBucket
551
+
552
552
  class AriData(Struct):
553
553
  x: int = 0
554
-
554
+
555
555
  class UcsData(Struct):
556
556
  x: int = 0
557
-
558
- class UserUnion(BrainlessStruct):
557
+
558
+ class UserUnion(BrainlessBucket):
559
559
  id: int = 0
560
560
  _: Union[UcsData, AriData] = field(default_factory=UcsData)
561
-
561
+
562
562
  db = BrainlessDB(namespace="ari")
563
563
  > coll = await db.collection(UserUnion)
564
564
  E TypeError: object Collection can't be used in 'await' expression
@@ -572,12 +572,12 @@ self = <tests.test_sync.TestMultiLocationSync object at 0x720273f197c0>
572
572
  async def test_two_locations_see_same_data(self):
573
573
  """Both locations can read data written by either."""
574
574
  nats = MockNats()
575
-
575
+
576
576
  db1 = BrainlessDB(nats, namespace="loc1", flush_interval=0)
577
577
  db2 = BrainlessDB(nats, namespace="loc2", flush_interval=0)
578
578
  await db1.start()
579
579
  await db2.start()
580
-
580
+
581
581
  > users1 = await db1.collection(UserV1)
582
582
  E TypeError: object Collection can't be used in 'await' expression
583
583
 
@@ -590,12 +590,12 @@ self = <tests.test_sync.TestMultiLocationSync object at 0x720273f125b0>
590
590
  async def test_watch_receives_remote_changes(self):
591
591
  """Watch fires when other location makes changes."""
592
592
  nats = MockNats()
593
-
593
+
594
594
  db1 = BrainlessDB(nats, namespace="loc1", flush_interval=0)
595
595
  db2 = BrainlessDB(nats, namespace="loc2", flush_interval=0)
596
596
  await db1.start()
597
597
  await db2.start()
598
-
598
+
599
599
  > users1 = await db1.collection(UserV1)
600
600
  E TypeError: object Collection can't be used in 'await' expression
601
601
 
@@ -608,10 +608,10 @@ self = <tests.test_sync.TestMultiLocationSync object at 0x720273eadd00>
608
608
  async def test_location_tracked_in_metadata(self):
609
609
  """Config bucket tracks which location made the change."""
610
610
  nats = MockNats()
611
-
611
+
612
612
  db1 = BrainlessDB(nats, namespace="loc1", flush_interval=0)
613
613
  await db1.start()
614
-
614
+
615
615
  > users1 = await db1.collection(UserV1)
616
616
  E TypeError: object Collection can't be used in 'await' expression
617
617
 
@@ -624,12 +624,12 @@ self = <tests.test_sync.TestMultiLocationSync object at 0x720273ead040>
624
624
  async def test_remote_delete_fires_event(self):
625
625
  """Delete from one location fires event on other."""
626
626
  nats = MockNats()
627
-
627
+
628
628
  db1 = BrainlessDB(nats, namespace="loc1", flush_interval=0)
629
629
  db2 = BrainlessDB(nats, namespace="loc2", flush_interval=0)
630
630
  await db1.start()
631
631
  await db2.start()
632
-
632
+
633
633
  > users1 = await db1.collection(UserV1)
634
634
  E TypeError: object Collection can't be used in 'await' expression
635
635
 
@@ -5,10 +5,10 @@ from typing import Annotated, Optional
5
5
  import pytest
6
6
  from msgspec import Meta
7
7
 
8
- from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessStruct
8
+ from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessBucket
9
9
 
10
10
 
11
- class UserV1(BrainlessStruct):
11
+ class UserV1(BrainlessBucket):
12
12
  """Test user struct with indexed username."""
13
13
 
14
14
  id: Annotated[int, Meta()] = 0
@@ -20,21 +20,21 @@ class UserV1(BrainlessStruct):
20
20
  active: Annotated[bool, Meta()] = True
21
21
 
22
22
 
23
- class ChannelV1(BrainlessStruct):
23
+ class ChannelV1(BrainlessBucket):
24
24
  """Test channel struct."""
25
25
 
26
26
  id: Annotated[str, Meta()] = ""
27
27
  name: Annotated[str, Meta()] = ""
28
28
 
29
29
 
30
- class AgentV1(BrainlessStruct):
30
+ class AgentV1(BrainlessBucket):
31
31
  """Test agent struct."""
32
32
 
33
33
  id: Annotated[int, Meta()] = 0
34
34
  channel: Optional[ChannelV1] = None
35
35
 
36
36
 
37
- class QueueItemV1(BrainlessStruct):
37
+ class QueueItemV1(BrainlessBucket):
38
38
  """Test queue item with nested structs."""
39
39
 
40
40
  inbound_id: Annotated[
@@ -45,7 +45,7 @@ class QueueItemV1(BrainlessStruct):
45
45
  agents: Optional[AgentV1] = None
46
46
 
47
47
 
48
- class UniqueUserV1(BrainlessStruct):
48
+ class UniqueUserV1(BrainlessBucket):
49
49
  """Test struct with UNIQUE field."""
50
50
 
51
51
  id: int = 0
@@ -515,3 +515,99 @@ class TestUnique:
515
515
  unique_users.add(UniqueUserV1(id=1, email=""))
516
516
  with pytest.raises(UniqueConstraintError):
517
517
  unique_users.add(UniqueUserV1(id=2, email=""))
518
+
519
+
520
+ class TestOptionalFieldRoundTrip:
521
+ """Test that Optional fields without defaults survive config round-trip."""
522
+
523
+ @pytest.mark.asyncio
524
+ async def test_none_optional_field_preserved_in_config(self):
525
+ """Optional field set to None must be stored and restored."""
526
+ from typing import Optional
527
+
528
+ from brainlessdb import BrainlessDB, BrainlessBucket
529
+
530
+ class DeviceV1(BrainlessBucket):
531
+ name: str = ""
532
+ host_override: Optional[str] = None
533
+
534
+ db = BrainlessDB(namespace="test")
535
+ devices = db.collection(DeviceV1)
536
+ await db.start()
537
+
538
+ device = DeviceV1(name="phone1")
539
+ devices.add(device)
540
+
541
+ config = devices._to_config(device)
542
+ assert "host_override" in config
543
+ assert config["host_override"] is None
544
+
545
+ restored = devices._from_parts(str(device.uuid), config)
546
+ assert restored.host_override is None
547
+ assert restored.name == "phone1"
548
+
549
+ @pytest.mark.asyncio
550
+ async def test_none_field_survives_json_round_trip(self):
551
+ """Optional[str] = None must survive JSON encode/decode cycle."""
552
+ from typing import Optional
553
+
554
+ import msgspec
555
+
556
+ from brainlessdb import BrainlessDB, BrainlessBucket
557
+ from brainlessdb.struct import ConfigWrapper
558
+
559
+ class DeviceV1(BrainlessBucket):
560
+ name: str = ""
561
+ host_override: Optional[str] = None
562
+
563
+ db = BrainlessDB(namespace="test")
564
+ devices = db.collection(DeviceV1)
565
+ await db.start()
566
+
567
+ device = DeviceV1(name="phone1")
568
+ devices.add(device)
569
+
570
+ # Simulate flush + load cycle
571
+ config = devices._to_config(device)
572
+ wrapper = ConfigWrapper.create(config, "test")
573
+ raw = msgspec.json.encode(wrapper)
574
+ decoded = msgspec.json.decode(raw, type=ConfigWrapper)
575
+
576
+ restored = devices._from_parts(str(device.uuid), decoded.d)
577
+ assert restored.host_override is None
578
+ assert restored.name == "phone1"
579
+
580
+ @pytest.mark.asyncio
581
+ async def test_none_field_with_nats_flush_reload(self):
582
+ """Full flush/reload through mock NATS preserves None fields."""
583
+ from typing import Optional
584
+
585
+ from brainlessdb import BrainlessDB, BrainlessBucket
586
+ from tests.mock_nats import MockNats
587
+
588
+ class DeviceV1(BrainlessBucket):
589
+ name: str = ""
590
+ host_override: Optional[str] = None
591
+
592
+ nats = MockNats()
593
+ db = BrainlessDB(nats, namespace="test", flush_interval=0)
594
+ devices = db.collection(DeviceV1)
595
+ await db.start()
596
+
597
+ device = DeviceV1(name="phone1")
598
+ devices.add(device)
599
+ await db.flush()
600
+ device_uuid = str(device.uuid)
601
+
602
+ # Reload from scratch
603
+ await db.unwatch()
604
+ devices.clear()
605
+ devices._loaded = False
606
+ await devices.load()
607
+
608
+ restored = devices.get(device_uuid)
609
+ assert restored is not None
610
+ assert restored.name == "phone1"
611
+ assert restored.host_override is None
612
+
613
+ await db.stop()
@@ -9,7 +9,7 @@ from typing import Annotated, Optional, Union
9
9
  import pytest
10
10
  from msgspec import Meta, Struct, field
11
11
 
12
- from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessStruct
12
+ from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessBucket
13
13
  from brainlessdb.collection import Collection
14
14
 
15
15
 
@@ -20,7 +20,7 @@ class LocalData(Struct):
20
20
  count: int = 0
21
21
 
22
22
 
23
- class EntityWithIntFields(BrainlessStruct):
23
+ class EntityWithIntFields(BrainlessBucket):
24
24
  """Entity with various int fields in different buckets."""
25
25
 
26
26
  # Config field (no flags)
@@ -48,7 +48,7 @@ class EntityWithIntFields(BrainlessStruct):
48
48
  _: Optional[LocalData] = None
49
49
 
50
50
 
51
- class EntityMixedTypes(BrainlessStruct):
51
+ class EntityMixedTypes(BrainlessBucket):
52
52
  """Entity with mixed types to test type preservation."""
53
53
 
54
54
  int_field: int = 0
@@ -427,14 +427,14 @@ class TestRealWorldUserPattern:
427
427
  """Test Optional[Annotated[int, Meta(...)]] pattern like queue_reason."""
428
428
  from enum import IntEnum
429
429
  import msgspec
430
- from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessStruct
430
+ from brainlessdb import BrainlessDB, BrainlessDBFeat, BrainlessBucket
431
431
  from brainlessdb.struct import ConfigWrapper
432
432
 
433
433
  class Status(IntEnum):
434
434
  OFFLINE = 0
435
435
  ONLINE = 1
436
436
 
437
- class UserLike(BrainlessStruct):
437
+ class UserLike(BrainlessBucket):
438
438
  id: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.INDEX})] = 0
439
439
  group_id: int = 0
440
440
  # Pattern from queue_reason - Optional[Annotated[int, ...]]
@@ -490,14 +490,14 @@ class TestRealWorldUserPattern:
490
490
  async def test_nested_struct_pattern(self):
491
491
  """Test nested struct like AgentChannelsV1."""
492
492
  import msgspec
493
- from brainlessdb import BrainlessDB, BrainlessStruct
493
+ from brainlessdb import BrainlessDB, BrainlessBucket
494
494
  from brainlessdb.struct import ConfigWrapper
495
495
 
496
496
  class Channels(Struct):
497
497
  voice: int = 0
498
498
  chat: int = 0
499
499
 
500
- class UserWithChannels(BrainlessStruct):
500
+ class UserWithChannels(BrainlessBucket):
501
501
  id: int = 0
502
502
  channels: Channels = field(default_factory=Channels)
503
503
 
@@ -526,14 +526,14 @@ class TestRealWorldUserPattern:
526
526
  async def test_vcard_list_pattern(self):
527
527
  """Test list of nested structs like vcard: list[VCardV1]."""
528
528
  import msgspec
529
- from brainlessdb import BrainlessDB, BrainlessStruct
529
+ from brainlessdb import BrainlessDB, BrainlessBucket
530
530
  from brainlessdb.struct import ConfigWrapper
531
531
 
532
532
  class VCard(Struct):
533
533
  name: str = ""
534
534
  value: str = ""
535
535
 
536
- class UserWithVCard(BrainlessStruct):
536
+ class UserWithVCard(BrainlessBucket):
537
537
  id: int = 0
538
538
  vcard: Optional[list[VCard]] = None
539
539
 
@@ -568,7 +568,7 @@ class TestLocalClassDetection:
568
568
  @pytest.mark.asyncio
569
569
  async def test_optional_annotated_union_pattern(self):
570
570
  """Test Optional[Annotated[Union[A, B], Meta()]] pattern from UserV2."""
571
- from brainlessdb import BrainlessDB, BrainlessStruct
571
+ from brainlessdb import BrainlessDB, BrainlessBucket
572
572
 
573
573
  class AriLocal(Struct):
574
574
  counter: int = 0
@@ -576,7 +576,7 @@ class TestLocalClassDetection:
576
576
  class UcsLocal(Struct):
577
577
  counter: int = 0
578
578
 
579
- class UserWithUnionLocal(BrainlessStruct):
579
+ class UserWithUnionLocal(BrainlessBucket):
580
580
  id: int = 0
581
581
  _: Optional[Annotated[Union[UcsLocal, AriLocal], Meta()]] = None
582
582
 
@@ -593,12 +593,12 @@ class TestLocalClassDetection:
593
593
  @pytest.mark.asyncio
594
594
  async def test_optional_single_class_pattern(self):
595
595
  """Test Optional[LocalClass] pattern."""
596
- from brainlessdb import BrainlessDB, BrainlessStruct
596
+ from brainlessdb import BrainlessDB, BrainlessBucket
597
597
 
598
598
  class TestLocal(Struct):
599
599
  value: int = 0
600
600
 
601
- class UserWithLocal(BrainlessStruct):
601
+ class UserWithLocal(BrainlessBucket):
602
602
  id: int = 0
603
603
  _: Optional[TestLocal] = None
604
604
 
@@ -609,7 +609,7 @@ class TestLocalClassDetection:
609
609
  @pytest.mark.asyncio
610
610
  async def test_union_without_optional_pattern(self):
611
611
  """Test Union[A, B] (without Optional) pattern."""
612
- from brainlessdb import BrainlessDB, BrainlessStruct
612
+ from brainlessdb import BrainlessDB, BrainlessBucket
613
613
 
614
614
  class AriData(Struct):
615
615
  x: int = 0
@@ -617,7 +617,7 @@ class TestLocalClassDetection:
617
617
  class UcsData(Struct):
618
618
  x: int = 0
619
619
 
620
- class UserUnion(BrainlessStruct):
620
+ class UserUnion(BrainlessBucket):
621
621
  id: int = 0
622
622
  _: Union[UcsData, AriData] = field(default_factory=UcsData)
623
623
 
@@ -4,7 +4,7 @@ from typing import Annotated, Optional, Union
4
4
 
5
5
  from msgspec import Meta, Struct
6
6
 
7
- from brainlessdb import BrainlessDBFeat, BrainlessStruct
7
+ from brainlessdb import BrainlessDBFeat, BrainlessBucket
8
8
  from brainlessdb.fields import analyze_fields
9
9
 
10
10
 
@@ -17,14 +17,14 @@ class AriV1(Struct):
17
17
  channel_id: str = ""
18
18
 
19
19
 
20
- class SimpleV1(BrainlessStruct):
20
+ class SimpleV1(BrainlessBucket):
21
21
  """Simple struct with no state or app-local."""
22
22
 
23
23
  id: Annotated[int, Meta()] = 0
24
24
  name: Annotated[str, Meta()] = ""
25
25
 
26
26
 
27
- class WithStateV1(BrainlessStruct):
27
+ class WithStateV1(BrainlessBucket):
28
28
  """Struct with state fields."""
29
29
 
30
30
  id: Annotated[int, Meta()] = 0
@@ -39,21 +39,21 @@ class WithStateV1(BrainlessStruct):
39
39
  ] = 0
40
40
 
41
41
 
42
- class WithLocalV1(BrainlessStruct):
42
+ class WithLocalV1(BrainlessBucket):
43
43
  """Struct with app-local field."""
44
44
 
45
45
  id: Annotated[int, Meta()] = 0
46
46
  _: Optional[UcsV1] = None
47
47
 
48
48
 
49
- class WithUnionLocalV1(BrainlessStruct):
49
+ class WithUnionLocalV1(BrainlessBucket):
50
50
  """Struct with Union app-local field."""
51
51
 
52
52
  id: Annotated[int, Meta()] = 0
53
53
  _: Union[UcsV1, AriV1, None] = None
54
54
 
55
55
 
56
- class FullV1(BrainlessStruct):
56
+ class FullV1(BrainlessBucket):
57
57
  """Struct with config, state, and app-local."""
58
58
 
59
59
  id: Annotated[int, Meta()] = 0
@@ -0,0 +1,274 @@
1
+ """Test graceful error handling during bucket deserialization.
2
+
3
+ Verifies that corrupt items in buckets are skipped with a logged error
4
+ while valid items still load successfully.
5
+ """
6
+
7
+ import logging
8
+ from uuid import uuid1
9
+
10
+ import msgspec
11
+ import pytest
12
+
13
+ from brainlessdb import BrainlessDB
14
+ from brainlessdb.struct import ConfigWrapper
15
+ from tests.conftest import UserV1
16
+ from tests.mock_nats import MockNats
17
+
18
+
19
+ @pytest.fixture
20
+ def nats():
21
+ return MockNats()
22
+
23
+
24
+ @pytest.fixture
25
+ async def db_and_users(nats):
26
+ """Create a DB with UserV1 collection connected to mock NATS."""
27
+ db = BrainlessDB(nats, namespace="test", flush_interval=0)
28
+ users = db.collection(UserV1)
29
+ return db, users
30
+
31
+
32
+ class TestCorruptConfigBucket:
33
+ """Test load() skips corrupt entries in config bucket."""
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_corrupt_config_entry_skipped(self, nats, db_and_users, caplog):
37
+ db, users = db_and_users
38
+ await db.start()
39
+
40
+ # Add a valid user and flush
41
+ valid = UserV1(id=1, username="alice")
42
+ users.add(valid)
43
+ await db.flush()
44
+ valid_uuid = str(valid.uuid)
45
+
46
+ # Stop watch to avoid interference during reload
47
+ await db.unwatch()
48
+
49
+ # Inject corrupt data directly into config bucket
50
+ js = nats.jetstream()
51
+ kv = await js.key_value("UserV1")
52
+ await kv.put("corrupt-uuid-1", b"not valid json {{{")
53
+
54
+ # Reload collection
55
+ users.clear()
56
+ users._loaded = False
57
+
58
+ with caplog.at_level(logging.ERROR, logger="brainlessdb.collection"):
59
+ count = await users.load()
60
+
61
+ assert count == 1
62
+ assert users.get(valid_uuid) is not None
63
+ assert users.get("corrupt-uuid-1") is None
64
+ assert "Failed to decode config for 'corrupt-uuid-1'" in caplog.text
65
+
66
+ await db.stop()
67
+
68
+ @pytest.mark.asyncio
69
+ async def test_all_corrupt_config_loads_zero(self, nats, db_and_users, caplog):
70
+ db, users = db_and_users
71
+
72
+ # Inject only corrupt data before start
73
+ js = nats.jetstream()
74
+ kv = await js.create_key_value(type("C", (), {"bucket": "UserV1"})())
75
+ await kv.put("bad-1", b"???")
76
+ await kv.put("bad-2", b"{invalid")
77
+
78
+ with caplog.at_level(logging.ERROR, logger="brainlessdb.collection"):
79
+ await db.start()
80
+
81
+ assert len(users) == 0
82
+ assert "Failed to decode config" in caplog.text
83
+
84
+ await db.stop()
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_valid_entries_survive_alongside_corrupt(self, nats, db_and_users, caplog):
88
+ db, users = db_and_users
89
+ await db.start()
90
+
91
+ # Flush two valid users
92
+ u1 = UserV1(id=1, username="alice")
93
+ u2 = UserV1(id=2, username="bob")
94
+ users.add(u1)
95
+ users.add(u2)
96
+ await db.flush()
97
+
98
+ # Stop watch to avoid interference during reload
99
+ await db.unwatch()
100
+
101
+ # Inject corrupt entry
102
+ js = nats.jetstream()
103
+ kv = await js.key_value("UserV1")
104
+ await kv.put("corrupt-1", b"nope")
105
+
106
+ # Reload
107
+ users.clear()
108
+ users._loaded = False
109
+ with caplog.at_level(logging.ERROR, logger="brainlessdb.collection"):
110
+ count = await users.load()
111
+
112
+ assert count == 2
113
+ assert users.find(username="alice") is not None
114
+ assert users.find(username="bob") is not None
115
+
116
+ await db.stop()
117
+
118
+
119
+ class TestCorruptStateBucket:
120
+ """Test load() skips corrupt entries in state bucket."""
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_corrupt_state_entry_skipped(self, nats, caplog):
124
+ from typing import Annotated
125
+
126
+ from msgspec import Meta
127
+
128
+ from brainlessdb import BrainlessDBFeat, BrainlessBucket
129
+
130
+ class AgentV1(BrainlessBucket):
131
+ id: int = 0
132
+ status: Annotated[int, Meta(extra={"brainlessdb_flags": BrainlessDBFeat.STATE})] = 0
133
+
134
+ db = BrainlessDB(nats, namespace="test", flush_interval=0)
135
+ agents = db.collection(AgentV1)
136
+ await db.start()
137
+
138
+ # Add valid agent and flush
139
+ a1 = AgentV1(id=1, status=1)
140
+ agents.add(a1)
141
+ await db.flush()
142
+ a1_uuid = str(a1.uuid)
143
+
144
+ # Stop watch to avoid interference
145
+ await db.unwatch()
146
+
147
+ # Inject corrupt state data
148
+ js = nats.jetstream()
149
+ kv = await js.key_value("AgentV1-State")
150
+ await kv.put("corrupt-state-uuid", b"not json!!!")
151
+
152
+ # Reload
153
+ agents.clear()
154
+ agents._loaded = False
155
+ with caplog.at_level(logging.ERROR, logger="brainlessdb.collection"):
156
+ count = await agents.load()
157
+
158
+ assert count == 1
159
+ assert agents.get(a1_uuid) is not None
160
+ assert "Failed to decode key 'corrupt-state-uuid' from bucket 'AgentV1-State'" in caplog.text
161
+
162
+ await db.stop()
163
+
164
+
165
+ class TestCorruptLocalBucket:
166
+ """Test load() skips corrupt entries in local bucket."""
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_corrupt_local_entry_skipped(self, nats, caplog):
170
+ from typing import Optional
171
+
172
+ from msgspec import Struct
173
+
174
+ from brainlessdb import BrainlessDB, BrainlessBucket
175
+
176
+ class TestLocal(Struct):
177
+ sid: int = 0
178
+
179
+ class ItemV1(BrainlessBucket):
180
+ id: int = 0
181
+ _: Optional[TestLocal] = None
182
+
183
+ db = BrainlessDB(nats, namespace="test", flush_interval=0)
184
+ items = db.collection(ItemV1)
185
+ await db.start()
186
+
187
+ # Add valid item
188
+ i1 = ItemV1(id=1, _=TestLocal(sid=42))
189
+ items.add(i1)
190
+ await db.flush()
191
+
192
+ # Stop watch to avoid interference
193
+ await db.unwatch()
194
+
195
+ # Inject corrupt local data
196
+ js = nats.jetstream()
197
+ kv = await js.key_value("ItemV1-TestLocal")
198
+ await kv.put("corrupt-local-uuid", b"{{bad}}")
199
+
200
+ # Reload
201
+ items.clear()
202
+ items._loaded = False
203
+ with caplog.at_level(logging.ERROR, logger="brainlessdb.collection"):
204
+ count = await items.load()
205
+
206
+ assert count == 1
207
+ assert items.get(str(i1.uuid)) is not None
208
+ assert "Failed to decode key 'corrupt-local-uuid' from bucket 'ItemV1-TestLocal'" in caplog.text
209
+
210
+ await db.stop()
211
+
212
+
213
+ class TestCorruptEntityAssembly:
214
+ """Test load() skips entities that fail during _from_parts assembly."""
215
+
216
+ @pytest.mark.asyncio
217
+ async def test_invalid_config_data_skipped(self, nats, db_and_users, caplog):
218
+ """Config data that decodes as JSON but fails struct conversion."""
219
+ db, users = db_and_users
220
+
221
+ js = nats.jetstream()
222
+ kv = await js.create_key_value(type("C", (), {"bucket": "UserV1"})())
223
+
224
+ # Valid ConfigWrapper but with data that will fail msgspec.convert
225
+ bad_uuid = str(uuid1())
226
+ bad_wrapper = ConfigWrapper.create({"id": "not-an-int", "username": [1, 2, 3]}, "test")
227
+ await kv.put(bad_uuid, msgspec.json.encode(bad_wrapper))
228
+
229
+ # Valid entry with proper UUID and data
230
+ good_uuid = str(uuid1())
231
+ good_wrapper = ConfigWrapper.create({"id": 1, "username": "alice"}, "test")
232
+ await kv.put(good_uuid, msgspec.json.encode(good_wrapper))
233
+
234
+ with caplog.at_level(logging.ERROR, logger="brainlessdb.collection"):
235
+ await db.start()
236
+
237
+ assert len(users) == 1
238
+ assert users.get(good_uuid) is not None
239
+ assert users.get(bad_uuid) is None
240
+ assert f"Failed to build entity '{bad_uuid}'" in caplog.text
241
+
242
+ await db.stop()
243
+
244
+ @pytest.mark.asyncio
245
+ async def test_mix_of_all_error_types(self, nats, db_and_users, caplog):
246
+ """Corrupt config JSON, invalid assembly, and valid entries all together."""
247
+ db, users = db_and_users
248
+
249
+ js = nats.jetstream()
250
+ kv = await js.create_key_value(type("C", (), {"bucket": "UserV1"})())
251
+
252
+ # 1. Corrupt JSON - fails at decode
253
+ await kv.put("corrupt-json", b"not json")
254
+
255
+ # 2. Valid JSON, bad schema - fails at entity assembly
256
+ bad_uuid = str(uuid1())
257
+ bad_wrapper = ConfigWrapper.create({"id": [1, 2], "username": {"nested": True}}, "test")
258
+ await kv.put(bad_uuid, msgspec.json.encode(bad_wrapper))
259
+
260
+ # 3. Valid entry
261
+ good_uuid = str(uuid1())
262
+ good_wrapper = ConfigWrapper.create({"id": 42, "username": "valid"}, "test")
263
+ await kv.put(good_uuid, msgspec.json.encode(good_wrapper))
264
+
265
+ with caplog.at_level(logging.ERROR, logger="brainlessdb.collection"):
266
+ await db.start()
267
+
268
+ assert len(users) == 1
269
+ assert users.get(good_uuid) is not None
270
+ assert users.get(good_uuid).username == "valid"
271
+ assert "Failed to decode config for 'corrupt-json'" in caplog.text
272
+ assert f"Failed to build entity '{bad_uuid}'" in caplog.text
273
+
274
+ await db.stop()
@@ -17,7 +17,7 @@ wheels = [
17
17
 
18
18
  [[package]]
19
19
  name = "brainlessdb"
20
- version = "0.4.1"
20
+ version = "0.7.0"
21
21
  source = { editable = "." }
22
22
  dependencies = [
23
23
  { name = "msgspec" },
File without changes
File without changes
File without changes