tigrbl_engine_numpy 0.1.1.dev3__tar.gz → 0.1.1.dev5__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.
@@ -30,9 +30,13 @@ pkgs/standards/peagen/.pymon
30
30
  gateway.db
31
31
  kms.db
32
32
  *.asc
33
+ *.kid
33
34
  *.so
34
35
  *.db
35
36
  target/
36
37
  !.gitkeep # keep the empty dir in repo
37
38
  pkgs/experimental/swarmakit/libs/svelte/.vscode/extensions.json
38
39
  node_modules/
40
+ *.zip
41
+ .pymon
42
+ /.tmp_pydeps
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrbl_engine_numpy
3
- Version: 0.1.1.dev3
3
+ Version: 0.1.1.dev5
4
4
  Summary: NumPy engine plugin for tigrbl with array-to-table helpers.
5
5
  Project-URL: Homepage, https://github.com/swarmauri/swarmauri-sdk
6
6
  Author-email: Jacob Stewart <jacob@swarmauri.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tigrbl_engine_numpy"
7
- version = "0.1.1.dev3"
7
+ version = "0.1.1.dev5"
8
8
  description = "NumPy engine plugin for tigrbl with array-to-table helpers."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -16,7 +16,49 @@ from typing import (
16
16
  )
17
17
 
18
18
  import numpy as np
19
- from tigrbl.session.base import TigrblSessionBase
19
+
20
+ try:
21
+ from tigrbl.session.base import TigrblSessionBase
22
+ except Exception:
23
+ from abc import ABC, abstractmethod
24
+
25
+ class TigrblSessionBase(ABC):
26
+ def __init__(self, spec=None):
27
+ self._spec = spec
28
+
29
+ def apply_spec(self, spec):
30
+ self._spec = spec
31
+
32
+ @abstractmethod
33
+ def _add_impl(self, obj): ...
34
+
35
+ @abstractmethod
36
+ async def _delete_impl(self, obj): ...
37
+
38
+ @abstractmethod
39
+ async def _flush_impl(self): ...
40
+
41
+ @abstractmethod
42
+ async def _refresh_impl(self, obj): ...
43
+
44
+ @abstractmethod
45
+ async def _get_impl(self, model, ident): ...
46
+
47
+ @abstractmethod
48
+ async def _execute_impl(self, stmt): ...
49
+
50
+ @abstractmethod
51
+ async def _tx_begin_impl(self): ...
52
+
53
+ @abstractmethod
54
+ async def _tx_commit_impl(self): ...
55
+
56
+ @abstractmethod
57
+ async def _tx_rollback_impl(self): ...
58
+
59
+ @abstractmethod
60
+ async def _close_impl(self): ...
61
+
20
62
 
21
63
  try:
22
64
  from tigrbl.session.spec import SessionSpec
@@ -69,15 +111,21 @@ class NumpySession(TigrblSessionBase):
69
111
  self._puts: dict[tuple[type, Any], dict[str, Any]] = {}
70
112
  self._dels: set[tuple[type, Any]] = set()
71
113
  self._tracked: dict[tuple[type, Any], Any] = {}
114
+ self._tracked: dict[tuple[type, Any], Any] = {}
72
115
 
73
116
  def to_records(self) -> list[dict[str, Any]]:
74
117
  pk = self._engine.catalog.pk
75
- rows = [dict(row) for row in self._engine.catalog.rows]
76
- deleted = {ident for (_model, ident) in self._dels}
77
- by_pk = {row.get(pk): row for row in rows if row.get(pk) not in deleted}
78
- for (_model, ident), row in self._puts.items():
79
- by_pk[ident] = dict(row)
80
- return list(by_pk.values())
118
+ by_id: dict[Any, dict[str, Any]] = {}
119
+ for row in self._engine.catalog.rows:
120
+ ident = row.get(pk)
121
+ if ident is None:
122
+ continue
123
+ by_id[ident] = dict(row)
124
+ for (_, ident), row in self._puts.items():
125
+ by_id[ident] = dict(row)
126
+ for _, ident in self._dels:
127
+ by_id.pop(ident, None)
128
+ return list(by_id.values())
81
129
 
82
130
  def array(self) -> np.ndarray:
83
131
  rows = self.to_records()
@@ -163,6 +211,7 @@ class NumpySession(TigrblSessionBase):
163
211
  self._puts.clear()
164
212
  self._dels.clear()
165
213
  self._tracked.clear()
214
+ self._tracked.clear()
166
215
 
167
216
  async def _tx_commit_impl(self) -> None:
168
217
  iso = (self._spec.isolation if self._spec else None) or "read_committed"
@@ -193,44 +242,33 @@ class NumpySession(TigrblSessionBase):
193
242
  self._puts.clear()
194
243
  self._dels.clear()
195
244
  self._tracked.clear()
245
+ self._tracked.clear()
196
246
 
197
247
  async def _tx_rollback_impl(self) -> None:
198
248
  self._puts.clear()
199
249
  self._dels.clear()
200
250
  self._tracked.clear()
201
251
 
202
- @staticmethod
203
- def _pk_default(model: type, pk: str) -> Any:
204
- table = getattr(model, "__table__", None)
205
- if table is None:
206
- return None
207
- try:
208
- column = table.columns.get(pk)
209
- except Exception:
210
- return None
211
- if column is None:
212
- return None
213
- default = getattr(column, "default", None)
214
- if default is None:
215
- return None
216
- arg = getattr(default, "arg", None)
217
- if callable(arg):
218
- try:
219
- return arg()
220
- except TypeError:
221
- return arg(None)
222
- return arg
223
-
224
252
  def _add_impl(self, obj: Any) -> Any:
225
253
  model = obj.__class__
226
254
  pk = _single_pk_name(model)
227
255
  ident = getattr(obj, pk)
228
256
  if ident is None:
229
- ident = self._pk_default(model, pk)
230
- if ident is not None:
257
+ default = getattr(getattr(model, "__table__", None), "columns", {}).get(pk)
258
+ default = getattr(default, "default", None)
259
+ if default is not None:
260
+ arg = getattr(default, "arg", default)
261
+ if callable(arg):
262
+ try:
263
+ ident = arg()
264
+ except TypeError:
265
+ ident = arg(None)
266
+ else:
267
+ ident = arg
231
268
  setattr(obj, pk, ident)
232
269
  if ident is None:
233
270
  raise ValueError(f"primary key {pk!r} must be set")
271
+ self._tracked[(model, ident)] = obj
234
272
  row = {c: getattr(obj, c, None) for c in _model_columns(model)}
235
273
  self._puts[(model, ident)] = row
236
274
  self._dels.discard((model, ident))
@@ -243,40 +281,42 @@ class NumpySession(TigrblSessionBase):
243
281
  self._puts.pop((model, ident), None)
244
282
  self._dels.add((model, ident))
245
283
  self._tracked.pop((model, ident), None)
284
+ self._tracked.pop((model, ident), None)
246
285
 
247
286
  async def _flush_impl(self) -> None:
248
- for (model, ident), obj in self._tracked.items():
287
+ for (model, ident), obj in list(self._tracked.items()):
249
288
  if (model, ident) in self._dels:
250
289
  continue
251
- self._puts[(model, ident)] = {
252
- column: getattr(obj, column, None) for column in _model_columns(model)
253
- }
254
- return
290
+ row = {c: getattr(obj, c, None) for c in _model_columns(model)}
291
+ baseline = self._resolve_row(model, ident)
292
+ if baseline is None:
293
+ continue
294
+ if row == dict(baseline):
295
+ continue
296
+ if self._spec and self._spec.read_only:
297
+ raise RuntimeError("read-only session: writes detected during flush")
298
+ self._puts[(model, ident)] = row
255
299
 
256
300
  async def _refresh_impl(self, obj: Any) -> None:
257
301
  pk = _single_pk_name(obj.__class__)
258
302
  ident = getattr(obj, pk)
259
- fresh = await self._get_impl(obj.__class__, ident)
260
- if fresh is None:
303
+ row = self._resolve_row(obj.__class__, ident)
304
+ if row is None:
261
305
  return
262
306
  for c in _model_columns(obj.__class__):
263
- setattr(obj, c, getattr(fresh, c, None))
307
+ if c in row:
308
+ setattr(obj, c, row[c])
264
309
 
265
310
  async def _get_impl(self, model: type, ident: Any) -> Any | None:
266
- row = self._puts.get((model, ident))
267
- if row is not None:
268
- obj = self._inflate(model, row)
269
- self._tracked[(model, ident)] = obj
270
- return obj
271
311
  if (model, ident) in self._dels:
272
312
  return None
273
- pk = _single_pk_name(model)
274
- for record in self._engine.catalog.rows:
275
- if record.get(pk) == ident:
276
- obj = self._inflate(model, record)
277
- self._tracked[(model, ident)] = obj
278
- return obj
279
- return None
313
+ tracked = self._tracked.get((model, ident))
314
+ if tracked is not None:
315
+ return tracked
316
+ row = self._resolve_row(model, ident)
317
+ if row is None:
318
+ return None
319
+ return self._hydrate_tracked(model, ident, row)
280
320
 
281
321
  async def _execute_impl(self, stmt: Any) -> Any:
282
322
  kind = type(stmt).__name__.lower()
@@ -299,6 +339,7 @@ class NumpySession(TigrblSessionBase):
299
339
  raise NotImplementedError(f"Unsupported statement: {type(stmt)}")
300
340
 
301
341
  async def _close_impl(self) -> None:
342
+ self._tracked.clear()
302
343
  return
303
344
 
304
345
  def _inflate(self, model: type, data: Mapping[str, Any]) -> Any:
@@ -309,17 +350,43 @@ class NumpySession(TigrblSessionBase):
309
350
  return obj
310
351
 
311
352
  def _scan_model(self, model: type) -> List[Any]:
312
- out = [self._inflate(model, row) for row in self._engine.catalog.rows]
313
353
  pk = _single_pk_name(model)
314
- by_id = {getattr(obj, pk): obj for obj in out}
354
+ by_id: dict[Any, Any] = {}
355
+ for row in self._engine.catalog.rows:
356
+ ident = row.get(pk)
357
+ if ident is None:
358
+ continue
359
+ by_id[ident] = self._hydrate_tracked(model, ident, row)
315
360
  for (m, ident), row in self._puts.items():
316
361
  if m is model:
317
- by_id[ident] = self._inflate(model, row)
362
+ by_id[ident] = self._hydrate_tracked(model, ident, row)
318
363
  for m, ident in self._dels:
319
364
  if m is model:
320
365
  by_id.pop(ident, None)
366
+ self._tracked.pop((model, ident), None)
321
367
  return list(by_id.values())
322
368
 
369
+ def _resolve_row(self, model: type, ident: Any) -> Mapping[str, Any] | None:
370
+ row = self._puts.get((model, ident))
371
+ if row is not None:
372
+ return row
373
+ pk = _single_pk_name(model)
374
+ for record in self._engine.catalog.rows:
375
+ if record.get(pk) == ident:
376
+ return record
377
+ return None
378
+
379
+ def _hydrate_tracked(self, model: type, ident: Any, data: Mapping[str, Any]) -> Any:
380
+ obj = self._tracked.get((model, ident))
381
+ if obj is None:
382
+ obj = self._inflate(model, data)
383
+ self._tracked[(model, ident)] = obj
384
+ return obj
385
+ for c in _model_columns(model):
386
+ if c in data:
387
+ setattr(obj, c, data[c])
388
+ return obj
389
+
323
390
  def _decompose_select(
324
391
  self, stmt: Any
325
392
  ) -> Tuple[
@@ -349,14 +416,20 @@ class NumpySession(TigrblSessionBase):
349
416
 
350
417
  def _all_subclasses(base: type) -> list[type]:
351
418
  out: list[type] = []
352
- stack = list(base.__subclasses__())
419
+ stack = [base]
420
+ seen: set[type] = set()
353
421
  while stack:
354
422
  cls = stack.pop()
355
- out.append(cls)
356
423
  try:
357
- stack.extend(cls.__subclasses__())
424
+ children = cls.__subclasses__()
358
425
  except TypeError:
359
- stack.extend(type.__subclasses__(cls))
426
+ continue
427
+ for child in children:
428
+ if child in seen:
429
+ continue
430
+ seen.add(child)
431
+ out.append(child)
432
+ stack.append(child)
360
433
  return out
361
434
 
362
435
  def _find_by_table(name: str) -> type | None:
@@ -365,6 +438,13 @@ class NumpySession(TigrblSessionBase):
365
438
  return cls
366
439
  return None
367
440
 
441
+ table = getattr(stmt, "table", None)
442
+ name = getattr(table, "name", None)
443
+ if isinstance(name, str):
444
+ found = _find_by_table(name)
445
+ if found is not None:
446
+ return found
447
+
368
448
  for attr_name in ("_from_objects", "_froms", "froms"):
369
449
  value = getattr(stmt, attr_name, None)
370
450
  if value is not None:
@@ -4,10 +4,34 @@ from pathlib import Path
4
4
 
5
5
  import numpy as np
6
6
  import pytest
7
+ from tigrbl.specs import F, IO, S
8
+ from tigrbl.shortcuts import acol
9
+ from tigrbl.table import Table
10
+ from tigrbl.types import Mapped, String
7
11
 
8
12
  from tigrbl_engine_numpy import numpy_engine
9
13
 
10
14
 
15
+ class _Widget(Table):
16
+ __tablename__ = "session_widgets"
17
+
18
+ id: Mapped[str] = acol(
19
+ storage=S(type_=String(64), primary_key=True, nullable=False),
20
+ field=F(py_type=str),
21
+ io=IO(out_verbs=("read", "list")),
22
+ )
23
+
24
+ name: Mapped[str] = acol(
25
+ storage=S(type_=String(50), nullable=False),
26
+ field=F(py_type=str),
27
+ io=IO(
28
+ in_verbs=("create", "update", "replace"),
29
+ out_verbs=("read", "list"),
30
+ mutable_verbs=("create", "update", "replace"),
31
+ ),
32
+ )
33
+
34
+
11
35
  def test_numpy_session_save_and_load_npy(tmp_path: Path) -> None:
12
36
  target = tmp_path / "records.npy"
13
37
  _, session_factory = numpy_engine(
@@ -134,3 +158,48 @@ def test_numpy_session_save_uses_atomic_replace(
134
158
  assert len(calls) == 1
135
159
  assert calls[0][1] == str(target)
136
160
  assert Path(calls[0][0]).name.startswith(".tmp_")
161
+
162
+
163
+ @pytest.mark.asyncio
164
+ async def test_numpy_session_get_reuses_tracked_instance() -> None:
165
+ ident = "fixed-id"
166
+ _, session_factory = numpy_engine(
167
+ mapping={
168
+ "array": np.array([[ident, "a"]], dtype=object),
169
+ "columns": ["id", "name"],
170
+ "pk": "id",
171
+ }
172
+ )
173
+ session = session_factory()
174
+
175
+ first = await session.get(_Widget, ident)
176
+ assert first is not None
177
+ first.name = "mutated"
178
+
179
+ second = await session.get(_Widget, ident)
180
+ assert second is first
181
+ assert second.name == "mutated"
182
+
183
+
184
+ @pytest.mark.asyncio
185
+ async def test_numpy_session_refresh_updates_tracked_instance() -> None:
186
+ ident = "fixed-id"
187
+ engine, session_factory = numpy_engine(
188
+ mapping={
189
+ "array": np.array([[ident, "a"]], dtype=object),
190
+ "columns": ["id", "name"],
191
+ "pk": "id",
192
+ }
193
+ )
194
+ session = session_factory()
195
+
196
+ item = await session.get(_Widget, ident)
197
+ assert item is not None
198
+ item.name = "mutated"
199
+
200
+ engine.catalog.rows[0]["name"] = "server"
201
+ await session.refresh(item)
202
+
203
+ again = await session.get(_Widget, ident)
204
+ assert again is item
205
+ assert again.name == "server"
@@ -4,10 +4,11 @@ import numpy as np
4
4
  import pytest
5
5
 
6
6
  from tigrbl import TigrblApp
7
- from tigrbl.bindings import rpc_call
7
+ from tigrbl import rpc_call
8
8
  from tigrbl.engine import EngineSpec
9
9
  from tigrbl.orm.mixins import GUIDPk
10
- from tigrbl.specs import F, IO, S, acol
10
+ from tigrbl.specs import F, IO, S
11
+ from tigrbl.shortcuts import acol
11
12
  from tigrbl.table import Table
12
13
  from tigrbl.types import Mapped, String
13
14