aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a3__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.
- aethergraph/__init__.py +4 -10
- aethergraph/__main__.py +296 -0
- aethergraph/api/v1/__init__.py +0 -0
- aethergraph/api/v1/agents.py +46 -0
- aethergraph/api/v1/apps.py +70 -0
- aethergraph/api/v1/artifacts.py +415 -0
- aethergraph/api/v1/channels.py +89 -0
- aethergraph/api/v1/deps.py +168 -0
- aethergraph/api/v1/graphs.py +259 -0
- aethergraph/api/v1/identity.py +25 -0
- aethergraph/api/v1/memory.py +353 -0
- aethergraph/api/v1/misc.py +47 -0
- aethergraph/api/v1/pagination.py +29 -0
- aethergraph/api/v1/runs.py +568 -0
- aethergraph/api/v1/schemas.py +535 -0
- aethergraph/api/v1/session.py +323 -0
- aethergraph/api/v1/stats.py +201 -0
- aethergraph/api/v1/viz.py +152 -0
- aethergraph/config/config.py +22 -0
- aethergraph/config/loader.py +3 -2
- aethergraph/config/storage.py +209 -0
- aethergraph/contracts/__init__.py +0 -0
- aethergraph/contracts/services/__init__.py +0 -0
- aethergraph/contracts/services/artifacts.py +27 -14
- aethergraph/contracts/services/memory.py +45 -17
- aethergraph/contracts/services/metering.py +129 -0
- aethergraph/contracts/services/runs.py +50 -0
- aethergraph/contracts/services/sessions.py +87 -0
- aethergraph/contracts/services/state_stores.py +3 -0
- aethergraph/contracts/services/viz.py +44 -0
- aethergraph/contracts/storage/artifact_index.py +88 -0
- aethergraph/contracts/storage/artifact_store.py +99 -0
- aethergraph/contracts/storage/async_kv.py +34 -0
- aethergraph/contracts/storage/blob_store.py +50 -0
- aethergraph/contracts/storage/doc_store.py +35 -0
- aethergraph/contracts/storage/event_log.py +31 -0
- aethergraph/contracts/storage/vector_index.py +48 -0
- aethergraph/core/__init__.py +0 -0
- aethergraph/core/execution/forward_scheduler.py +13 -2
- aethergraph/core/execution/global_scheduler.py +21 -15
- aethergraph/core/execution/step_forward.py +10 -1
- aethergraph/core/graph/__init__.py +0 -0
- aethergraph/core/graph/graph_builder.py +8 -4
- aethergraph/core/graph/graph_fn.py +156 -15
- aethergraph/core/graph/graph_spec.py +8 -0
- aethergraph/core/graph/graphify.py +146 -27
- aethergraph/core/graph/node_spec.py +0 -2
- aethergraph/core/graph/node_state.py +3 -0
- aethergraph/core/graph/task_graph.py +39 -1
- aethergraph/core/runtime/__init__.py +0 -0
- aethergraph/core/runtime/ad_hoc_context.py +64 -4
- aethergraph/core/runtime/base_service.py +28 -4
- aethergraph/core/runtime/execution_context.py +13 -15
- aethergraph/core/runtime/graph_runner.py +222 -37
- aethergraph/core/runtime/node_context.py +510 -6
- aethergraph/core/runtime/node_services.py +12 -5
- aethergraph/core/runtime/recovery.py +15 -1
- aethergraph/core/runtime/run_manager.py +783 -0
- aethergraph/core/runtime/run_manager_local.py +204 -0
- aethergraph/core/runtime/run_registration.py +2 -2
- aethergraph/core/runtime/run_types.py +89 -0
- aethergraph/core/runtime/runtime_env.py +136 -7
- aethergraph/core/runtime/runtime_metering.py +71 -0
- aethergraph/core/runtime/runtime_registry.py +36 -13
- aethergraph/core/runtime/runtime_services.py +194 -6
- aethergraph/core/tools/builtins/toolset.py +1 -1
- aethergraph/core/tools/toolkit.py +5 -0
- aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
- aethergraph/plugins/agents/default_chat_agent.py +171 -0
- aethergraph/plugins/agents/shared.py +81 -0
- aethergraph/plugins/channel/adapters/webui.py +112 -112
- aethergraph/plugins/channel/routes/webui_routes.py +367 -102
- aethergraph/plugins/channel/utils/slack_utils.py +115 -59
- aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
- aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
- aethergraph/runtime/__init__.py +15 -0
- aethergraph/server/app_factory.py +196 -34
- aethergraph/server/clients/channel_client.py +202 -0
- aethergraph/server/http/channel_http_routes.py +116 -0
- aethergraph/server/http/channel_ws_routers.py +45 -0
- aethergraph/server/loading.py +117 -0
- aethergraph/server/server.py +131 -0
- aethergraph/server/server_state.py +240 -0
- aethergraph/server/start.py +227 -66
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
- aethergraph/server/ui_static/index.html +15 -0
- aethergraph/server/ui_static/logo.png +0 -0
- aethergraph/services/artifacts/__init__.py +0 -0
- aethergraph/services/artifacts/facade.py +1239 -132
- aethergraph/services/auth/{dev.py → authn.py} +0 -8
- aethergraph/services/auth/authz.py +100 -0
- aethergraph/services/channel/__init__.py +0 -0
- aethergraph/services/channel/channel_bus.py +19 -1
- aethergraph/services/channel/factory.py +13 -1
- aethergraph/services/channel/ingress.py +311 -0
- aethergraph/services/channel/queue_adapter.py +75 -0
- aethergraph/services/channel/session.py +502 -19
- aethergraph/services/container/default_container.py +122 -43
- aethergraph/services/continuations/continuation.py +6 -0
- aethergraph/services/continuations/stores/fs_store.py +19 -0
- aethergraph/services/eventhub/event_hub.py +76 -0
- aethergraph/services/kv/__init__.py +0 -0
- aethergraph/services/kv/ephemeral.py +244 -0
- aethergraph/services/llm/__init__.py +0 -0
- aethergraph/services/llm/generic_client copy.py +691 -0
- aethergraph/services/llm/generic_client.py +1288 -187
- aethergraph/services/llm/providers.py +3 -1
- aethergraph/services/llm/types.py +47 -0
- aethergraph/services/llm/utils.py +284 -0
- aethergraph/services/logger/std.py +3 -0
- aethergraph/services/mcp/__init__.py +9 -0
- aethergraph/services/mcp/http_client.py +38 -0
- aethergraph/services/mcp/service.py +225 -1
- aethergraph/services/mcp/stdio_client.py +41 -6
- aethergraph/services/mcp/ws_client.py +44 -2
- aethergraph/services/memory/__init__.py +0 -0
- aethergraph/services/memory/distillers/llm_long_term.py +234 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
- aethergraph/services/memory/distillers/long_term.py +225 -0
- aethergraph/services/memory/facade/__init__.py +3 -0
- aethergraph/services/memory/facade/chat.py +440 -0
- aethergraph/services/memory/facade/core.py +447 -0
- aethergraph/services/memory/facade/distillation.py +424 -0
- aethergraph/services/memory/facade/rag.py +410 -0
- aethergraph/services/memory/facade/results.py +315 -0
- aethergraph/services/memory/facade/retrieval.py +139 -0
- aethergraph/services/memory/facade/types.py +77 -0
- aethergraph/services/memory/facade/utils.py +43 -0
- aethergraph/services/memory/facade_dep.py +1539 -0
- aethergraph/services/memory/factory.py +9 -3
- aethergraph/services/memory/utils.py +10 -0
- aethergraph/services/metering/eventlog_metering.py +470 -0
- aethergraph/services/metering/noop.py +25 -4
- aethergraph/services/rag/__init__.py +0 -0
- aethergraph/services/rag/facade.py +279 -23
- aethergraph/services/rag/index_factory.py +2 -2
- aethergraph/services/rag/node_rag.py +317 -0
- aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
- aethergraph/services/registry/__init__.py +0 -0
- aethergraph/services/registry/agent_app_meta.py +419 -0
- aethergraph/services/registry/registry_key.py +1 -1
- aethergraph/services/registry/unified_registry.py +74 -6
- aethergraph/services/scope/scope.py +159 -0
- aethergraph/services/scope/scope_factory.py +164 -0
- aethergraph/services/state_stores/serialize.py +5 -0
- aethergraph/services/state_stores/utils.py +2 -1
- aethergraph/services/viz/__init__.py +0 -0
- aethergraph/services/viz/facade.py +413 -0
- aethergraph/services/viz/viz_service.py +69 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
- aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
- aethergraph/storage/artifacts/cas_store.py +422 -0
- aethergraph/storage/artifacts/fs_cas.py +18 -0
- aethergraph/storage/artifacts/s3_cas.py +14 -0
- aethergraph/storage/artifacts/utils.py +124 -0
- aethergraph/storage/blob/fs_blob.py +86 -0
- aethergraph/storage/blob/s3_blob.py +115 -0
- aethergraph/storage/continuation_store/fs_cont.py +283 -0
- aethergraph/storage/continuation_store/inmem_cont.py +146 -0
- aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
- aethergraph/storage/docstore/fs_doc.py +63 -0
- aethergraph/storage/docstore/sqlite_doc.py +31 -0
- aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
- aethergraph/storage/eventlog/fs_event.py +136 -0
- aethergraph/storage/eventlog/sqlite_event.py +47 -0
- aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
- aethergraph/storage/factory.py +432 -0
- aethergraph/storage/fs_utils.py +28 -0
- aethergraph/storage/graph_state_store/state_store.py +64 -0
- aethergraph/storage/kv/inmem_kv.py +103 -0
- aethergraph/storage/kv/layered_kv.py +52 -0
- aethergraph/storage/kv/sqlite_kv.py +39 -0
- aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
- aethergraph/storage/memory/event_persist.py +68 -0
- aethergraph/storage/memory/fs_persist.py +118 -0
- aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
- aethergraph/{services → storage}/memory/indices.py +31 -7
- aethergraph/storage/metering/meter_event.py +55 -0
- aethergraph/storage/runs/doc_store.py +280 -0
- aethergraph/storage/runs/inmen_store.py +82 -0
- aethergraph/storage/runs/sqlite_run_store.py +403 -0
- aethergraph/storage/sessions/doc_store.py +183 -0
- aethergraph/storage/sessions/inmem_store.py +110 -0
- aethergraph/storage/sessions/sqlite_session_store.py +399 -0
- aethergraph/storage/vector_index/chroma_index.py +138 -0
- aethergraph/storage/vector_index/faiss_index.py +179 -0
- aethergraph/storage/vector_index/sqlite_index.py +187 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a3.dist-info/RECORD +356 -0
- aethergraph-0.1.0a3.dist-info/entry_points.txt +3 -0
- aethergraph/services/artifacts/factory.py +0 -35
- aethergraph/services/artifacts/fs_store.py +0 -656
- aethergraph/services/artifacts/jsonl_index.py +0 -123
- aethergraph/services/artifacts/sqlite_index.py +0 -209
- aethergraph/services/memory/distillers/episode.py +0 -116
- aethergraph/services/memory/distillers/rolling.py +0 -74
- aethergraph/services/memory/facade.py +0 -633
- aethergraph/services/memory/persist_fs.py +0 -40
- aethergraph/services/rag/index/base.py +0 -27
- aethergraph/services/rag/index/faiss_index.py +0 -121
- aethergraph/services/rag/index/sqlite_index.py +0 -134
- aethergraph-0.1.0a1.dist-info/RECORD +0 -182
- aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from aethergraph.contracts.services.continuations import AsyncContinuationStore
|
|
11
|
+
from aethergraph.contracts.services.kv import AsyncKV
|
|
12
|
+
from aethergraph.contracts.storage.doc_store import DocStore
|
|
13
|
+
from aethergraph.contracts.storage.event_log import EventLog
|
|
14
|
+
from aethergraph.services.continuations.continuation import Continuation, Correlator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KVDocContinuationStore(AsyncContinuationStore):
|
|
18
|
+
"""
|
|
19
|
+
Continuation store backed by:
|
|
20
|
+
- DocStore: main continuation document (one per (run_id, node_id))
|
|
21
|
+
- AsyncKV: token and correlator indices
|
|
22
|
+
- EventLog: (optional) audit trail of continuation events
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
doc_store: DocStore,
|
|
29
|
+
kv: AsyncKV,
|
|
30
|
+
event_log: EventLog | None = None,
|
|
31
|
+
secret: bytes,
|
|
32
|
+
namespace: str = "cont",
|
|
33
|
+
):
|
|
34
|
+
self._docs = doc_store
|
|
35
|
+
self._kv = kv
|
|
36
|
+
self._log = event_log
|
|
37
|
+
self._secret = secret
|
|
38
|
+
self._ns = namespace.rstrip("/") # namespace prefix for KV keys
|
|
39
|
+
|
|
40
|
+
# ---------- key helpers ----------
|
|
41
|
+
def _cont_id(self, run_id: str, node_id: str) -> str:
|
|
42
|
+
# one doc per continuation
|
|
43
|
+
return f"{self._ns}/runs/{run_id}/nodes/{node_id}"
|
|
44
|
+
|
|
45
|
+
def _token_key(self, token: str) -> str:
|
|
46
|
+
return f"{self._ns}:token:{token}"
|
|
47
|
+
|
|
48
|
+
def _corr_key(self, corr: Correlator) -> str:
|
|
49
|
+
scheme, channel, thread, message = corr.key()
|
|
50
|
+
return f"{self._ns}:corr:{scheme}:{channel}:{thread}:{message}"
|
|
51
|
+
|
|
52
|
+
# ---------- token helpers ----------
|
|
53
|
+
def _hmac(self, *parts: str) -> str:
|
|
54
|
+
h = hmac.new(self._secret, digestmod=hashlib.sha256)
|
|
55
|
+
for p in parts:
|
|
56
|
+
h.update(p.encode("utf-8"))
|
|
57
|
+
return h.hexdigest()
|
|
58
|
+
|
|
59
|
+
async def mint_token(self, run_id: str, node_id: str, attempts: int) -> str:
|
|
60
|
+
token = self._hmac(run_id, node_id, str(attempts), os.urandom(8).hex())
|
|
61
|
+
return token
|
|
62
|
+
|
|
63
|
+
# ---------- main methods ----------
|
|
64
|
+
async def save(self, cont: Continuation) -> None:
|
|
65
|
+
payload = cont.to_dict() if hasattr(cont, "to_dict") else asdict(cont)
|
|
66
|
+
|
|
67
|
+
# Normalize datetime fields to ISO format
|
|
68
|
+
for k in ("deadline", "next_wakeup_at", "created_at"):
|
|
69
|
+
v = payload.get(k)
|
|
70
|
+
if isinstance(v, datetime):
|
|
71
|
+
payload[k] = v.astimezone(timezone.utc).isoformat()
|
|
72
|
+
|
|
73
|
+
doc_id = self._cont_id(cont.run_id, cont.node_id)
|
|
74
|
+
await self._docs.put(doc_id, payload)
|
|
75
|
+
|
|
76
|
+
# token -> (run_id, node_id)
|
|
77
|
+
await self._kv.set(
|
|
78
|
+
self._token_key(cont.token),
|
|
79
|
+
{"run_id": cont.run_id, "node_id": cont.node_id},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if self._log is not None:
|
|
83
|
+
evt = {
|
|
84
|
+
"scope_id": cont.run_id,
|
|
85
|
+
"kind": "continuation.save",
|
|
86
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
87
|
+
"tags": [cont.channel or "", cont.kind or ""],
|
|
88
|
+
"payload": payload,
|
|
89
|
+
}
|
|
90
|
+
await self._log.append(evt)
|
|
91
|
+
|
|
92
|
+
async def _doc_to_cont(self, data: dict[str, Any] | None) -> Continuation | None:
|
|
93
|
+
if data is None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
for k in ("deadline", "next_wakeup_at", "created_at"):
|
|
97
|
+
v = data.get(k)
|
|
98
|
+
if isinstance(v, str):
|
|
99
|
+
data[k] = datetime.fromisoformat(v)
|
|
100
|
+
|
|
101
|
+
data["closed"] = bool(data.get("closed", False))
|
|
102
|
+
return Continuation(**data)
|
|
103
|
+
|
|
104
|
+
async def get(self, run_id: str, node_id: str) -> Continuation | None:
|
|
105
|
+
doc_id = self._cont_id(run_id, node_id)
|
|
106
|
+
data = await self._docs.get(doc_id)
|
|
107
|
+
return await self._doc_to_cont(data)
|
|
108
|
+
|
|
109
|
+
async def list_cont_by_run(self, run_id: str) -> list[Continuation]:
|
|
110
|
+
prefix = f"{self._ns}/runs/{run_id}/nodes/"
|
|
111
|
+
ids = await self._docs.list()
|
|
112
|
+
out: list[Continuation] = []
|
|
113
|
+
for doc_id in ids:
|
|
114
|
+
if not doc_id.startswith(prefix):
|
|
115
|
+
continue
|
|
116
|
+
data = await self._docs.get(doc_id)
|
|
117
|
+
cont = await self._doc_to_cont(data)
|
|
118
|
+
if cont is not None:
|
|
119
|
+
out.append(cont)
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
async def delete(self, run_id: str, node_id: str) -> None:
|
|
123
|
+
cont = await self.get(run_id, node_id)
|
|
124
|
+
doc_id = self._cont_id(run_id, node_id)
|
|
125
|
+
await self._docs.delete(doc_id)
|
|
126
|
+
if cont:
|
|
127
|
+
# best-effort delete of token mapping
|
|
128
|
+
await self._kv.delete(self._token_key(cont.token))
|
|
129
|
+
|
|
130
|
+
# ---------- token methods ----------
|
|
131
|
+
async def get_by_token(self, token: str) -> Continuation | None:
|
|
132
|
+
ref = await self._kv.get(self._token_key(token), default=None)
|
|
133
|
+
if not ref:
|
|
134
|
+
return None
|
|
135
|
+
run_id = ref.get("run_id")
|
|
136
|
+
node_id = ref.get("node_id")
|
|
137
|
+
if not run_id or not node_id:
|
|
138
|
+
return None
|
|
139
|
+
return await self.get(run_id, node_id)
|
|
140
|
+
|
|
141
|
+
async def mark_closed(self, token: str) -> None:
|
|
142
|
+
cont = await self.get_by_token(token)
|
|
143
|
+
if not cont:
|
|
144
|
+
return
|
|
145
|
+
if not cont.closed:
|
|
146
|
+
cont.closed = True
|
|
147
|
+
await self.save(cont)
|
|
148
|
+
|
|
149
|
+
async def verify_token(self, run_id: str, node_id: str, token: str) -> bool:
|
|
150
|
+
cont = await self.get(run_id, node_id)
|
|
151
|
+
return bool(cont and hmac.compare_digest(token, cont.token))
|
|
152
|
+
|
|
153
|
+
# ---------- correlator methods ----------
|
|
154
|
+
async def bind_correlator(self, *, token: str, corr: str) -> None:
|
|
155
|
+
key = self._corr_key(corr)
|
|
156
|
+
toks: list[str] = await self._kv.get(key, default=[]) or []
|
|
157
|
+
if token not in toks:
|
|
158
|
+
toks.append(token)
|
|
159
|
+
await self._kv.set(key, toks)
|
|
160
|
+
|
|
161
|
+
# TODO: consider reverse mapping: token -> correlators for easier cleanup
|
|
162
|
+
# for now we don't need that
|
|
163
|
+
|
|
164
|
+
async def find_by_correlator(self, *, corr: Correlator) -> list[Continuation] | None:
|
|
165
|
+
"""
|
|
166
|
+
Find all continuations matching the correlator. Following the method:
|
|
167
|
+
- get all tokens for the correlator
|
|
168
|
+
- fetch each continuation by token
|
|
169
|
+
- filter out closed or expired continuations
|
|
170
|
+
- return the first valid continuation found (or None)
|
|
171
|
+
"""
|
|
172
|
+
key = self._corr_key(corr)
|
|
173
|
+
toks: list[str] = await self._kv.get(key, default=[]) or []
|
|
174
|
+
from datetime import datetime as dt, timezone as tz
|
|
175
|
+
|
|
176
|
+
for tok in reversed(toks):
|
|
177
|
+
cont = await self.get_by_token(tok)
|
|
178
|
+
if not cont or cont.closed:
|
|
179
|
+
continue
|
|
180
|
+
if cont.deadline and dt.now(tz.utc) > cont.deadline.astimezone(tz.utc):
|
|
181
|
+
continue
|
|
182
|
+
return cont
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
# ---------- scans ----------
|
|
186
|
+
async def last_open(self, *, channel: str, kind: str) -> Continuation | None:
|
|
187
|
+
"""
|
|
188
|
+
Slow scan (like FS version) – OK for dev scale.
|
|
189
|
+
We scan all docs and pick the most recent created_at with matching channel/kind.
|
|
190
|
+
NOTE: this is only for debugging and development purposes. Do not use in production!
|
|
191
|
+
"""
|
|
192
|
+
ids = await self._docs.list()
|
|
193
|
+
best: Continuation | None = None
|
|
194
|
+
best_ts: float | None = None
|
|
195
|
+
|
|
196
|
+
for doc_id in ids:
|
|
197
|
+
if not doc_id.startswith(f"{self._ns}/runs/"):
|
|
198
|
+
continue
|
|
199
|
+
data = await self._docs.get(doc_id)
|
|
200
|
+
cont = await self._doc_to_cont(data)
|
|
201
|
+
if not cont or cont.closed:
|
|
202
|
+
continue
|
|
203
|
+
if cont.channel != channel or cont.kind != kind:
|
|
204
|
+
continue
|
|
205
|
+
created = cont.created_at or datetime.min.replace(tzinf=timezone.utc)
|
|
206
|
+
ts = created.timestamp()
|
|
207
|
+
if best_ts is None or ts > best_ts:
|
|
208
|
+
best = cont
|
|
209
|
+
best_ts = ts
|
|
210
|
+
return best
|
|
211
|
+
|
|
212
|
+
async def list_waits(self) -> list[dict[str, Any]]:
|
|
213
|
+
"""
|
|
214
|
+
Return all continuations as dicts (similar to FS version).
|
|
215
|
+
Caller can filter for waits if needed.
|
|
216
|
+
"""
|
|
217
|
+
ids = await self._docs.list()
|
|
218
|
+
out: list[dict[str, Any]] = []
|
|
219
|
+
for doc_id in ids:
|
|
220
|
+
if not doc_id.startswith(f"{self._ns}/runs/"):
|
|
221
|
+
continue
|
|
222
|
+
data = await self._docs.get(doc_id)
|
|
223
|
+
if data:
|
|
224
|
+
out.append(data)
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
async def clear(self) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Best-effort clear of continuation documents.
|
|
230
|
+
|
|
231
|
+
NOTE: KV indexes may remain unless we have a scan-capable KV implementation.
|
|
232
|
+
They are harmless: get_by_token() will just return None if the doc is gone.
|
|
233
|
+
"""
|
|
234
|
+
# 1) DocStore cleanup
|
|
235
|
+
ids = await self._docs.list()
|
|
236
|
+
for doc_id in ids:
|
|
237
|
+
if doc_id.startswith(f"{self._ns}/runs/"):
|
|
238
|
+
await self._docs.delete(doc_id)
|
|
239
|
+
|
|
240
|
+
# 2) Best-effort KV cleanup if scan_keys is available
|
|
241
|
+
# TODO: implement KV indexes if we have scan capability
|
|
242
|
+
scan = getattr(self._kv, "scan_keys", None)
|
|
243
|
+
if callable(scan):
|
|
244
|
+
token_prefix = f"{self._ns}:token:"
|
|
245
|
+
corr_prefix = f"{self._ns}:corr:"
|
|
246
|
+
|
|
247
|
+
for pfx in (token_prefix, corr_prefix):
|
|
248
|
+
try:
|
|
249
|
+
keys: list[str] = await scan(pfx) # type: ignore[call-arg]
|
|
250
|
+
except Exception:
|
|
251
|
+
# If a backend throws here, we still consider clear() successful
|
|
252
|
+
continue
|
|
253
|
+
for k in keys:
|
|
254
|
+
try:
|
|
255
|
+
await self._kv.delete(k)
|
|
256
|
+
except Exception:
|
|
257
|
+
# swallow individual key errors; we're best-effort here
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
async def alias_for(self, token: str) -> str | None:
|
|
261
|
+
return token[:24]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from aethergraph.contracts.storage.doc_store import DocStore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FSDocStore(DocStore):
|
|
12
|
+
def __init__(self, root: str):
|
|
13
|
+
self.root = Path(root)
|
|
14
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
self._lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
def _path_for(self, doc_id: str) -> Path:
|
|
18
|
+
p = self.root / f"{doc_id}.json"
|
|
19
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
return p
|
|
21
|
+
|
|
22
|
+
async def put(self, doc_id: str, doc: dict[str, Any]) -> None:
|
|
23
|
+
path = self._path_for(doc_id)
|
|
24
|
+
|
|
25
|
+
def _write():
|
|
26
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
27
|
+
with self._lock, tmp.open("w", encoding="utf-8") as f:
|
|
28
|
+
json.dump(doc, f, ensure_ascii=False, indent=2)
|
|
29
|
+
os.replace(tmp, path)
|
|
30
|
+
|
|
31
|
+
await asyncio.to_thread(_write)
|
|
32
|
+
|
|
33
|
+
async def get(self, doc_id: str) -> dict[str, Any] | None:
|
|
34
|
+
path = self._path_for(doc_id)
|
|
35
|
+
|
|
36
|
+
def _read():
|
|
37
|
+
if not path.exists():
|
|
38
|
+
return None
|
|
39
|
+
with self._lock, path.open("r", encoding="utf-8") as f:
|
|
40
|
+
return json.load(f)
|
|
41
|
+
|
|
42
|
+
return await asyncio.to_thread(_read)
|
|
43
|
+
|
|
44
|
+
async def delete(self, doc_id):
|
|
45
|
+
path = self._path_for(doc_id)
|
|
46
|
+
|
|
47
|
+
def _delete():
|
|
48
|
+
if path.exists():
|
|
49
|
+
with self._lock:
|
|
50
|
+
path.unlink()
|
|
51
|
+
|
|
52
|
+
await asyncio.to_thread(_delete)
|
|
53
|
+
|
|
54
|
+
async def list(self) -> list[str]:
|
|
55
|
+
def _list():
|
|
56
|
+
out = []
|
|
57
|
+
for p in self.root.rglob("*.json"):
|
|
58
|
+
rel = p.relative_to(self.root)
|
|
59
|
+
doc_id = str(rel.with_suffix("").as_posix())
|
|
60
|
+
out.append(doc_id)
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
return await asyncio.to_thread(_list)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aethergraph.contracts.storage.doc_store import DocStore
|
|
7
|
+
|
|
8
|
+
from .sqlite_doc_sync import SQLiteDocStoreSync
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SqliteDocStore(DocStore):
|
|
12
|
+
"""
|
|
13
|
+
Async DocStore implemented on top of SQLiteDocStoreSync via asyncio.to_thread.
|
|
14
|
+
|
|
15
|
+
Safe to use from multiple threads (sidecar + main loop) due to RLock in sync core.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, path: str):
|
|
19
|
+
self._sync = SQLiteDocStoreSync(path)
|
|
20
|
+
|
|
21
|
+
async def put(self, doc_id: str, doc: dict[str, Any]) -> None:
|
|
22
|
+
await asyncio.to_thread(self._sync.put, doc_id, doc)
|
|
23
|
+
|
|
24
|
+
async def get(self, doc_id: str) -> dict[str, Any] | None:
|
|
25
|
+
return await asyncio.to_thread(self._sync.get, doc_id)
|
|
26
|
+
|
|
27
|
+
async def delete(self, doc_id: str) -> None:
|
|
28
|
+
await asyncio.to_thread(self._sync.delete, doc_id)
|
|
29
|
+
|
|
30
|
+
async def list(self) -> list[str]:
|
|
31
|
+
return await asyncio.to_thread(self._sync.list)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sqlite3
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
This is not used in the main codebase; only used by async wrapper SqliteDocStore.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SQLiteDocStoreSync:
|
|
16
|
+
"""
|
|
17
|
+
Durable document store on SQLite.
|
|
18
|
+
|
|
19
|
+
- Single connection per instance.
|
|
20
|
+
- Thread-safe via RLock.
|
|
21
|
+
- Values are JSON-serialized dicts.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: str):
|
|
25
|
+
path_obj = Path(path)
|
|
26
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
self._db = sqlite3.connect(
|
|
29
|
+
str(path_obj),
|
|
30
|
+
check_same_thread=False, # allow multi-thread access (guarded by RLock)
|
|
31
|
+
isolation_level=None, # autocommit
|
|
32
|
+
)
|
|
33
|
+
self._db.execute("PRAGMA journal_mode=WAL;")
|
|
34
|
+
self._db.execute("PRAGMA synchronous=NORMAL;")
|
|
35
|
+
self._db.execute(
|
|
36
|
+
"""
|
|
37
|
+
CREATE TABLE IF NOT EXISTS docs (
|
|
38
|
+
doc_id TEXT PRIMARY KEY,
|
|
39
|
+
data_json TEXT NOT NULL,
|
|
40
|
+
updated_at REAL NOT NULL
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
)
|
|
44
|
+
self._lock = threading.RLock()
|
|
45
|
+
|
|
46
|
+
def put(self, doc_id: str, doc: dict[str, Any]) -> None:
|
|
47
|
+
payload = json.dumps(doc, ensure_ascii=False)
|
|
48
|
+
now = time.time()
|
|
49
|
+
# TEMP: tiny backoff to avoid rare SQLite stalls under continuations.
|
|
50
|
+
# This is a hacky workaround.
|
|
51
|
+
# It happens when following conditions align:
|
|
52
|
+
# 1) continuation store using sqlite doc to save
|
|
53
|
+
# 2) the continuation is created under if/else or for loop nodes
|
|
54
|
+
# NOTE: this bug only appears in SQLite and not in other DBs.
|
|
55
|
+
# Remove once we move continuations to Postgres or refactor SQLite usage.
|
|
56
|
+
time.sleep(0.01)
|
|
57
|
+
with self._lock:
|
|
58
|
+
try:
|
|
59
|
+
self._db.execute(
|
|
60
|
+
"""
|
|
61
|
+
INSERT INTO docs (doc_id, data_json, updated_at)
|
|
62
|
+
VALUES (?, ?, ?)
|
|
63
|
+
ON CONFLICT(doc_id) DO UPDATE SET
|
|
64
|
+
data_json = excluded.data_json,
|
|
65
|
+
updated_at = excluded.updated_at
|
|
66
|
+
""",
|
|
67
|
+
(doc_id, payload, now),
|
|
68
|
+
)
|
|
69
|
+
except sqlite3.Error as e:
|
|
70
|
+
print("🍓 SQLiteDocStoreSync ERROR during put:", doc_id, repr(e))
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
def get(self, doc_id: str) -> dict[str, Any] | None:
|
|
74
|
+
with self._lock:
|
|
75
|
+
row = self._db.execute(
|
|
76
|
+
"SELECT data_json FROM docs WHERE doc_id = ?",
|
|
77
|
+
(doc_id,),
|
|
78
|
+
).fetchone()
|
|
79
|
+
if not row:
|
|
80
|
+
return None
|
|
81
|
+
return json.loads(row[0])
|
|
82
|
+
|
|
83
|
+
def delete(self, doc_id: str) -> None:
|
|
84
|
+
with self._lock:
|
|
85
|
+
self._db.execute("DELETE FROM docs WHERE doc_id = ?", (doc_id,))
|
|
86
|
+
|
|
87
|
+
def list(self) -> list[str]:
|
|
88
|
+
with self._lock:
|
|
89
|
+
rows = self._db.execute("SELECT doc_id FROM docs").fetchall()
|
|
90
|
+
return [r[0] for r in rows]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from aethergraph.contracts.storage.event_log import EventLog
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _to_ts_float(v) -> float | None:
|
|
12
|
+
"""
|
|
13
|
+
Normalize event ts field to a float UNIX timestamp.
|
|
14
|
+
|
|
15
|
+
Supports:
|
|
16
|
+
- float / int already
|
|
17
|
+
- ISO 8601 string, e.g. '2025-11-27T19:48:09.758687+00:00'
|
|
18
|
+
- ISO with 'Z' suffix, e.g. '2025-11-27T19:48:09Z'
|
|
19
|
+
"""
|
|
20
|
+
if v is None:
|
|
21
|
+
return None
|
|
22
|
+
if isinstance(v, int | float):
|
|
23
|
+
return float(v)
|
|
24
|
+
if isinstance(v, str):
|
|
25
|
+
try:
|
|
26
|
+
s = v.replace("Z", "+00:00") if v.endswith("Z") else v
|
|
27
|
+
dt = datetime.fromisoformat(s)
|
|
28
|
+
if dt.tzinfo is None:
|
|
29
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
30
|
+
return dt.timestamp()
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
if isinstance(v, datetime):
|
|
34
|
+
if v.tzinfo is None:
|
|
35
|
+
v = v.replace(tzinfo=timezone.utc)
|
|
36
|
+
return v.timestamp()
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FSEventLog(EventLog):
|
|
41
|
+
def __init__(self, root: str):
|
|
42
|
+
self.root = Path(root)
|
|
43
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
self._lock = threading.Lock()
|
|
45
|
+
self._log_path = self.root / "events.jsonl"
|
|
46
|
+
|
|
47
|
+
async def append(self, evt: dict) -> None:
|
|
48
|
+
def _write():
|
|
49
|
+
self._log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
row = evt.copy()
|
|
51
|
+
|
|
52
|
+
# Normalize ts to a float UNIX timestamp
|
|
53
|
+
ts = _to_ts_float(row.get("ts"))
|
|
54
|
+
if ts is None:
|
|
55
|
+
ts = time.time()
|
|
56
|
+
row["ts"] = ts
|
|
57
|
+
|
|
58
|
+
with self._lock, self._log_path.open("a", encoding="utf-8") as f:
|
|
59
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
60
|
+
|
|
61
|
+
await asyncio.to_thread(_write)
|
|
62
|
+
|
|
63
|
+
async def query(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
scope_id: str | None = None,
|
|
67
|
+
since: datetime | None = None,
|
|
68
|
+
until: datetime | None = None,
|
|
69
|
+
kinds: list[str] | None = None,
|
|
70
|
+
limit: int | None = None,
|
|
71
|
+
tags: list[str] | None = None,
|
|
72
|
+
offset: int = 0,
|
|
73
|
+
user_id: str | None = None,
|
|
74
|
+
org_id: str | None = None,
|
|
75
|
+
) -> list[dict]:
|
|
76
|
+
"""
|
|
77
|
+
FSEventLog reads the single events.jsonl file linearly, applies
|
|
78
|
+
all filters (scope_id, time window, kinds, tags, tenant) in Python,
|
|
79
|
+
and then slices via offset + limit.
|
|
80
|
+
|
|
81
|
+
This is fine for dev/demo / low event volumes. For production,
|
|
82
|
+
prefer SQLiteEventLog or a DB-backed implementation.
|
|
83
|
+
"""
|
|
84
|
+
if not self._log_path.exists():
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
def _read() -> list[dict]:
|
|
88
|
+
out: list[dict] = []
|
|
89
|
+
t_min = since.timestamp() if since else None
|
|
90
|
+
t_max = until.timestamp() if until else None
|
|
91
|
+
|
|
92
|
+
# If we want to early-break, we need enough rows to cover offset+limit.
|
|
93
|
+
needed = None
|
|
94
|
+
if limit is not None:
|
|
95
|
+
needed = (offset or 0) + limit
|
|
96
|
+
|
|
97
|
+
with self._lock, self._log_path.open("r", encoding="utf-8") as f:
|
|
98
|
+
for line in f:
|
|
99
|
+
if not line.strip():
|
|
100
|
+
continue
|
|
101
|
+
row = json.loads(line)
|
|
102
|
+
|
|
103
|
+
ts_val = _to_ts_float(row.get("ts"))
|
|
104
|
+
|
|
105
|
+
if t_min is not None and ts_val is not None and ts_val < t_min:
|
|
106
|
+
continue
|
|
107
|
+
if t_max is not None and ts_val is not None and ts_val > t_max:
|
|
108
|
+
continue
|
|
109
|
+
if scope_id is not None and row.get("scope_id") != scope_id:
|
|
110
|
+
continue
|
|
111
|
+
if kinds is not None and row.get("kind") not in kinds:
|
|
112
|
+
continue
|
|
113
|
+
if tags is not None:
|
|
114
|
+
row_tags = set(row.get("tags", []))
|
|
115
|
+
if not row_tags.issuperset(tags):
|
|
116
|
+
continue
|
|
117
|
+
if user_id is not None and row.get("user_id") != user_id:
|
|
118
|
+
continue
|
|
119
|
+
if org_id is not None and row.get("org_id") != org_id:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
out.append(row)
|
|
123
|
+
|
|
124
|
+
# Only break early when we've collected enough to satisfy offset+limit
|
|
125
|
+
if needed is not None and len(out) >= needed:
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
# Apply offset/limit on the filtered rows
|
|
129
|
+
if offset > 0:
|
|
130
|
+
out = out[offset:]
|
|
131
|
+
if limit is not None:
|
|
132
|
+
out = out[:limit]
|
|
133
|
+
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
return await asyncio.to_thread(_read)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# storage/events/sqlite_event_log.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from aethergraph.contracts.storage.event_log import EventLog
|
|
8
|
+
|
|
9
|
+
from .sqlite_event_sync import SQLiteEventLogSync
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SqliteEventLog(EventLog):
|
|
13
|
+
"""
|
|
14
|
+
Async EventLog wrapper around SQLiteEventLogSync via asyncio.to_thread.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, path: str):
|
|
18
|
+
self._sync = SQLiteEventLogSync(path)
|
|
19
|
+
|
|
20
|
+
async def append(self, evt: dict) -> None:
|
|
21
|
+
await asyncio.to_thread(self._sync.append, evt)
|
|
22
|
+
|
|
23
|
+
async def query(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
scope_id: str | None = None,
|
|
27
|
+
since: datetime | None = None,
|
|
28
|
+
until: datetime | None = None,
|
|
29
|
+
kinds: list[str] | None = None,
|
|
30
|
+
limit: int | None = None,
|
|
31
|
+
tags: list[str] | None = None,
|
|
32
|
+
offset: int = 0,
|
|
33
|
+
user_id: str | None = None,
|
|
34
|
+
org_id: str | None = None,
|
|
35
|
+
) -> list[dict]:
|
|
36
|
+
return await asyncio.to_thread(
|
|
37
|
+
self._sync.query,
|
|
38
|
+
scope_id=scope_id,
|
|
39
|
+
since=since,
|
|
40
|
+
until=until,
|
|
41
|
+
kinds=kinds,
|
|
42
|
+
limit=limit,
|
|
43
|
+
tags=tags,
|
|
44
|
+
offset=offset,
|
|
45
|
+
user_id=user_id,
|
|
46
|
+
org_id=org_id,
|
|
47
|
+
)
|