memframe 0.2.0__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.
- memframe/__init__.py +8 -0
- memframe/cache/__init__.py +10 -0
- memframe/cache/cache_manager.py +321 -0
- memframe/core/__init__.py +1 -0
- memframe/core/analytix/_response.py +89 -0
- memframe/core/analytix/arithmetic.py +847 -0
- memframe/core/analytix/cleaning.py +2948 -0
- memframe/core/analytix/inspection.py +1969 -0
- memframe/core/analytix/selection.py +952 -0
- memframe/core/analytix/stats.py +1766 -0
- memframe/core/ingestion/datatype_detector.py +315 -0
- memframe/core/ingestion/upload/__init__.py +21 -0
- memframe/core/ingestion/upload/base.py +643 -0
- memframe/core/ingestion/upload/clickhouse.py +140 -0
- memframe/core/ingestion/upload/duckdb.py +83 -0
- memframe/core/ingestion/upload/postgres.py +104 -0
- memframe/core/ingestion/upload/strategies/__init__.py +13 -0
- memframe/core/ingestion/upload/strategies/base.py +60 -0
- memframe/core/ingestion/upload/strategies/csv.py +485 -0
- memframe/core/ingestion/upload/strategies/df.py +122 -0
- memframe/core/ingestion/upload/strategies/parquet.py +106 -0
- memframe/core/orchestrator/analytix/arithmetic.py +213 -0
- memframe/core/orchestrator/analytix/cleaning.py +489 -0
- memframe/core/orchestrator/analytix/inspection.py +242 -0
- memframe/core/orchestrator/analytix/selection.py +303 -0
- memframe/core/orchestrator/analytix/stats.py +329 -0
- memframe/core/orchestrator/plots/bar.py +128 -0
- memframe/core/orchestrator/plots/bar_polar.py +109 -0
- memframe/core/orchestrator/plots/line.py +80 -0
- memframe/core/orchestrator/plots/pie.py +90 -0
- memframe/core/orchestrator/plots/scatter.py +77 -0
- memframe/core/orchestrator/plots/scatter_3d.py +126 -0
- memframe/core/plots/bar.py +252 -0
- memframe/core/plots/bar_polar.py +191 -0
- memframe/core/plots/line.py +213 -0
- memframe/core/plots/pie.py +177 -0
- memframe/core/plots/scatter.py +213 -0
- memframe/core/plots/scatter_3d.py +222 -0
- memframe/db_manager/__init__.py +1 -0
- memframe/db_manager/adapters/base.py +59 -0
- memframe/db_manager/adapters/clickhouse.py +347 -0
- memframe/db_manager/adapters/duckdb.py +92 -0
- memframe/db_manager/adapters/factory.py +27 -0
- memframe/db_manager/adapters/postgresql.py +105 -0
- memframe/db_manager/connection/__init__.py +13 -0
- memframe/db_manager/connection/connector.py +82 -0
- memframe/db_manager/connection/pool.py +268 -0
- memframe/db_manager/context.py +152 -0
- memframe/db_manager/context.pyi +56 -0
- memframe/db_manager/ops.py +189 -0
- memframe/db_manager/setup/__init__.py +26 -0
- memframe/db_manager/setup/base.py +196 -0
- memframe/db_manager/setup/clickhouse.py +129 -0
- memframe/db_manager/setup/duckdb.py +97 -0
- memframe/db_manager/setup/postgres.py +98 -0
- memframe/exceptions.py +25 -0
- memframe/main.py +205 -0
- memframe/main.pyi +59 -0
- memframe/py.typed +0 -0
- memframe/utils/__init__.py +1 -0
- memframe/utils/async_sync.py +43 -0
- memframe/utils/helper.py +291 -0
- memframe/utils/plot_renderer.py +126 -0
- memframe/wrappers/__init__.py +1 -0
- memframe/wrappers/analytix/__init__.py +0 -0
- memframe/wrappers/analytix/arithmetic.py +546 -0
- memframe/wrappers/analytix/arithmetic.pyi +366 -0
- memframe/wrappers/analytix/cleaning.py +506 -0
- memframe/wrappers/analytix/cleaning.pyi +230 -0
- memframe/wrappers/analytix/inspection.py +525 -0
- memframe/wrappers/analytix/inspection.pyi +186 -0
- memframe/wrappers/analytix/selection.py +302 -0
- memframe/wrappers/analytix/selection.pyi +136 -0
- memframe/wrappers/analytix/stats.py +487 -0
- memframe/wrappers/analytix/stats.pyi +264 -0
- memframe/wrappers/base.py +2 -0
- memframe/wrappers/plots/__init__.py +0 -0
- memframe/wrappers/plots/bar.py +203 -0
- memframe/wrappers/plots/bar.pyi +151 -0
- memframe/wrappers/plots/bar_polar.py +131 -0
- memframe/wrappers/plots/bar_polar.pyi +119 -0
- memframe/wrappers/plots/line.py +105 -0
- memframe/wrappers/plots/line.pyi +80 -0
- memframe/wrappers/plots/pie.py +123 -0
- memframe/wrappers/plots/pie.pyi +92 -0
- memframe/wrappers/plots/scatter.py +100 -0
- memframe/wrappers/plots/scatter.pyi +78 -0
- memframe/wrappers/plots/scatter3d.py +155 -0
- memframe/wrappers/plots/scatter3d.pyi +148 -0
- memframe-0.2.0.dist-info/METADATA +197 -0
- memframe-0.2.0.dist-info/RECORD +114 -0
- memframe-0.2.0.dist-info/WHEEL +4 -0
- memframe-0.2.0.dist-info/licenses/LICENSE +661 -0
- memframe_ai/__init__.py +4 -0
- memframe_ai/agents/__init__.py +18 -0
- memframe_ai/agents/analytics.py +316 -0
- memframe_ai/agents/planning.py +123 -0
- memframe_ai/config.py +14 -0
- memframe_ai/domain.py +126 -0
- memframe_ai/entrypoints.py +33 -0
- memframe_ai/gateway.py +75 -0
- memframe_ai/observe.py +112 -0
- memframe_ai/sessions.py +194 -0
- memframe_ai/tools/__init__.py +11 -0
- memframe_ai/tools/_helpers.py +61 -0
- memframe_ai/tools/arithmetic.py +141 -0
- memframe_ai/tools/clean.py +132 -0
- memframe_ai/tools/context.py +19 -0
- memframe_ai/tools/inspect.py +175 -0
- memframe_ai/tools/plot.py +149 -0
- memframe_ai/tools/select.py +85 -0
- memframe_ai/tools/stats.py +179 -0
- memframe_ai/tools/upload.py +16 -0
- memframe_ai/wrappers.py +34 -0
memframe/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""memFrame - Database-backed DataFrame operations."""
|
|
2
|
+
|
|
3
|
+
from .main import MemFrame
|
|
4
|
+
from .core.ingestion.datatype_detector import Backend
|
|
5
|
+
from .db_manager.setup import DatabaseBackend
|
|
6
|
+
from .db_manager.context import ContextManager
|
|
7
|
+
|
|
8
|
+
__all__ = ["MemFrame", "Backend", "DatabaseBackend", "ContextManager"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Method-call caching for memFrame.
|
|
2
|
+
|
|
3
|
+
The ``record_call`` decorator (a ``CacheManager`` instance) implements the
|
|
4
|
+
two-level data cache used by the orchestrator layer: signature-only logging by
|
|
5
|
+
default, and persistent transient table storage when ``deep_cache`` is enabled.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cache_manager import CacheManager, record_call
|
|
9
|
+
|
|
10
|
+
__all__ = ["CacheManager", "record_call"]
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, Callable, Dict, Optional
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import pyarrow as pa
|
|
11
|
+
|
|
12
|
+
from memframe.core.ingestion.datatype_detector import Backend
|
|
13
|
+
from memframe.exceptions import OperationError
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("memFrame.cache")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CacheManager:
|
|
19
|
+
"""Two-level method-call cache backed by the transient registry.
|
|
20
|
+
|
|
21
|
+
Level 1 (default, deep_cache=False): records arg/kwarg signatures only —
|
|
22
|
+
an audit/lineage log; repeat calls re-execute (no table to replay).
|
|
23
|
+
Level 2 (deep, deep_cache=True): persists result DataFrames as typed
|
|
24
|
+
transient tables; repeat calls load the table and skip execution.
|
|
25
|
+
Resolution order: decorator arg -> MemFrame.deep_cache -> False.
|
|
26
|
+
|
|
27
|
+
The instance itself is the ``record_call`` decorator:
|
|
28
|
+
@record_call
|
|
29
|
+
@record_call()
|
|
30
|
+
@record_call(deep_cache=True)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# ── cache key (signature) ──────────────────────────────────────
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _json_default(o: Any) -> Any:
|
|
36
|
+
if isinstance(o, np.generic):
|
|
37
|
+
return o.item()
|
|
38
|
+
if isinstance(o, np.ndarray):
|
|
39
|
+
return o.tolist()
|
|
40
|
+
if isinstance(o, pd.DataFrame):
|
|
41
|
+
return {"__dataframe__": list(o.shape), "columns": list(o.columns)}
|
|
42
|
+
if isinstance(o, pd.Series):
|
|
43
|
+
return {"__series__": list(o.shape), "name": str(o.name)}
|
|
44
|
+
return f"<{type(o).__name__}>"
|
|
45
|
+
|
|
46
|
+
def _signature(self, value: Any) -> str:
|
|
47
|
+
return json.dumps(value, sort_keys=True, default=self._json_default)
|
|
48
|
+
|
|
49
|
+
def _qualify(self, schema: str, bare: str, backend) -> str:
|
|
50
|
+
if getattr(backend, "backend", None) == Backend.CLICKHOUSE:
|
|
51
|
+
return f"`{schema}`.`{bare}`"
|
|
52
|
+
return f'{schema}."{bare}"'
|
|
53
|
+
|
|
54
|
+
# ── table load / store ─────────────────────────────────────────
|
|
55
|
+
async def _load_generated_table(self, backend, qualified: str) -> Optional[pd.DataFrame]:
|
|
56
|
+
start = time.perf_counter()
|
|
57
|
+
try:
|
|
58
|
+
rows = await backend.fetch(f"SELECT * FROM {qualified}")
|
|
59
|
+
if not rows:
|
|
60
|
+
return None
|
|
61
|
+
# ponytail: per-backend column name query — real backend difference
|
|
62
|
+
be = getattr(backend, "backend", None)
|
|
63
|
+
if be == Backend.POSTGRES:
|
|
64
|
+
schema, table = qualified.replace('"', "").split(".")
|
|
65
|
+
col_rows = await backend.fetch(
|
|
66
|
+
"SELECT column_name FROM information_schema.columns "
|
|
67
|
+
"WHERE table_schema = $1 AND table_name = $2",
|
|
68
|
+
schema.strip(), table.strip(),
|
|
69
|
+
)
|
|
70
|
+
col_names = [r[0] for r in col_rows]
|
|
71
|
+
elif be == Backend.CLICKHOUSE:
|
|
72
|
+
col_rows = await backend.fetch(f"DESCRIBE TABLE {qualified}")
|
|
73
|
+
col_names = [r[0] for r in col_rows]
|
|
74
|
+
else:
|
|
75
|
+
col_rows = await backend.fetch(f"DESCRIBE {qualified}")
|
|
76
|
+
col_names = [r[0] for r in col_rows]
|
|
77
|
+
df = pd.DataFrame(rows, columns=col_names)
|
|
78
|
+
logger.debug(
|
|
79
|
+
"[cache] reloaded %s (%d rows, %.3fs)", qualified, len(df),
|
|
80
|
+
time.perf_counter() - start,
|
|
81
|
+
)
|
|
82
|
+
return df
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
logger.warning("[cache] reload failed for %s: %s", qualified, exc)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
async def _create_deep_cache_table(self, mf, backend, data_id: str, df: pd.DataFrame) -> str:
|
|
88
|
+
"""Persist a DataFrame as a typed transient table (deep-cache payload)."""
|
|
89
|
+
op = await backend.fetchval(
|
|
90
|
+
f"SELECT COALESCE(MAX(opidx), 0) FROM {backend.transient_registry_table} "
|
|
91
|
+
f"WHERE data_id = {backend.placeholder(1)}", data_id,
|
|
92
|
+
)
|
|
93
|
+
op = (op or 0) + 1
|
|
94
|
+
qualified = backend.get_transient_table_name(data_id, op)
|
|
95
|
+
arrow = pa.Table.from_pandas(df, preserve_index=False)
|
|
96
|
+
uploader = mf._uploader
|
|
97
|
+
schema = {}
|
|
98
|
+
for i, name in enumerate(arrow.schema.names):
|
|
99
|
+
pg_type = uploader._arrow_type_to_postgres(arrow.schema.field(i).type)
|
|
100
|
+
schema[name] = {
|
|
101
|
+
"postgres_type": pg_type,
|
|
102
|
+
"clickhouse_type": uploader._postgres_type_to_clickhouse(pg_type),
|
|
103
|
+
"is_nullable": True,
|
|
104
|
+
}
|
|
105
|
+
await uploader._create_final_table_typed(qualified, arrow.schema.names, schema)
|
|
106
|
+
await uploader._insert_arrow_table(qualified, arrow)
|
|
107
|
+
return qualified
|
|
108
|
+
|
|
109
|
+
def _make_hit_response(self, payload: Dict[str, Any], cached: pd.DataFrame, bare_name: str, schema: str) -> Dict[str, Any]:
|
|
110
|
+
qualified = self._qualify(schema, bare_name, payload.get("_backend"))
|
|
111
|
+
return {
|
|
112
|
+
"is_error": False,
|
|
113
|
+
"message": (
|
|
114
|
+
f"Cache hit for {payload['class_name']}."
|
|
115
|
+
f"{payload['method_name']}; "
|
|
116
|
+
f"reused generated table '{bare_name}'"
|
|
117
|
+
),
|
|
118
|
+
"error_message": None,
|
|
119
|
+
"involved_cols": [],
|
|
120
|
+
"generated_cols": list(cached.columns),
|
|
121
|
+
"result": cached,
|
|
122
|
+
"new_table": bare_name,
|
|
123
|
+
"result_metadata": {
|
|
124
|
+
"from_cache": True,
|
|
125
|
+
"saved_table": qualified,
|
|
126
|
+
"row_count": len(cached),
|
|
127
|
+
"column_count": len(cached.columns),
|
|
128
|
+
"strict_args_kwargs_match": True,
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# ── decorator ──────────────────────────────────────────────────
|
|
133
|
+
def __call__(self, func: Optional[Callable] = None, deep_cache: Optional[bool] = None):
|
|
134
|
+
if func is not None:
|
|
135
|
+
return self._make_decorator(func, deep_cache)
|
|
136
|
+
return lambda f: self._make_decorator(f, deep_cache)
|
|
137
|
+
|
|
138
|
+
def _make_writer(self, mf):
|
|
139
|
+
async def writer(payload, data_id, generated_table_name=None, is_deep_cache=False, schema=None):
|
|
140
|
+
await mf._arecord_method_call(
|
|
141
|
+
data_id=data_id,
|
|
142
|
+
class_name=payload["class_name"],
|
|
143
|
+
method_name=payload["method_name"],
|
|
144
|
+
args_sig=payload["args_sig"],
|
|
145
|
+
kwargs_sig=payload["kwargs_sig"],
|
|
146
|
+
generated_table_name=generated_table_name,
|
|
147
|
+
is_deep_cache=is_deep_cache,
|
|
148
|
+
schema=schema,
|
|
149
|
+
)
|
|
150
|
+
return writer
|
|
151
|
+
|
|
152
|
+
def _make_decorator(self, func: Callable, decorator_deep_cache: Optional[bool]):
|
|
153
|
+
manager = self
|
|
154
|
+
|
|
155
|
+
if asyncio.iscoroutinefunction(func):
|
|
156
|
+
@wraps(func)
|
|
157
|
+
async def async_wrapper(self, *args, **kwargs):
|
|
158
|
+
mf = getattr(self, "_memframe", None)
|
|
159
|
+
if mf is None:
|
|
160
|
+
raise OperationError(
|
|
161
|
+
f"Cannot cache {func.__qualname__}: instance lacks `_memframe`. "
|
|
162
|
+
"Inherit from LoggableMixin or set `self._memframe`."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
data_id = getattr(self, "_data_id", None) or mf._active_id
|
|
166
|
+
if not data_id:
|
|
167
|
+
raise OperationError(
|
|
168
|
+
f"Cannot cache {func.__qualname__}: no data_id available."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# ponytail: master switch — MemFrame(deep_cache=False) overrides all
|
|
172
|
+
mf_deep = getattr(mf, "deep_cache", None)
|
|
173
|
+
is_deep = False
|
|
174
|
+
if decorator_deep_cache is not None:
|
|
175
|
+
is_deep = decorator_deep_cache
|
|
176
|
+
elif mf_deep is True:
|
|
177
|
+
is_deep = True
|
|
178
|
+
if mf_deep is False:
|
|
179
|
+
is_deep = False
|
|
180
|
+
backend = getattr(mf, "_backend", None)
|
|
181
|
+
|
|
182
|
+
writer = getattr(self, "_call_writer", None)
|
|
183
|
+
if writer is None:
|
|
184
|
+
writer = manager._make_writer(mf)
|
|
185
|
+
self._call_writer = writer
|
|
186
|
+
|
|
187
|
+
payload = {
|
|
188
|
+
"class_name": self.__class__.__name__,
|
|
189
|
+
"method_name": func.__name__,
|
|
190
|
+
"args_sig": manager._signature(args),
|
|
191
|
+
"kwargs_sig": manager._signature(kwargs),
|
|
192
|
+
"_backend": backend,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# --- Lookup: only for deep cache entries ---
|
|
196
|
+
if is_deep and backend:
|
|
197
|
+
method = f"{payload['class_name']}.{payload['method_name']}"
|
|
198
|
+
logger.debug(
|
|
199
|
+
"[cache] LOOKUP %s data_id=%s args=%s kwargs=%s",
|
|
200
|
+
method, data_id, payload["args_sig"], payload["kwargs_sig"],
|
|
201
|
+
)
|
|
202
|
+
lookup_start = time.perf_counter()
|
|
203
|
+
row = await backend.fetch_row(
|
|
204
|
+
f"""
|
|
205
|
+
SELECT generated_table_name, schema
|
|
206
|
+
FROM {backend.transient_registry_table}
|
|
207
|
+
WHERE data_id = {backend.placeholder(1)}
|
|
208
|
+
AND operation_type = 'method_call'
|
|
209
|
+
AND class_name = {backend.placeholder(2)}
|
|
210
|
+
AND method_name = {backend.placeholder(3)}
|
|
211
|
+
AND args = {backend.placeholder(4)}
|
|
212
|
+
AND kwargs = {backend.placeholder(5)}
|
|
213
|
+
AND {backend.backend == "clickhouse" and "CAST(is_deep_cache AS UInt8) = 1" or "is_deep_cache = TRUE"}
|
|
214
|
+
AND generated_table_name IS NOT NULL
|
|
215
|
+
ORDER BY opidx DESC LIMIT 1
|
|
216
|
+
""",
|
|
217
|
+
data_id,
|
|
218
|
+
payload["class_name"],
|
|
219
|
+
payload["method_name"],
|
|
220
|
+
payload["args_sig"],
|
|
221
|
+
payload["kwargs_sig"],
|
|
222
|
+
)
|
|
223
|
+
if row:
|
|
224
|
+
bare, sch = row[0], row[1]
|
|
225
|
+
sch = sch or backend.transient_schema
|
|
226
|
+
cached = await manager._load_generated_table(backend, manager._qualify(sch, bare, backend))
|
|
227
|
+
if cached is not None:
|
|
228
|
+
logger.info(
|
|
229
|
+
"[cache] HIT %s data_id=%s → %s (%d rows, lookup+reload %.3fs)",
|
|
230
|
+
method, data_id, bare, len(cached),
|
|
231
|
+
time.perf_counter() - lookup_start,
|
|
232
|
+
)
|
|
233
|
+
return manager._make_hit_response(payload, cached, bare, sch)
|
|
234
|
+
logger.info(
|
|
235
|
+
"[cache] MISS %s data_id=%s — registry row found but reload failed for %s",
|
|
236
|
+
method, data_id, bare,
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
logger.info(
|
|
240
|
+
"[cache] MISS %s data_id=%s — no matching registry row",
|
|
241
|
+
method, data_id,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# --- Execute ---
|
|
245
|
+
result = await func(self, *args, **kwargs)
|
|
246
|
+
generated_table_name = None
|
|
247
|
+
schema = None
|
|
248
|
+
|
|
249
|
+
if is_deep and isinstance(result, dict) and not result.get("is_error", False):
|
|
250
|
+
bare_table = result.get("new_table") or result.get("generated_table_name")
|
|
251
|
+
if bare_table and backend:
|
|
252
|
+
if await backend.table_exists(manager._qualify(backend.transient_schema, bare_table, backend)):
|
|
253
|
+
generated_table_name = bare_table
|
|
254
|
+
schema = backend.transient_schema
|
|
255
|
+
elif await backend.table_exists(manager._qualify("transient", bare_table, backend)):
|
|
256
|
+
generated_table_name = bare_table
|
|
257
|
+
schema = "transient"
|
|
258
|
+
elif await backend.table_exists(manager._qualify(backend.upload_schema, bare_table, backend)):
|
|
259
|
+
be = getattr(backend, "backend", None)
|
|
260
|
+
if be == Backend.CLICKHOUSE:
|
|
261
|
+
await backend.execute(
|
|
262
|
+
f"RENAME TABLE {manager._qualify(backend.upload_schema, bare_table, backend)} "
|
|
263
|
+
f"TO {manager._qualify(backend.transient_schema, bare_table, backend)}"
|
|
264
|
+
)
|
|
265
|
+
elif be == Backend.DUCKDB:
|
|
266
|
+
await backend.execute(
|
|
267
|
+
f"CREATE TABLE {manager._qualify(backend.transient_schema, bare_table, backend)} "
|
|
268
|
+
f"AS SELECT * FROM {manager._qualify(backend.upload_schema, bare_table, backend)}"
|
|
269
|
+
)
|
|
270
|
+
await backend.execute(
|
|
271
|
+
f"DROP TABLE {manager._qualify(backend.upload_schema, bare_table, backend)}"
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
await backend.execute(
|
|
275
|
+
f"ALTER TABLE {manager._qualify(backend.upload_schema, bare_table, backend)} "
|
|
276
|
+
f"SET SCHEMA {backend.transient_schema}"
|
|
277
|
+
)
|
|
278
|
+
generated_table_name = bare_table
|
|
279
|
+
schema = backend.transient_schema
|
|
280
|
+
elif "result" in result:
|
|
281
|
+
df = result["result"]
|
|
282
|
+
if isinstance(df, pd.DataFrame) and not df.empty and backend:
|
|
283
|
+
qualified = await manager._create_deep_cache_table(
|
|
284
|
+
mf, backend, data_id, df
|
|
285
|
+
)
|
|
286
|
+
if qualified:
|
|
287
|
+
_sch, _bare = backend._split_qualified_table_name(qualified)
|
|
288
|
+
generated_table_name = _bare
|
|
289
|
+
schema = _sch
|
|
290
|
+
elif not is_deep and isinstance(result, dict) and not result.get("is_error", False):
|
|
291
|
+
# ponytail: drop any table the method created — deep_cache=False means no tables
|
|
292
|
+
bare_table = result.get("new_table") or result.get("generated_table_name")
|
|
293
|
+
if bare_table and backend:
|
|
294
|
+
for sch in (backend.transient_schema, "transient", backend.upload_schema):
|
|
295
|
+
q = manager._qualify(sch, bare_table, backend)
|
|
296
|
+
if q and await backend.table_exists(q):
|
|
297
|
+
await backend.drop_table(q)
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
await writer(payload, data_id, generated_table_name, is_deep_cache=is_deep, schema=schema)
|
|
301
|
+
if is_deep and generated_table_name:
|
|
302
|
+
logger.info(
|
|
303
|
+
"[cache] STORE %s.%s data_id=%s → %s.%s (deep)",
|
|
304
|
+
payload["class_name"], payload["method_name"],
|
|
305
|
+
data_id, schema, generated_table_name,
|
|
306
|
+
)
|
|
307
|
+
elif not is_deep:
|
|
308
|
+
logger.debug(
|
|
309
|
+
"[cache] STORE %s.%s data_id=%s (signature-only)",
|
|
310
|
+
payload["class_name"], payload["method_name"], data_id,
|
|
311
|
+
)
|
|
312
|
+
return result
|
|
313
|
+
return async_wrapper
|
|
314
|
+
else:
|
|
315
|
+
@wraps(func)
|
|
316
|
+
def sync_wrapper(self, *args, **kwargs):
|
|
317
|
+
return func(self, *args, **kwargs)
|
|
318
|
+
return sync_wrapper
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
record_call = CacheManager()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core operations package for memFrame."""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Shared response envelope for analytix operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from memframe.exceptions import OperationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OperationResponse(BaseModel):
|
|
13
|
+
"""Validated internal representation of an operation response."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(
|
|
16
|
+
extra="allow",
|
|
17
|
+
arbitrary_types_allowed=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
is_error: bool
|
|
21
|
+
message: str = ""
|
|
22
|
+
error_message: str | None = None
|
|
23
|
+
involved_cols: list[str] = Field(default_factory=list)
|
|
24
|
+
generated_cols: list[str] = Field(default_factory=list)
|
|
25
|
+
result: Any = None
|
|
26
|
+
|
|
27
|
+
def to_payload(self) -> dict[str, Any]:
|
|
28
|
+
"""Return the existing dictionary API payload without dropping None."""
|
|
29
|
+
return self.model_dump(exclude_none=False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_operation_response(value: Any) -> bool:
|
|
33
|
+
"""Return whether a value is a canonical analytix response payload."""
|
|
34
|
+
return isinstance(value, dict) and {
|
|
35
|
+
"is_error",
|
|
36
|
+
"error_message",
|
|
37
|
+
"result",
|
|
38
|
+
}.issubset(value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def unwrap_response(value: Any) -> Any:
|
|
42
|
+
"""Return a public operation value or raise its operation error."""
|
|
43
|
+
if not is_operation_response(value):
|
|
44
|
+
return value
|
|
45
|
+
if value["is_error"]:
|
|
46
|
+
raise OperationError(
|
|
47
|
+
value.get("error_message") or value.get("message") or "Operation failed"
|
|
48
|
+
)
|
|
49
|
+
if value.get("result") is None and value.get("iterator") is not None:
|
|
50
|
+
return value["iterator"]
|
|
51
|
+
return value["result"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Keep the argument order used by the existing analytix response builders.
|
|
55
|
+
def ok(
|
|
56
|
+
message: str = "",
|
|
57
|
+
involved_cols: list[str] | None = None,
|
|
58
|
+
generated_cols: list[str] | None = None,
|
|
59
|
+
result: Any = None,
|
|
60
|
+
**extra: Any,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
"""Validate and build a successful response payload."""
|
|
63
|
+
return OperationResponse(
|
|
64
|
+
is_error=False,
|
|
65
|
+
message=message,
|
|
66
|
+
error_message=None,
|
|
67
|
+
involved_cols=involved_cols or [],
|
|
68
|
+
generated_cols=generated_cols or [],
|
|
69
|
+
result=result,
|
|
70
|
+
**extra,
|
|
71
|
+
).to_payload()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fail(
|
|
75
|
+
error_message: str,
|
|
76
|
+
involved_cols: list[str] | None = None,
|
|
77
|
+
generated_cols: list[str] | None = None,
|
|
78
|
+
**extra: Any,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
"""Validate and build a failed response payload."""
|
|
81
|
+
return OperationResponse(
|
|
82
|
+
is_error=True,
|
|
83
|
+
message="",
|
|
84
|
+
error_message=error_message,
|
|
85
|
+
involved_cols=involved_cols or [],
|
|
86
|
+
generated_cols=generated_cols or [],
|
|
87
|
+
result=None,
|
|
88
|
+
**extra,
|
|
89
|
+
).to_payload()
|