brainlessdb 0.17.3__tar.gz → 0.17.4__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 (36) hide show
  1. brainlessdb-0.17.4/.python-version +1 -0
  2. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/CHANGELOG.md +9 -0
  3. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/PKG-INFO +2 -2
  4. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/brainlessdb/__init__.py +1 -1
  5. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/brainlessdb/registry.py +7 -0
  6. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/brainlessdb/struct.py +4 -1
  7. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/pyproject.toml +1 -1
  8. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_registry.py +15 -0
  9. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/uv.lock +23 -23
  10. brainlessdb-0.17.3/.python-version +0 -1
  11. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/.gitignore +0 -0
  12. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/README.md +0 -0
  13. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/brainlessdb/bucket.py +0 -0
  14. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/brainlessdb/client.py +0 -0
  15. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/brainlessdb/collection.py +0 -0
  16. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/brainlessdb/fields.py +0 -0
  17. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/cliff.toml +0 -0
  18. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/code_style_guide.md +0 -0
  19. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/justfile +0 -0
  20. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/test_results.txt +0 -0
  21. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/__init__.py +0 -0
  22. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/conftest.py +0 -0
  23. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/mock_nats.py +0 -0
  24. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_bucket.py +0 -0
  25. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_client.py +0 -0
  26. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_collection.py +0 -0
  27. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_dead_letter.py +0 -0
  28. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_deserialization.py +0 -0
  29. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_duplicates.py +0 -0
  30. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_fields.py +0 -0
  31. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_flush_errors.py +0 -0
  32. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_load_errors.py +0 -0
  33. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_sync.py +0 -0
  34. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_thread_safety.py +0 -0
  35. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_update_atomic.py +0 -0
  36. {brainlessdb-0.17.3 → brainlessdb-0.17.4}/tests/test_version.py +0 -0
@@ -0,0 +1 @@
1
+ 3.14
@@ -3,6 +3,15 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
 
6
+ ## [0.17.4] - 2026-11-03
7
+
8
+ ### 🐛 Bug Fixes
9
+
10
+ - Validate registry on save so invalid values are never flushed
11
+ - Plain attribute assignment is not type-checked by msgspec, so e.g. None in
12
+ a non-Optional field was flushed and every reader dead-lettered the entry.
13
+
14
+
6
15
  ## [0.17.3] - 2026-10-21
7
16
 
8
17
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: brainlessdb
3
- Version: 0.17.3
3
+ Version: 0.17.4
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.17.3"
3
+ __version__ = "0.17.4"
4
4
 
5
5
  from typing import Any, Optional, TypeVar
6
6
 
@@ -81,6 +81,13 @@ class Registry(Generic[T]):
81
81
  # === Dirty / save ===
82
82
 
83
83
  def mark_dirty(self) -> None:
84
+ """Validate the instance and schedule it for flush.
85
+
86
+ Plain attribute assignment is not type-checked by msgspec, so an invalid
87
+ value (e.g. None for a non-Optional field) is caught here and raises
88
+ msgspec.ValidationError before it can be flushed and break every reader.
89
+ """
90
+ self._apply(self._to_data())
84
91
  self._dirty = True
85
92
  self._client.schedule_flush()
86
93
 
@@ -205,7 +205,10 @@ class BrainlessRegistry(Struct, kw_only=True, tag=True, tag_field="TYPE", dict=T
205
205
  return uuid5(_REGISTRY_NS, type(self).__key__)
206
206
 
207
207
  def save(self) -> None:
208
- """Mark this registry entry dirty in its Registry."""
208
+ """Validate and mark this registry entry dirty in its Registry.
209
+
210
+ Raises msgspec.ValidationError when a field holds a value of wrong type.
211
+ """
209
212
  owner_ref = getattr(self, "_owner", None)
210
213
  owner = owner_ref() if owner_ref is not None else None
211
214
  if owner is not None:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brainlessdb"
3
- version = "0.17.3"
3
+ version = "0.17.4"
4
4
  description = "Typed collections backed by NATS JetStream KV"
5
5
  authors = [
6
6
  {name = "INSOFT s.r.o.", email = "helpdesk@insoft.cz"}
@@ -62,6 +62,21 @@ class TestRegistryBasics:
62
62
  class TestRegistryPersistence:
63
63
  """Save / reload through the shared bucket."""
64
64
 
65
+ @pytest.mark.asyncio
66
+ async def test_save_rejects_invalid_value(self):
67
+ nats = MockNats()
68
+ db = BrainlessDB(nats, namespace="loc1", flush_interval=0)
69
+ sip = db.registry(SipConfigV1)
70
+ await db.start()
71
+
72
+ sip.host = None # not Optional
73
+ with pytest.raises(msgspec.ValidationError):
74
+ sip.save()
75
+ assert await db.flush() == 0
76
+ assert SipConfigV1.storage_key() not in await nats.jetstream()._buckets[REGISTRY_BUCKET].keys()
77
+
78
+ await db.stop()
79
+
65
80
  @pytest.mark.asyncio
66
81
  async def test_save_and_reload(self):
67
82
  nats = MockNats()
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 5
3
3
  requires-python = ">=3.9"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.10'",
@@ -17,7 +17,7 @@ wheels = [
17
17
 
18
18
  [[package]]
19
19
  name = "brainlessdb"
20
- version = "0.17.2"
20
+ version = "0.17.3"
21
21
  source = { editable = "." }
22
22
  dependencies = [
23
23
  { name = "msgspec" },
@@ -60,7 +60,7 @@ name = "exceptiongroup"
60
60
  version = "1.3.1"
61
61
  source = { registry = "https://pypi.org/simple" }
62
62
  dependencies = [
63
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
63
+ { name = "typing-extensions" },
64
64
  ]
65
65
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
66
66
  wheels = [
@@ -196,13 +196,13 @@ resolution-markers = [
196
196
  "python_full_version < '3.10'",
197
197
  ]
198
198
  dependencies = [
199
- { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
200
- { name = "exceptiongroup", marker = "python_full_version < '3.10'" },
201
- { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
202
- { name = "packaging", marker = "python_full_version < '3.10'" },
203
- { name = "pluggy", marker = "python_full_version < '3.10'" },
204
- { name = "pygments", marker = "python_full_version < '3.10'" },
205
- { name = "tomli", marker = "python_full_version < '3.10'" },
199
+ { name = "colorama", marker = "sys_platform == 'win32'" },
200
+ { name = "exceptiongroup" },
201
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } },
202
+ { name = "packaging" },
203
+ { name = "pluggy" },
204
+ { name = "pygments" },
205
+ { name = "tomli" },
206
206
  ]
207
207
  sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
208
208
  wheels = [
@@ -217,13 +217,13 @@ resolution-markers = [
217
217
  "python_full_version >= '3.10'",
218
218
  ]
219
219
  dependencies = [
220
- { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
221
- { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
222
- { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
223
- { name = "packaging", marker = "python_full_version >= '3.10'" },
224
- { name = "pluggy", marker = "python_full_version >= '3.10'" },
225
- { name = "pygments", marker = "python_full_version >= '3.10'" },
226
- { name = "tomli", marker = "python_full_version == '3.10.*'" },
220
+ { name = "colorama", marker = "sys_platform == 'win32'" },
221
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
222
+ { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" } },
223
+ { name = "packaging" },
224
+ { name = "pluggy" },
225
+ { name = "pygments" },
226
+ { name = "tomli", marker = "python_full_version < '3.11'" },
227
227
  ]
228
228
  sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
229
229
  wheels = [
@@ -238,9 +238,9 @@ resolution-markers = [
238
238
  "python_full_version < '3.10'",
239
239
  ]
240
240
  dependencies = [
241
- { name = "backports-asyncio-runner", marker = "python_full_version < '3.10'" },
242
- { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
243
- { name = "typing-extensions", marker = "python_full_version < '3.10'" },
241
+ { name = "backports-asyncio-runner" },
242
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" } },
243
+ { name = "typing-extensions" },
244
244
  ]
245
245
  sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" }
246
246
  wheels = [
@@ -255,9 +255,9 @@ resolution-markers = [
255
255
  "python_full_version >= '3.10'",
256
256
  ]
257
257
  dependencies = [
258
- { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*'" },
259
- { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
260
- { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" },
258
+ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
259
+ { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" } },
260
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
261
261
  ]
262
262
  sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
263
263
  wheels = [
@@ -1 +0,0 @@
1
- 3.9
File without changes
File without changes
File without changes
File without changes