tigrbl_engine_dataframe 0.1.10.dev1__py3-none-any.whl
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.
- tigrbl_engine_dataframe/__init__.py +41 -0
- tigrbl_engine_dataframe/df_engine.py +90 -0
- tigrbl_engine_dataframe/df_session.py +487 -0
- tigrbl_engine_dataframe-0.1.10.dev1.dist-info/METADATA +50 -0
- tigrbl_engine_dataframe-0.1.10.dev1.dist-info/RECORD +8 -0
- tigrbl_engine_dataframe-0.1.10.dev1.dist-info/WHEEL +4 -0
- tigrbl_engine_dataframe-0.1.10.dev1.dist-info/entry_points.txt +2 -0
- tigrbl_engine_dataframe-0.1.10.dev1.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""tigrbl_engine_dataframe: DataFrame-backed Tigrbl engine"""
|
|
2
|
+
|
|
3
|
+
from .df_engine import dataframe_engine, dataframe_capabilities, DataFrameCatalog
|
|
4
|
+
from .df_session import TransactionalDataFrameSession
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"dataframe_engine",
|
|
8
|
+
"dataframe_capabilities",
|
|
9
|
+
"DataFrameCatalog",
|
|
10
|
+
"TransactionalDataFrameSession",
|
|
11
|
+
"register",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register() -> None:
|
|
16
|
+
"""
|
|
17
|
+
Entry point target for group 'tigrbl.engine'. This function will be loaded
|
|
18
|
+
by Tigrbl's plugin system. It attempts to register the 'dataframe' kind
|
|
19
|
+
with whatever registry is exposed by the installed Tigrbl version.
|
|
20
|
+
"""
|
|
21
|
+
register_fn = None
|
|
22
|
+
try:
|
|
23
|
+
from tigrbl.engine.registry import register_engine as _reg
|
|
24
|
+
|
|
25
|
+
register_fn = _reg
|
|
26
|
+
except Exception:
|
|
27
|
+
try:
|
|
28
|
+
from tigrbl.engine.plugins import register_engine as _reg2
|
|
29
|
+
|
|
30
|
+
register_fn = _reg2
|
|
31
|
+
except Exception:
|
|
32
|
+
try:
|
|
33
|
+
from tigrbl.engine import register_engine as _reg3 # type: ignore
|
|
34
|
+
|
|
35
|
+
register_fn = _reg3
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
"Could not locate Tigrbl engine registry to register plugin"
|
|
39
|
+
) from exc
|
|
40
|
+
|
|
41
|
+
register_fn("dataframe", dataframe_engine, dataframe_capabilities)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from tigrbl.session.spec import SessionSpec
|
|
10
|
+
from .df_session import TransactionalDataFrameSession
|
|
11
|
+
|
|
12
|
+
# ---- Engine object: in-memory catalog of DataFrames + versions ----
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DataFrameCatalog:
|
|
17
|
+
tables: Dict[str, pd.DataFrame] = field(default_factory=dict) # name -> live frame
|
|
18
|
+
pks: Dict[str, str] = field(default_factory=dict) # name -> primary-key column
|
|
19
|
+
table_ver: Dict[str, int] = field(default_factory=dict) # name -> monotonic version
|
|
20
|
+
lock: threading.RLock = field(
|
|
21
|
+
default_factory=threading.RLock
|
|
22
|
+
) # atomic apply on commit
|
|
23
|
+
|
|
24
|
+
def get_live(self, name: str) -> pd.DataFrame:
|
|
25
|
+
if name not in self.tables:
|
|
26
|
+
self.tables[name] = pd.DataFrame()
|
|
27
|
+
self.table_ver[name] = 0
|
|
28
|
+
self.table_ver.setdefault(name, 0)
|
|
29
|
+
return self.tables[name]
|
|
30
|
+
|
|
31
|
+
def bump(self, name: str) -> None:
|
|
32
|
+
self.table_ver[name] = self.table_ver.get(name, 0) + 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---- Builder expected by Tigrbl EngineSpec (kind='dataframe') ----
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dataframe_engine(
|
|
39
|
+
*,
|
|
40
|
+
mapping: Optional[Mapping[str, object]] = None,
|
|
41
|
+
spec: Any = None,
|
|
42
|
+
dsn: Optional[str] = None,
|
|
43
|
+
) -> Tuple[DataFrameCatalog, Callable[[], TransactionalDataFrameSession]]:
|
|
44
|
+
"""
|
|
45
|
+
Return (engine, sessionmaker) for the 'dataframe' kind.
|
|
46
|
+
|
|
47
|
+
EngineSpec(kind="dataframe") calls this with:
|
|
48
|
+
- mapping: plugin-specific config (tables, pks)
|
|
49
|
+
- spec: the EngineSpec instance (not used here)
|
|
50
|
+
- dsn: optional DSN (not used here)
|
|
51
|
+
"""
|
|
52
|
+
m = dict(mapping or {})
|
|
53
|
+
initial_tables = m.get("tables") or {}
|
|
54
|
+
pks = m.get("pks") or {}
|
|
55
|
+
|
|
56
|
+
if not isinstance(initial_tables, dict):
|
|
57
|
+
raise TypeError("mapping['tables'] must be a dict[str, pandas.DataFrame]")
|
|
58
|
+
if not isinstance(pks, dict):
|
|
59
|
+
raise TypeError("mapping['pks'] must be a dict[str, str]")
|
|
60
|
+
|
|
61
|
+
# Defensive copy of tables
|
|
62
|
+
tables = {
|
|
63
|
+
k: (v.copy() if isinstance(v, pd.DataFrame) else v)
|
|
64
|
+
for k, v in initial_tables.items()
|
|
65
|
+
}
|
|
66
|
+
cat = DataFrameCatalog(tables=tables, pks=dict(pks))
|
|
67
|
+
|
|
68
|
+
def mk() -> TransactionalDataFrameSession:
|
|
69
|
+
# A neutral SessionSpec is attached here; the effective SessionSpec from
|
|
70
|
+
# session_ctx is typically applied by the Tigrbl layer wrapping the sessionmaker.
|
|
71
|
+
return TransactionalDataFrameSession(cat, spec=SessionSpec())
|
|
72
|
+
|
|
73
|
+
return cat, mk
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---- Capabilities (optional but useful for validation) ----
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def dataframe_capabilities() -> dict[str, object]:
|
|
80
|
+
"""Report capabilities for the 'dataframe' engine."""
|
|
81
|
+
return {
|
|
82
|
+
"transactional": True,
|
|
83
|
+
"read_only_enforced": True,
|
|
84
|
+
"isolation_levels": {
|
|
85
|
+
"read_committed",
|
|
86
|
+
"repeatable_read",
|
|
87
|
+
"snapshot",
|
|
88
|
+
"serializable",
|
|
89
|
+
},
|
|
90
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
# Prefer Tigrbl's base; provide a minimal fallback if missing (package can still import)
|
|
8
|
+
try:
|
|
9
|
+
from tigrbl.session.base import TigrblSessionBase
|
|
10
|
+
except Exception: # fallback minimal ABC
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
|
|
13
|
+
class TigrblSessionBase(ABC):
|
|
14
|
+
def __init__(self, spec=None):
|
|
15
|
+
self._spec = spec
|
|
16
|
+
self._open = False
|
|
17
|
+
self._dirty = False
|
|
18
|
+
|
|
19
|
+
async def begin(self):
|
|
20
|
+
self._open = True
|
|
21
|
+
|
|
22
|
+
async def commit(self):
|
|
23
|
+
self._open = False
|
|
24
|
+
self._dirty = False
|
|
25
|
+
|
|
26
|
+
async def rollback(self):
|
|
27
|
+
self._open = False
|
|
28
|
+
self._dirty = False
|
|
29
|
+
|
|
30
|
+
def in_transaction(self):
|
|
31
|
+
return self._open
|
|
32
|
+
|
|
33
|
+
async def run_sync(self, fn):
|
|
34
|
+
rv = fn(self)
|
|
35
|
+
return await rv if hasattr(rv, "__await__") else rv
|
|
36
|
+
|
|
37
|
+
def apply_spec(self, spec):
|
|
38
|
+
self._spec = spec
|
|
39
|
+
|
|
40
|
+
# abstract CRUD/lifecycle
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def _add_impl(self, obj): ...
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def _delete_impl(self, obj): ...
|
|
45
|
+
@abstractmethod
|
|
46
|
+
async def _flush_impl(self): ...
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def _refresh_impl(self, obj): ...
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def _get_impl(self, model, ident): ...
|
|
51
|
+
@abstractmethod
|
|
52
|
+
async def _execute_impl(self, stmt): ...
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def _tx_begin_impl(self): ...
|
|
55
|
+
@abstractmethod
|
|
56
|
+
async def _tx_commit_impl(self): ...
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def _tx_rollback_impl(self): ...
|
|
59
|
+
@abstractmethod
|
|
60
|
+
async def _close_impl(self): ...
|
|
61
|
+
# public CRUD wrappers
|
|
62
|
+
def add(self, obj):
|
|
63
|
+
self._dirty = True
|
|
64
|
+
return self._add_impl(obj)
|
|
65
|
+
|
|
66
|
+
async def delete(self, obj):
|
|
67
|
+
self._dirty = True
|
|
68
|
+
return await self._delete_impl(obj)
|
|
69
|
+
|
|
70
|
+
async def flush(self):
|
|
71
|
+
return await self._flush_impl()
|
|
72
|
+
|
|
73
|
+
async def refresh(self, obj):
|
|
74
|
+
return await self._refresh_impl(obj)
|
|
75
|
+
|
|
76
|
+
async def get(self, model, ident):
|
|
77
|
+
return await self._get_impl(model, ident)
|
|
78
|
+
|
|
79
|
+
async def execute(self, stmt):
|
|
80
|
+
return await self._execute_impl(stmt)
|
|
81
|
+
|
|
82
|
+
async def close(self):
|
|
83
|
+
return await self._close_impl()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
from tigrbl.session.spec import SessionSpec
|
|
88
|
+
except Exception:
|
|
89
|
+
|
|
90
|
+
class SessionSpec:
|
|
91
|
+
def __init__(self, isolation=None, read_only=None):
|
|
92
|
+
self.isolation = isolation
|
|
93
|
+
self.read_only = read_only
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
from tigrbl.core.crud.helpers.model import _single_pk_name, _model_columns
|
|
98
|
+
except Exception:
|
|
99
|
+
# Minimal fallbacks
|
|
100
|
+
def _single_pk_name(model):
|
|
101
|
+
return "id"
|
|
102
|
+
|
|
103
|
+
def _model_columns(model):
|
|
104
|
+
return getattr(model, "__annotations__", {}) or {"id": int}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
from tigrbl.core.crud.helpers import NoResultFound
|
|
109
|
+
except Exception:
|
|
110
|
+
|
|
111
|
+
class NoResultFound(Exception):
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
from .df_engine import DataFrameCatalog
|
|
116
|
+
|
|
117
|
+
# ---- Result facades compatible with Tigrbl CRUD ----
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _ScalarResult:
|
|
121
|
+
def __init__(self, items: Sequence[Any]) -> None:
|
|
122
|
+
self._items = list(items)
|
|
123
|
+
|
|
124
|
+
def scalars(self) -> "_ScalarResult":
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def all(self) -> List[Any]:
|
|
128
|
+
return list(self._items)
|
|
129
|
+
|
|
130
|
+
def scalar_one(self) -> Any:
|
|
131
|
+
if len(self._items) != 1:
|
|
132
|
+
raise NoResultFound("expected exactly one row")
|
|
133
|
+
return self._items[0]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class _ExecuteResult(_ScalarResult):
|
|
137
|
+
rowcount: int = 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---- Transactional DataFrame Session ----
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TransactionalDataFrameSession(TigrblSessionBase):
|
|
144
|
+
"""Native-transaction session over pandas DataFrames."""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self, catalog: DataFrameCatalog, spec: Optional[SessionSpec] = None
|
|
148
|
+
) -> None:
|
|
149
|
+
super().__init__(spec)
|
|
150
|
+
self._cat = catalog
|
|
151
|
+
self._snap: Dict[str, pd.DataFrame] = {}
|
|
152
|
+
self._snap_ver: Dict[str, int] = {}
|
|
153
|
+
self._puts: Dict[Tuple[type, Any], Dict[str, Any]] = {}
|
|
154
|
+
self._dels: set[Tuple[type, Any]] = set()
|
|
155
|
+
|
|
156
|
+
# ---- lifecycle / async marker ----
|
|
157
|
+
async def run_sync(self, fn: Callable[[Any], Any]) -> Any:
|
|
158
|
+
out = fn(self)
|
|
159
|
+
return await out if hasattr(out, "__await__") else out
|
|
160
|
+
|
|
161
|
+
# ---- TX primitives ----
|
|
162
|
+
async def _tx_begin_impl(self) -> None:
|
|
163
|
+
self._snap.clear()
|
|
164
|
+
self._snap_ver.clear()
|
|
165
|
+
self._puts.clear()
|
|
166
|
+
self._dels.clear()
|
|
167
|
+
|
|
168
|
+
async def _tx_commit_impl(self) -> None:
|
|
169
|
+
iso = (self._spec.isolation if self._spec else None) or "read_committed"
|
|
170
|
+
# Conflict detection (coarse, per-table)
|
|
171
|
+
if iso in ("repeatable_read", "snapshot", "serializable"):
|
|
172
|
+
for tbl, ver in self._snap_ver.items():
|
|
173
|
+
if self._cat.table_ver.get(tbl, 0) != ver:
|
|
174
|
+
raise RuntimeError(f"transaction conflict on table '{tbl}'")
|
|
175
|
+
|
|
176
|
+
# Apply mutations atomically
|
|
177
|
+
with self._cat.lock:
|
|
178
|
+
# deletes
|
|
179
|
+
dels_by_tbl: Dict[str, List[Any]] = {}
|
|
180
|
+
for model, ident in self._dels:
|
|
181
|
+
tbl = self._table(model)
|
|
182
|
+
dels_by_tbl.setdefault(tbl, []).append(ident)
|
|
183
|
+
for tbl, idents in dels_by_tbl.items():
|
|
184
|
+
pk = self._pk_of(tbl)
|
|
185
|
+
live = self._cat.get_live(tbl)
|
|
186
|
+
if pk in live.columns and not live.empty:
|
|
187
|
+
self._cat.tables[tbl] = live[~live[pk].isin(idents)].copy()
|
|
188
|
+
self._cat.bump(tbl)
|
|
189
|
+
|
|
190
|
+
# upserts
|
|
191
|
+
puts_by_tbl: Dict[str, List[Dict[str, Any]]] = {}
|
|
192
|
+
for (model, _), row in self._puts.items():
|
|
193
|
+
puts_by_tbl.setdefault(self._table(model), []).append(row)
|
|
194
|
+
for tbl, rows in puts_by_tbl.items():
|
|
195
|
+
pk = self._pk_of(tbl)
|
|
196
|
+
live = self._cat.get_live(tbl)
|
|
197
|
+
df_new = pd.DataFrame(rows)
|
|
198
|
+
if df_new.empty:
|
|
199
|
+
continue
|
|
200
|
+
if pk not in df_new.columns:
|
|
201
|
+
raise RuntimeError(f"missing pk '{pk}' for table '{tbl}'")
|
|
202
|
+
if live.empty:
|
|
203
|
+
combined = df_new.copy()
|
|
204
|
+
else:
|
|
205
|
+
combined = live[~live[pk].isin(df_new[pk])].copy()
|
|
206
|
+
combined = pd.concat([combined, df_new], ignore_index=True)
|
|
207
|
+
self._cat.tables[tbl] = combined
|
|
208
|
+
self._cat.bump(tbl)
|
|
209
|
+
|
|
210
|
+
self._puts.clear()
|
|
211
|
+
self._dels.clear()
|
|
212
|
+
|
|
213
|
+
async def _tx_rollback_impl(self) -> None:
|
|
214
|
+
self._snap.clear()
|
|
215
|
+
self._snap_ver.clear()
|
|
216
|
+
self._puts.clear()
|
|
217
|
+
self._dels.clear()
|
|
218
|
+
|
|
219
|
+
# ---- CRUD primitives ----
|
|
220
|
+
def _add_impl(self, obj: Any) -> Any:
|
|
221
|
+
model = obj.__class__
|
|
222
|
+
pk = _single_pk_name(model)
|
|
223
|
+
ident = getattr(obj, pk)
|
|
224
|
+
if ident is None:
|
|
225
|
+
raise ValueError(f"primary key {pk!r} must be set")
|
|
226
|
+
row = {c: getattr(obj, c, None) for c in _model_columns(model)}
|
|
227
|
+
self._puts[(model, ident)] = row
|
|
228
|
+
self._dels.discard((model, ident))
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
async def _delete_impl(self, obj: Any) -> None:
|
|
232
|
+
model = obj.__class__
|
|
233
|
+
pk = _single_pk_name(model)
|
|
234
|
+
ident = getattr(obj, pk)
|
|
235
|
+
self._puts.pop((model, ident), None)
|
|
236
|
+
self._dels.add((model, ident))
|
|
237
|
+
|
|
238
|
+
async def _flush_impl(self) -> None:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
async def _refresh_impl(self, obj: Any) -> None:
|
|
242
|
+
pk = _single_pk_name(obj.__class__)
|
|
243
|
+
ident = getattr(obj, pk)
|
|
244
|
+
fresh = await self._get_impl(obj.__class__, ident)
|
|
245
|
+
if fresh is None:
|
|
246
|
+
return
|
|
247
|
+
for c in _model_columns(obj.__class__):
|
|
248
|
+
setattr(obj, c, getattr(fresh, c, None))
|
|
249
|
+
|
|
250
|
+
async def _get_impl(self, model: type, ident: Any) -> Any | None:
|
|
251
|
+
row = self._puts.get((model, ident))
|
|
252
|
+
if row is not None:
|
|
253
|
+
return self._inflate(model, row)
|
|
254
|
+
if (model, ident) in self._dels:
|
|
255
|
+
return None
|
|
256
|
+
df = self._frame_for(model)
|
|
257
|
+
pk = _single_pk_name(model)
|
|
258
|
+
if pk not in df.columns or df.empty:
|
|
259
|
+
return None
|
|
260
|
+
m = df[df[pk] == ident]
|
|
261
|
+
if m.empty:
|
|
262
|
+
return None
|
|
263
|
+
return self._inflate(model, m.iloc[0].to_dict())
|
|
264
|
+
|
|
265
|
+
async def _execute_impl(self, stmt: Any) -> Any:
|
|
266
|
+
kind = type(stmt).__name__.lower()
|
|
267
|
+
if "select" in kind:
|
|
268
|
+
model, where, order, limit, offset = self._decompose_select(stmt)
|
|
269
|
+
items = self._scan_model(model)
|
|
270
|
+
items = [o for o in items if self._matches_obj(o, where)]
|
|
271
|
+
items = self._order_slice(items, order, limit, offset)
|
|
272
|
+
return _ExecuteResult(items)
|
|
273
|
+
if "delete" in kind:
|
|
274
|
+
model, where = self._decompose_delete(stmt)
|
|
275
|
+
items = self._scan_model(model)
|
|
276
|
+
items = [o for o in items if self._matches_obj(o, where)]
|
|
277
|
+
for o in items:
|
|
278
|
+
pk = _single_pk_name(model)
|
|
279
|
+
ident = getattr(o, pk)
|
|
280
|
+
self._puts.pop((model, ident), None)
|
|
281
|
+
self._dels.add((model, ident))
|
|
282
|
+
res = _ExecuteResult([])
|
|
283
|
+
res.rowcount = len(items)
|
|
284
|
+
return res
|
|
285
|
+
raise NotImplementedError(f"Unsupported statement: {type(stmt)}")
|
|
286
|
+
|
|
287
|
+
async def _close_impl(self) -> None:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# ---- helpers ----
|
|
291
|
+
def _table(self, model: type) -> str:
|
|
292
|
+
return getattr(model, "__tablename__", None) or model.__name__
|
|
293
|
+
|
|
294
|
+
def _pk_of(self, table: str) -> str:
|
|
295
|
+
if table in self._cat.pks:
|
|
296
|
+
return self._cat.pks[table]
|
|
297
|
+
live = self._cat.get_live(table)
|
|
298
|
+
if "id" in live.columns:
|
|
299
|
+
return "id"
|
|
300
|
+
raise RuntimeError(f"primary key for table '{table}' is unknown")
|
|
301
|
+
|
|
302
|
+
def _frame_for(self, model: type) -> pd.DataFrame:
|
|
303
|
+
table = self._table(model)
|
|
304
|
+
iso = (self._spec.isolation if self._spec else None) or "read_committed"
|
|
305
|
+
if (
|
|
306
|
+
iso in ("repeatable_read", "snapshot", "serializable")
|
|
307
|
+
and table not in self._snap
|
|
308
|
+
):
|
|
309
|
+
live = self._cat.get_live(table)
|
|
310
|
+
self._snap[table] = live.copy(deep=True)
|
|
311
|
+
self._snap_ver[table] = self._cat.table_ver.get(table, 0)
|
|
312
|
+
return (
|
|
313
|
+
self._snap.get(table) if table in self._snap else self._cat.get_live(table)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _inflate(self, model: type, data: Mapping[str, Any]) -> Any:
|
|
317
|
+
obj = model()
|
|
318
|
+
for c in _model_columns(model):
|
|
319
|
+
if c in data:
|
|
320
|
+
setattr(obj, c, data[c])
|
|
321
|
+
return obj
|
|
322
|
+
|
|
323
|
+
def _scan_model(self, model: type) -> List[Any]:
|
|
324
|
+
df = self._frame_for(model)
|
|
325
|
+
out: List[Any] = []
|
|
326
|
+
if not df.empty:
|
|
327
|
+
for _, row in df.iterrows():
|
|
328
|
+
out.append(self._inflate(model, row.to_dict()))
|
|
329
|
+
# overlay upserts / deletes
|
|
330
|
+
pk = _single_pk_name(model)
|
|
331
|
+
by_id = {getattr(o, pk): o for o in out}
|
|
332
|
+
for (m, ident), row in self._puts.items():
|
|
333
|
+
if m is model:
|
|
334
|
+
by_id[ident] = self._inflate(model, row)
|
|
335
|
+
for m, ident in list(self._dels):
|
|
336
|
+
if m is model:
|
|
337
|
+
by_id.pop(ident, None)
|
|
338
|
+
return list(by_id.values())
|
|
339
|
+
|
|
340
|
+
# ---- duck-typed stmt parsing (eq/IN/order/limit/offset) ----
|
|
341
|
+
def _decompose_select(
|
|
342
|
+
self, stmt: Any
|
|
343
|
+
) -> Tuple[
|
|
344
|
+
type,
|
|
345
|
+
list[Tuple[str, str, Any]],
|
|
346
|
+
list[Tuple[str, str]],
|
|
347
|
+
Optional[int],
|
|
348
|
+
Optional[int],
|
|
349
|
+
]:
|
|
350
|
+
model = self._extract_model(stmt)
|
|
351
|
+
where = self._extract_predicates(stmt)
|
|
352
|
+
order = self._extract_order_by(stmt)
|
|
353
|
+
limit = self._extract_int(stmt, ["_limit", "_limit_clause", "limit"])
|
|
354
|
+
offset = self._extract_int(stmt, ["_offset", "_offset_clause", "offset"])
|
|
355
|
+
return model, where, order, limit, offset
|
|
356
|
+
|
|
357
|
+
def _decompose_delete(self, stmt: Any) -> Tuple[type, list[Tuple[str, str, Any]]]:
|
|
358
|
+
model = self._extract_model(stmt)
|
|
359
|
+
where = self._extract_predicates(stmt)
|
|
360
|
+
return model, where
|
|
361
|
+
|
|
362
|
+
def _extract_model(self, stmt: Any) -> type:
|
|
363
|
+
for a in ("_from_objects", "_froms", "froms"):
|
|
364
|
+
v = getattr(stmt, a, None)
|
|
365
|
+
if v:
|
|
366
|
+
t = v[0] if isinstance(v, (list, tuple)) else v
|
|
367
|
+
name = getattr(t, "name", None)
|
|
368
|
+
if isinstance(name, str):
|
|
369
|
+
for cls in object.__subclasses__(object):
|
|
370
|
+
if getattr(cls, "__tablename__", None) == name:
|
|
371
|
+
return cls
|
|
372
|
+
rc = getattr(stmt, "_raw_columns", None) or getattr(stmt, "columns", None)
|
|
373
|
+
if rc:
|
|
374
|
+
ent = rc[0]
|
|
375
|
+
table = getattr(ent, "table", None)
|
|
376
|
+
name = getattr(table, "name", None)
|
|
377
|
+
if name:
|
|
378
|
+
for cls in object.__subclasses__(object):
|
|
379
|
+
if getattr(cls, "__tablename__", None) == name:
|
|
380
|
+
return cls
|
|
381
|
+
raise RuntimeError("Cannot resolve model from statement")
|
|
382
|
+
|
|
383
|
+
def _extract_predicates(self, stmt: Any) -> list[Tuple[str, str, Any]]:
|
|
384
|
+
where = getattr(stmt, "whereclause", None) or getattr(
|
|
385
|
+
stmt, "_whereclause", None
|
|
386
|
+
)
|
|
387
|
+
if where is None:
|
|
388
|
+
return []
|
|
389
|
+
parts = (
|
|
390
|
+
getattr(where, "clauses", None)
|
|
391
|
+
or getattr(where, "get_children", lambda: [])()
|
|
392
|
+
)
|
|
393
|
+
nodes = list(parts) if parts else [where]
|
|
394
|
+
out: list[Tuple[str, str, Any]] = []
|
|
395
|
+
for n in nodes:
|
|
396
|
+
left, right, op = (
|
|
397
|
+
getattr(n, "left", None),
|
|
398
|
+
getattr(n, "right", None),
|
|
399
|
+
getattr(n, "operator", None),
|
|
400
|
+
)
|
|
401
|
+
if left is None or right is None:
|
|
402
|
+
continue
|
|
403
|
+
name = getattr(left, "key", None) or getattr(left, "name", None)
|
|
404
|
+
if name is None:
|
|
405
|
+
continue
|
|
406
|
+
opname = getattr(op, "__name__", str(op))
|
|
407
|
+
if "eq" in opname:
|
|
408
|
+
val = (
|
|
409
|
+
getattr(right, "value", None)
|
|
410
|
+
if hasattr(right, "value")
|
|
411
|
+
else getattr(right, "literal", None)
|
|
412
|
+
)
|
|
413
|
+
out.append((str(name), "eq", val))
|
|
414
|
+
continue
|
|
415
|
+
rclauses = getattr(right, "clauses", None)
|
|
416
|
+
if rclauses is not None and "in" in opname:
|
|
417
|
+
vals = [
|
|
418
|
+
getattr(lit, "value", None)
|
|
419
|
+
if hasattr(lit, "value")
|
|
420
|
+
else getattr(lit, "literal", None)
|
|
421
|
+
for lit in rclauses
|
|
422
|
+
]
|
|
423
|
+
out.append((str(name), "in", vals))
|
|
424
|
+
return out
|
|
425
|
+
|
|
426
|
+
def _extract_order_by(self, stmt: Any) -> list[Tuple[str, str]]:
|
|
427
|
+
order = getattr(stmt, "_order_by_clause", None) or getattr(
|
|
428
|
+
stmt, "_order_by_clauses", None
|
|
429
|
+
)
|
|
430
|
+
if not order:
|
|
431
|
+
return []
|
|
432
|
+
clauses = getattr(order, "clauses", None) or order
|
|
433
|
+
clauses = clauses if isinstance(clauses, (list, tuple)) else [clauses]
|
|
434
|
+
for ob in clauses:
|
|
435
|
+
col = (
|
|
436
|
+
getattr(ob, "element", None)
|
|
437
|
+
or getattr(ob, "this", None)
|
|
438
|
+
or getattr(ob, "expr", None)
|
|
439
|
+
)
|
|
440
|
+
name = getattr(col, "key", None) or getattr(col, "name", None)
|
|
441
|
+
direction = "desc" if "desc" in type(ob).__name__.lower() else "asc"
|
|
442
|
+
if name:
|
|
443
|
+
return [(str(name), direction)]
|
|
444
|
+
return []
|
|
445
|
+
|
|
446
|
+
def _extract_int(self, stmt: Any, names: Sequence[str]) -> Optional[int]:
|
|
447
|
+
for n in names:
|
|
448
|
+
v = getattr(stmt, n, None)
|
|
449
|
+
if v is None:
|
|
450
|
+
continue
|
|
451
|
+
try:
|
|
452
|
+
return int(v)
|
|
453
|
+
except Exception:
|
|
454
|
+
val = getattr(v, "value", None)
|
|
455
|
+
if val is not None:
|
|
456
|
+
try:
|
|
457
|
+
return int(val)
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
def _matches_obj(self, obj: Any, where: list[Tuple[str, str, Any]]) -> bool:
|
|
463
|
+
for name, op, val in where:
|
|
464
|
+
dv = getattr(obj, name, None)
|
|
465
|
+
if op == "eq" and dv != val:
|
|
466
|
+
return False
|
|
467
|
+
if op == "in" and dv not in set(val):
|
|
468
|
+
return False
|
|
469
|
+
return True
|
|
470
|
+
|
|
471
|
+
def _order_slice(
|
|
472
|
+
self,
|
|
473
|
+
items: List[Any],
|
|
474
|
+
order: list[Tuple[str, str]],
|
|
475
|
+
limit: Optional[int],
|
|
476
|
+
offset: Optional[int],
|
|
477
|
+
) -> List[Any]:
|
|
478
|
+
if order:
|
|
479
|
+
col, direction = order[0]
|
|
480
|
+
items.sort(
|
|
481
|
+
key=lambda o: getattr(o, col, None), reverse=(direction == "desc")
|
|
482
|
+
)
|
|
483
|
+
if isinstance(offset, int):
|
|
484
|
+
items = items[max(0, offset) :]
|
|
485
|
+
if isinstance(limit, int):
|
|
486
|
+
items = items[: max(0, limit)]
|
|
487
|
+
return items
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tigrbl_engine_dataframe
|
|
3
|
+
Version: 0.1.10.dev1
|
|
4
|
+
Summary: Tigrbl engine plugin providing transactional pandas DataFrame sessions.
|
|
5
|
+
Project-URL: Homepage, https://github.com/swarmauri/swarmauri-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/experimental/tigrbl_engine_dataframe
|
|
7
|
+
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: database,dataframe,engine,pandas,plugin,tigrbl
|
|
11
|
+
Classifier: Development Status :: 1 - Planning
|
|
12
|
+
Classifier: Environment :: Plugins
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Database
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: <3.14,>=3.10
|
|
24
|
+
Requires-Dist: pandas>=2.0
|
|
25
|
+
Requires-Dist: tigrbl
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# tigrbl_engine_dataframe
|
|
29
|
+
|
|
30
|
+
This file is a package-local distribution entry point.
|
|
31
|
+
It is not the authoritative location for repository governance, current target status, current state reporting, certification claims, or release evidence.
|
|
32
|
+
|
|
33
|
+
## Canonical repository docs
|
|
34
|
+
|
|
35
|
+
- `README.md`
|
|
36
|
+
- `docs/README.md`
|
|
37
|
+
- `docs/conformance/CURRENT_TARGET.md`
|
|
38
|
+
- `docs/conformance/CURRENT_STATE.md`
|
|
39
|
+
- `docs/conformance/NEXT_STEPS.md`
|
|
40
|
+
- `docs/governance/DOC_POINTERS.md`
|
|
41
|
+
- `docs/developer/PACKAGE_CATALOG.md`
|
|
42
|
+
- `docs/developer/PACKAGE_LAYOUT.md`
|
|
43
|
+
|
|
44
|
+
## Package identity
|
|
45
|
+
|
|
46
|
+
- workspace path: `pkgs/engines/tigrbl_engine_dataframe`
|
|
47
|
+
- workspace class: engine package
|
|
48
|
+
- implementation layout: `src/tigrbl_engine_dataframe/`
|
|
49
|
+
|
|
50
|
+
Long-form repository documentation is governed from `docs/`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
tigrbl_engine_dataframe/__init__.py,sha256=NfklNopxhOWn9m7O53l2mrWaHfPiyFstnnhGW5ReAtw,1295
|
|
2
|
+
tigrbl_engine_dataframe/df_engine.py,sha256=ltxVnXsPncT7LTVI_440MudQyCToZLe3xCwgp4BWRes,2932
|
|
3
|
+
tigrbl_engine_dataframe/df_session.py,sha256=C1o75BirKA4spgKkytvi2M0AjJFgQl5QtqVLveB11jU,16875
|
|
4
|
+
tigrbl_engine_dataframe-0.1.10.dev1.dist-info/METADATA,sha256=SQsL_AF8DlO6AACSqAISW_OZuqmyG-RgjyzAAq7r66M,1939
|
|
5
|
+
tigrbl_engine_dataframe-0.1.10.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
tigrbl_engine_dataframe-0.1.10.dev1.dist-info/entry_points.txt,sha256=YR-M4grLtN3mqwES7xGKYSBEA1jZ_jStk3R_mcFXe-U,61
|
|
7
|
+
tigrbl_engine_dataframe-0.1.10.dev1.dist-info/licenses/LICENSE,sha256=sLrcRvv1U-v7jmeeYS7dKLlr39FU5lLnR-p6Swg9YdU,766
|
|
8
|
+
tigrbl_engine_dataframe-0.1.10.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
Copyright 2025 Tigrbl
|
|
8
|
+
|
|
9
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
you may not use this file except in compliance with the License.
|
|
11
|
+
You may obtain a copy of the License at
|
|
12
|
+
|
|
13
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
|
|
15
|
+
Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
See the License for the specific language governing permissions and
|
|
19
|
+
limitations under the License.
|