brainlessdb 1.0.0__tar.gz → 1.2.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 (37) hide show
  1. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/CHANGELOG.md +25 -0
  2. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/PKG-INFO +1 -1
  3. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/__init__.py +1 -1
  4. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/client.py +8 -3
  5. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/ingest.py +13 -6
  6. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/struct.py +8 -7
  7. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/pyproject.toml +1 -1
  8. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_client.py +22 -0
  9. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_ingest.py +20 -2
  10. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/uv.lock +1 -1
  11. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/.gitignore +0 -0
  12. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/.python-version +0 -0
  13. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/README.md +0 -0
  14. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/bucket.py +0 -0
  15. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/collection.py +0 -0
  16. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/fields.py +0 -0
  17. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/brainlessdb/registry.py +0 -0
  18. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/cliff.toml +0 -0
  19. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/code_style_guide.md +0 -0
  20. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/justfile +0 -0
  21. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/test_results.txt +0 -0
  22. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/__init__.py +0 -0
  23. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/conftest.py +0 -0
  24. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/mock_nats.py +0 -0
  25. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_bucket.py +0 -0
  26. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_collection.py +0 -0
  27. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_dead_letter.py +0 -0
  28. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_deserialization.py +0 -0
  29. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_duplicates.py +0 -0
  30. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_fields.py +0 -0
  31. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_flush_errors.py +0 -0
  32. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_load_errors.py +0 -0
  33. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_registry.py +0 -0
  34. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_sync.py +0 -0
  35. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_thread_safety.py +0 -0
  36. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_update_atomic.py +0 -0
  37. {brainlessdb-1.0.0 → brainlessdb-1.2.0}/tests/test_version.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
+ ## [1.2.0] - 2026-11-15
7
+
8
+ ### 🚀 Features
9
+
10
+ - Plain __allowed_apps__ list grants access to a collection
11
+ - A class without allows_app() but with __allowed_apps__ (a list of app
12
+ namespaces) is allowed when the namespace is in the list. ucs_data
13
+ buckets declare only __allowed_apps__ and were all rejected with
14
+ AppAccessError. allows_app() still takes precedence.
15
+
16
+
17
+ ## [1.1.0] - 2026-11-03
18
+
19
+ ### 🚀 Features
20
+
21
+ - Ingest() reports unknown keys
22
+ - Top level keys which are not fields of the struct were silently left out,
23
+ so a typo or a renamed field went unnoticed. They are still left out, but
24
+ now reported as Repair with action "ignored".
25
+
26
+ ### ⚙️ Miscellaneous Tasks
27
+
28
+ - Update uv.lock to 1.0.0
29
+
30
+
6
31
  ## [1.0.0] - 2026-11-03
7
32
 
8
33
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: brainlessdb
3
- Version: 1.0.0
3
+ Version: 1.2.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__ = "1.0.0"
3
+ __version__ = "1.2.0"
4
4
 
5
5
  from typing import Any, Optional, TypeVar
6
6
 
@@ -87,11 +87,16 @@ class BrainlessDB:
87
87
  """Validate __allowed_apps__. Errors are deferred to start() before
88
88
  start, raised immediately after."""
89
89
  allows_app = getattr(cls, "allows_app", None)
90
+ allowed_apps = getattr(cls, "__allowed_apps__", None)
90
91
  error = None
91
- if allows_app is None:
92
+ if allows_app is not None:
93
+ if not allows_app(self._namespace):
94
+ error = AppAccessError(cls.__name__, self._namespace)
95
+ elif allowed_apps is not None:
96
+ if self._namespace not in allowed_apps:
97
+ error = AppAccessError(cls.__name__, self._namespace)
98
+ else:
92
99
  error = AppAccessError(cls.__name__)
93
- elif not allows_app(self._namespace):
94
- error = AppAccessError(cls.__name__, self._namespace)
95
100
 
96
101
  if error is not None:
97
102
  if self._started:
@@ -13,9 +13,10 @@ _MISSING = object()
13
13
  class Repair(Struct, frozen=True):
14
14
  """One value ingest() could not store as given.
15
15
 
16
- action is "kept" (current value kept), "default" (field default used) or
17
- "removed" (list item or dict entry left out). The invalid value itself is
18
- not recorded, it may be a password or key.
16
+ action is "kept" (current value kept), "default" (field default used),
17
+ "removed" (list item or dict entry left out) or "ignored" (top level key
18
+ is not a field of the struct). The invalid value itself is not recorded,
19
+ it may be a password or key.
19
20
  """
20
21
 
21
22
  path: Path
@@ -201,15 +202,21 @@ def repair(
201
202
  current is the stored item as builtins: invalid values fall back to it, except
202
203
  inside lists and dicts. Values are coerced like msgspec.convert(strict=False),
203
204
  e.g. "5060" to 5060. Raises msgspec.ValidationError when data can't be repaired,
204
- e.g. a required field without current value is invalid.
205
+ e.g. a required field without current value is invalid. Top level keys
206
+ which are not fields of cls are left out and reported as "ignored".
205
207
  """
206
- repairs: list[Repair] = []
208
+ fields = {field.encode_name: field for field in msgspec.structs.fields(cls)}
209
+ tag_field = cls.__struct_config__.tag_field
210
+ repairs = [
211
+ Repair((key,), "ignored", f"Object has unknown field `{key}`")
212
+ for key in data
213
+ if key not in fields and key != tag_field
214
+ ]
207
215
  try:
208
216
  fixed = _repair_struct(data, cls, current, (), repairs, partial=partial)
209
217
  except _Unrepairable as ex:
210
218
  raise msgspec.ValidationError(ex.error) from None
211
219
 
212
- fields = {field.encode_name: field for field in msgspec.structs.fields(cls)}
213
220
  converted = {
214
221
  key: msgspec.to_builtins(msgspec.convert(value, fields[key].type, strict=False))
215
222
  for key, value in fixed.items()
@@ -172,11 +172,12 @@ class BrainlessBucket(Struct, kw_only=True, tag=True, tag_field="TYPE"):
172
172
  """Like update(), but repair invalid values instead of raising.
173
173
 
174
174
  An invalid value keeps the current one; inside lists and dicts the invalid
175
- item is removed (items are not matched by position). Values are coerced,
176
- e.g. "5060" to 5060. Returns the repairs made. Raises like update() when
177
- data can't be repaired or violates a unique constraint.
175
+ item is removed (items are not matched by position). Unknown keys are
176
+ ignored and reported. Values are coerced, e.g. "5060" to 5060. Returns
177
+ the repairs made. Raises like update() when data can't be repaired or
178
+ violates a unique constraint.
178
179
  """
179
- fixed, repairs = repair(_merge_nested(self, data), type(self), to_builtins(self), partial=True)
180
+ fixed, repairs = repair({**data, **_merge_nested(self, data)}, type(self), to_builtins(self), partial=True)
180
181
  self.update(fixed)
181
182
  return repairs
182
183
 
@@ -227,11 +228,11 @@ class BrainlessRegistry(Struct, kw_only=True, tag=True, tag_field="TYPE", dict=T
227
228
  def ingest(self, data: dict) -> list[Repair]:
228
229
  """Set fields from data and save, repairing invalid values instead of raising.
229
230
 
230
- An invalid value keeps the current one. Values are coerced, e.g. "5060"
231
- to 5060. Returns the repairs made.
231
+ An invalid value keeps the current one, unknown keys are ignored and
232
+ reported. Values are coerced, e.g. "5060" to 5060. Returns the repairs made.
232
233
  """
233
234
  current = to_builtins(self)
234
- fixed, repairs = repair(_merge_nested(self, data), type(self), current, partial=True)
235
+ fixed, repairs = repair({**data, **_merge_nested(self, data)}, type(self), current, partial=True)
235
236
  new = convert({**current, **fixed}, type(self))
236
237
  for f in self.__struct_fields__:
237
238
  setattr(self, f, getattr(new, f))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brainlessdb"
3
- version = "1.0.0"
3
+ version = "1.2.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"}
@@ -123,6 +123,28 @@ class TestAppAccess:
123
123
  with pytest.raises(RuntimeError, match="must declare __allowed_apps__"):
124
124
  await db.start()
125
125
 
126
+ def test_collection_allowed_apps_attribute(self):
127
+ """__allowed_apps__ without allows_app - namespace in the list is allowed."""
128
+ from brainlessdb import BrainlessBucket
129
+
130
+ class ListedV1(BrainlessBucket):
131
+ __allowed_apps__ = ["test", "other"]
132
+
133
+ db = BrainlessDB(namespace="test")
134
+ db.collection(ListedV1)
135
+ assert db._app_errors == []
136
+
137
+ def test_collection_allowed_apps_attribute_wrong_app(self):
138
+ """__allowed_apps__ without allows_app - namespace not in the list is an error."""
139
+ from brainlessdb import BrainlessBucket
140
+
141
+ class ListedV1(BrainlessBucket):
142
+ __allowed_apps__ = ["other"]
143
+
144
+ db = BrainlessDB(namespace="test")
145
+ db.collection(ListedV1)
146
+ assert len(db._app_errors) == 1
147
+
126
148
  @pytest.mark.asyncio
127
149
  async def test_collection_after_start_wrong_app_raises_immediately(self):
128
150
  """Wrong namespace after start — raises immediately."""
@@ -60,6 +60,12 @@ class TestCollectionIngestAdd:
60
60
  assert item.vlan == 10
61
61
  assert phones.find(id=1) is item
62
62
 
63
+ @pytest.mark.asyncio
64
+ async def test_reports_unknown_keys(self, phones):
65
+ item, repairs = phones.ingest({"id": 1, "name": "a", "unknown": 1}, key="id")
66
+ assert item.name == "a"
67
+ assert actions(repairs) == {("unknown",): "ignored"}
68
+
63
69
  @pytest.mark.asyncio
64
70
  async def test_invalid_field_gets_default(self, phones):
65
71
  item, repairs = phones.ingest({"id": 1, "name": "a", "mac": "nope", "vlan": 5000}, key="id")
@@ -147,9 +153,10 @@ class TestItemIngest:
147
153
  assert actions(repairs) == {("vlan",): "kept"}
148
154
 
149
155
  @pytest.mark.asyncio
150
- async def test_ignores_unknown_keys(self, phones):
156
+ async def test_reports_unknown_keys(self, phones):
151
157
  item, _ = phones.ingest({"id": 1, "name": "a"}, key="id")
152
- assert item.ingest({"unknown": 1}) == []
158
+ assert actions(item.ingest({"unknown": 1})) == {("unknown",): "ignored"}
159
+ assert phones.find(id=1).name == "a"
153
160
 
154
161
 
155
162
  class TestRegistryIngest:
@@ -164,6 +171,17 @@ class TestRegistryIngest:
164
171
  assert actions(repairs) == {("host",): "kept"}
165
172
  await db.stop()
166
173
 
174
+ @pytest.mark.asyncio
175
+ async def test_reports_unknown_keys(self):
176
+ db = BrainlessDB(namespace="test")
177
+ sip = db.registry(SipConfigV1)
178
+ await db.start()
179
+
180
+ repairs = sip.ingest({"hots": "10.0.0.1", "port": 5070})
181
+ assert (sip.host, sip.port) == ("127.0.0.1", 5070)
182
+ assert actions(repairs) == {("hots",): "ignored"}
183
+ await db.stop()
184
+
167
185
  @pytest.mark.asyncio
168
186
  async def test_marks_dirty(self):
169
187
  db = BrainlessDB(namespace="test")
@@ -17,7 +17,7 @@ wheels = [
17
17
 
18
18
  [[package]]
19
19
  name = "brainlessdb"
20
- version = "0.17.4"
20
+ version = "1.0.0"
21
21
  source = { editable = "." }
22
22
  dependencies = [
23
23
  { name = "msgspec" },
File without changes
File without changes
File without changes
File without changes
File without changes