akasa 0.1.1__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.
- akasa-0.1.1/PKG-INFO +37 -0
- akasa-0.1.1/README.md +28 -0
- akasa-0.1.1/akasa/auth/__init__.py +151 -0
- akasa-0.1.1/akasa/db.py +139 -0
- akasa-0.1.1/akasa/events.py +31 -0
- akasa-0.1.1/akasa/extension.py +623 -0
- akasa-0.1.1/akasa/hooks.py +224 -0
- akasa-0.1.1/akasa/process.py +40 -0
- akasa-0.1.1/akasa/py.typed +0 -0
- akasa-0.1.1/akasa/sync.py +72 -0
- akasa-0.1.1/akasa/types.py +54 -0
- akasa-0.1.1/akasa/uri.py +109 -0
- akasa-0.1.1/akasa/utils.py +435 -0
- akasa-0.1.1/akasa/vfs.py +311 -0
- akasa-0.1.1/akasa/workers/__init__.py +653 -0
- akasa-0.1.1/akasa.egg-info/PKG-INFO +37 -0
- akasa-0.1.1/akasa.egg-info/SOURCES.txt +20 -0
- akasa-0.1.1/akasa.egg-info/dependency_links.txt +1 -0
- akasa-0.1.1/akasa.egg-info/requires.txt +2 -0
- akasa-0.1.1/akasa.egg-info/top_level.txt +1 -0
- akasa-0.1.1/pyproject.toml +35 -0
- akasa-0.1.1/setup.cfg +4 -0
akasa-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: akasa
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Agnostic Volume Router and Event Bus Substrate.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: Flask>=3.0.0
|
|
8
|
+
Requires-Dist: cryptography>=41.0.0
|
|
9
|
+
|
|
10
|
+
# 🌌 Akasa: Application Kernel & Volume Router
|
|
11
|
+
|
|
12
|
+
**Akasa** (Sanskrit for *Ether / Substrate / Space*) is a local-first, event-sourced Python micro-kernel.
|
|
13
|
+
|
|
14
|
+
Extracted from the [inSetu Developer OS](https://github.com/Callosemic/insetu), Akasa acts as the foundational substrate for multi-tenant, offline-first Python applications. It is strictly domain-agnostic: it knows nothing about your application's business logic, acting purely as a high-performance routing and synchronization engine.
|
|
15
|
+
|
|
16
|
+
## ✨ Core Mechanics
|
|
17
|
+
|
|
18
|
+
1. **The Volume Router (VFS):** An asynchronous, non-blocking Virtual File System. It abstracts physical disk I/O behind a strict Mount Table, allowing host applications to mount sandboxed repositories and virtual domains to unified URIs (e.g., `vfs://` or `ctx://`).
|
|
19
|
+
2. **Stateless Multi-Tenancy:** All databases, caches, and file resolutions are scoped to an active `workspace_id`, allowing a single daemon process to serve multiple isolated tenant environments safely.
|
|
20
|
+
3. **The Typed Event Bus:** A synchronous/asynchronous `HookRegistry` enforcing strict authorization boundaries for inter-module communication.
|
|
21
|
+
4. **The Metronome (Worker Engine):** A background SQLite-backed job ledger (`jobs` and `immediate_jobs`) providing distributed, dependency-driven task execution and UI telemetry without blocking the main event loop.
|
|
22
|
+
5. **SQLite WAL Connection Pooling:** Thread-local, auto-evicting database connection pooling explicitly optimized for Write-Ahead Logging concurrency.
|
|
23
|
+
|
|
24
|
+
## 🏗️ Architecture Mandate
|
|
25
|
+
|
|
26
|
+
Akasa adheres to the **Blind Substrate Mandate**.
|
|
27
|
+
* **Zero Domain Logic:** Akasa does not parse target repositories, compile RAG contexts, or understand Git diffs. It only provides the tools to store and route them.
|
|
28
|
+
* **Inversion of Control:** All application-specific behavior is injected into Akasa by the host application via `@hooks.on()` subscriptions, Driver registrations, and Volume mounts.
|
|
29
|
+
|
|
30
|
+
## 🚀 Installation
|
|
31
|
+
|
|
32
|
+
Akasa is designed to be installed as a foundational dependency for your host application.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install akasa
|
|
36
|
+
|
|
37
|
+
```
|
akasa-0.1.1/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 🌌 Akasa: Application Kernel & Volume Router
|
|
2
|
+
|
|
3
|
+
**Akasa** (Sanskrit for *Ether / Substrate / Space*) is a local-first, event-sourced Python micro-kernel.
|
|
4
|
+
|
|
5
|
+
Extracted from the [inSetu Developer OS](https://github.com/Callosemic/insetu), Akasa acts as the foundational substrate for multi-tenant, offline-first Python applications. It is strictly domain-agnostic: it knows nothing about your application's business logic, acting purely as a high-performance routing and synchronization engine.
|
|
6
|
+
|
|
7
|
+
## ✨ Core Mechanics
|
|
8
|
+
|
|
9
|
+
1. **The Volume Router (VFS):** An asynchronous, non-blocking Virtual File System. It abstracts physical disk I/O behind a strict Mount Table, allowing host applications to mount sandboxed repositories and virtual domains to unified URIs (e.g., `vfs://` or `ctx://`).
|
|
10
|
+
2. **Stateless Multi-Tenancy:** All databases, caches, and file resolutions are scoped to an active `workspace_id`, allowing a single daemon process to serve multiple isolated tenant environments safely.
|
|
11
|
+
3. **The Typed Event Bus:** A synchronous/asynchronous `HookRegistry` enforcing strict authorization boundaries for inter-module communication.
|
|
12
|
+
4. **The Metronome (Worker Engine):** A background SQLite-backed job ledger (`jobs` and `immediate_jobs`) providing distributed, dependency-driven task execution and UI telemetry without blocking the main event loop.
|
|
13
|
+
5. **SQLite WAL Connection Pooling:** Thread-local, auto-evicting database connection pooling explicitly optimized for Write-Ahead Logging concurrency.
|
|
14
|
+
|
|
15
|
+
## 🏗️ Architecture Mandate
|
|
16
|
+
|
|
17
|
+
Akasa adheres to the **Blind Substrate Mandate**.
|
|
18
|
+
* **Zero Domain Logic:** Akasa does not parse target repositories, compile RAG contexts, or understand Git diffs. It only provides the tools to store and route them.
|
|
19
|
+
* **Inversion of Control:** All application-specific behavior is injected into Akasa by the host application via `@hooks.on()` subscriptions, Driver registrations, and Volume mounts.
|
|
20
|
+
|
|
21
|
+
## 🚀 Installation
|
|
22
|
+
|
|
23
|
+
Akasa is designed to be installed as a foundational dependency for your host application.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install akasa
|
|
27
|
+
|
|
28
|
+
```
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import secrets
|
|
5
|
+
from flask import Blueprint, request, jsonify
|
|
6
|
+
from akasa.utils import load_config, save_json_config, get_workspace_physics
|
|
7
|
+
from akasa.extension import AkasaExtension
|
|
8
|
+
auth_bp = Blueprint('auth', __name__)
|
|
9
|
+
def get_master_key():
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
from akasa.utils import _cwd
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
control_dir_name = os.environ.get("AKASA_CONTROL_DIR", ".akasa")
|
|
16
|
+
global_key_path = Path.home().joinpath(control_dir_name, "master_key.txt")
|
|
17
|
+
local_key_path = Path(_cwd).joinpath(control_dir_name, "master_key.txt")
|
|
18
|
+
|
|
19
|
+
# Safe Migration: If a legacy local key exists...
|
|
20
|
+
if local_key_path.exists() and os.path.getsize(local_key_path) > 0:
|
|
21
|
+
# If no global key exists yet, elevate this local key to be the global key
|
|
22
|
+
if not global_key_path.exists():
|
|
23
|
+
global_key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
shutil.move(local_key_path.as_posix(), global_key_path.as_posix())
|
|
25
|
+
return global_key_path.read_bytes()
|
|
26
|
+
else:
|
|
27
|
+
# A global key already exists. This local key is different.
|
|
28
|
+
# We MUST keep using the local one here so we don't brick this specific workspace's secrets.
|
|
29
|
+
return local_key_path.read_bytes()
|
|
30
|
+
# Standard Global Key Generation/Loading
|
|
31
|
+
if not global_key_path.exists() or os.path.getsize(global_key_path) == 0:
|
|
32
|
+
from cryptography.fernet import Fernet
|
|
33
|
+
from akasa.vfs import execute_vfs_save
|
|
34
|
+
key = Fernet.generate_key()
|
|
35
|
+
execute_vfs_save("default", global_key_path.as_posix(), key.decode('utf-8'), data={"is_absolute_artifact": True, "ignore_ledger": True})
|
|
36
|
+
return key
|
|
37
|
+
|
|
38
|
+
return global_key_path.read_bytes()
|
|
39
|
+
|
|
40
|
+
def encrypt_secret(val: str) -> str:
|
|
41
|
+
if not val: return val
|
|
42
|
+
from cryptography.fernet import Fernet
|
|
43
|
+
f = Fernet(get_master_key())
|
|
44
|
+
return "v1:" + f.encrypt(val.encode('utf-8')).decode('utf-8')
|
|
45
|
+
|
|
46
|
+
def decrypt_secret(val: str) -> str:
|
|
47
|
+
if not val or not val.startswith("v1:"):
|
|
48
|
+
return val
|
|
49
|
+
from cryptography.fernet import Fernet
|
|
50
|
+
f = Fernet(get_master_key())
|
|
51
|
+
try:
|
|
52
|
+
return f.decrypt(val[3:].encode('utf-8')).decode('utf-8')
|
|
53
|
+
except Exception:
|
|
54
|
+
return ""
|
|
55
|
+
security_bp = AkasaExtension(
|
|
56
|
+
'security', __name__, title="Security & Encryption",
|
|
57
|
+
description="Local encryption key management.",
|
|
58
|
+
core=True,
|
|
59
|
+
settings_schema=lambda ws: [{
|
|
60
|
+
"id": "master_fernet_key",
|
|
61
|
+
"label": "Master Encryption Key (Fernet)",
|
|
62
|
+
"type": "text",
|
|
63
|
+
"scope": "daemon",
|
|
64
|
+
"secure": False,
|
|
65
|
+
"default": get_master_key().decode('utf-8'),
|
|
66
|
+
"description": "This global key encrypts your secrets.json. It is stored securely in your user home directory. If you move this workspace to another machine, you must copy this key to the new machine."
|
|
67
|
+
}]
|
|
68
|
+
)
|
|
69
|
+
@security_bp.route('settings', methods=['POST'])
|
|
70
|
+
def update_security_settings(ctx):
|
|
71
|
+
data = ctx.req.json or {}
|
|
72
|
+
new_key = data.get("master_fernet_key")
|
|
73
|
+
if new_key:
|
|
74
|
+
import os
|
|
75
|
+
from pathlib import Path
|
|
76
|
+
from akasa.utils import _cwd
|
|
77
|
+
control_dir_name = os.environ.get("AKASA_CONTROL_DIR", ".akasa")
|
|
78
|
+
global_key_path = Path.home().joinpath(control_dir_name, "master_key.txt")
|
|
79
|
+
local_key_path = Path(_cwd).joinpath(control_dir_name, "master_key.txt")
|
|
80
|
+
# If a local legacy key exists, update that one to avoid breaking state, otherwise update global
|
|
81
|
+
target_path = local_key_path if local_key_path.exists() else global_key_path
|
|
82
|
+
from akasa.vfs import execute_vfs_save
|
|
83
|
+
execute_vfs_save(ctx.workspace_id, target_path.as_posix(), new_key, data={"is_absolute_artifact": True, "ignore_ledger": True})
|
|
84
|
+
|
|
85
|
+
return {"status": "success", "requires_refresh": False}
|
|
86
|
+
# Generate a cryptographically sound scrolling runtime session token
|
|
87
|
+
# Anchor to the environment so it survives os.execv() and Werkzeug hot-reloads
|
|
88
|
+
BOOT_TOKEN = os.environ.get("AKASA_BOOT_TOKEN")
|
|
89
|
+
if not BOOT_TOKEN:
|
|
90
|
+
BOOT_TOKEN = secrets.token_hex(16)
|
|
91
|
+
os.environ["AKASA_BOOT_TOKEN"] = BOOT_TOKEN
|
|
92
|
+
@auth_bp.route('/auth/bootstrap', methods=['POST'])
|
|
93
|
+
def bootstrap():
|
|
94
|
+
"""Unauthenticated token exchange gate supporting absolute dynamic fallback routing."""
|
|
95
|
+
data = request.json or {}
|
|
96
|
+
|
|
97
|
+
# Extract real IP if behind Tailscale Serve or a Reverse Proxy
|
|
98
|
+
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0].strip()
|
|
99
|
+
# --- ROUTE A: LOCALHOST BYPASS ---
|
|
100
|
+
# If the user is physically on the machine, auto-authenticate
|
|
101
|
+
if client_ip == '127.0.0.1':
|
|
102
|
+
# Check for cross-origin browser requests (Localhost Drive-By CSRF prevention)
|
|
103
|
+
origin = request.headers.get('Origin', '')
|
|
104
|
+
referer = request.headers.get('Referer', '')
|
|
105
|
+
|
|
106
|
+
import urllib.parse
|
|
107
|
+
def is_allowed(url_str):
|
|
108
|
+
try:
|
|
109
|
+
hostname = urllib.parse.urlparse(url_str).hostname or ""
|
|
110
|
+
return hostname in ['127.0.0.1', 'localhost'] or hostname.endswith('.ts.net')
|
|
111
|
+
except Exception:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
if origin and not is_allowed(origin):
|
|
115
|
+
return jsonify({"error": "Forbidden: Cross-origin request blocked"}), 403
|
|
116
|
+
if referer and not is_allowed(referer):
|
|
117
|
+
return jsonify({"error": "Forbidden: Cross-origin request blocked"}), 403
|
|
118
|
+
|
|
119
|
+
return jsonify({
|
|
120
|
+
"status": "authenticated",
|
|
121
|
+
"token": BOOT_TOKEN,
|
|
122
|
+
"method": "localhost"
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
cfg = load_config()
|
|
126
|
+
# --- ROUTE B: INVERSION OF CONTROL (Zero-Knowledge Identity Protocols) ---
|
|
127
|
+
from akasa.hooks import hooks
|
|
128
|
+
for res in hooks.emit('execute_identity_handshake', request=request, client_ip=client_ip, config=cfg):
|
|
129
|
+
if res and isinstance(res, dict) and res.get("status") == "authenticated":
|
|
130
|
+
return jsonify({
|
|
131
|
+
"status": "authenticated",
|
|
132
|
+
"token": BOOT_TOKEN,
|
|
133
|
+
"method": res.get("method", "hook"),
|
|
134
|
+
"user": res.get("user")
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
# --- ROUTE C: PERSISTENT CONFIG TOKEN CHECK ---
|
|
138
|
+
client_token = data.get("token")
|
|
139
|
+
system_token = cfg.get("auth_token")
|
|
140
|
+
|
|
141
|
+
if system_token and client_token == system_token:
|
|
142
|
+
return jsonify({
|
|
143
|
+
"status": "authenticated",
|
|
144
|
+
"token": BOOT_TOKEN,
|
|
145
|
+
"method": "static_config"
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return jsonify({
|
|
149
|
+
"status": "challenge",
|
|
150
|
+
"message": "Authentication required."
|
|
151
|
+
}), 401
|
akasa-0.1.1/akasa/db.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
import sqlite3
|
|
4
|
+
import threading
|
|
5
|
+
from akasa.utils import get_workspace_physics
|
|
6
|
+
from akasa.hooks import hooks
|
|
7
|
+
|
|
8
|
+
# Thread-local storage guarantees safe connection pooling across the ASGI / Worker matrix
|
|
9
|
+
_local = threading.local()
|
|
10
|
+
|
|
11
|
+
_REGISTERED_SCHEMAS = {}
|
|
12
|
+
|
|
13
|
+
def register_schema(ext_name, schema_dict):
|
|
14
|
+
"""Registers a declarative SQLite schema for automatic workspace migration."""
|
|
15
|
+
_REGISTERED_SCHEMAS[ext_name] = schema_dict
|
|
16
|
+
|
|
17
|
+
def apply_declarative_schema(db_name, schema_dict, workspace_id=None):
|
|
18
|
+
"""Generates CREATE TABLE and executes ALTER TABLE ADD COLUMN migrations via diffing."""
|
|
19
|
+
conn = get_connection(db_name, workspace_id)
|
|
20
|
+
for table_name, columns in schema_dict.items():
|
|
21
|
+
# 1. Generate CREATE TABLE
|
|
22
|
+
col_defs = ", ".join([f"{col} {dtype}" for col, dtype in columns.items()])
|
|
23
|
+
conn.execute(f"CREATE TABLE IF NOT EXISTS {table_name} ({col_defs})")
|
|
24
|
+
|
|
25
|
+
# 2. Diff columns and ALTER TABLE
|
|
26
|
+
cursor = conn.execute(f"PRAGMA table_info({table_name})")
|
|
27
|
+
existing_cols = {row['name'] for row in cursor.fetchall()}
|
|
28
|
+
|
|
29
|
+
for col, dtype in columns.items():
|
|
30
|
+
if col not in existing_cols:
|
|
31
|
+
try:
|
|
32
|
+
conn.execute(f"ALTER TABLE {table_name} ADD COLUMN {col} {dtype}")
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"⚠️ Auto-migration failed for {table_name}.{col}: {e}")
|
|
35
|
+
conn.commit()
|
|
36
|
+
# Register core worker database schema for tracking all file deltas contextually
|
|
37
|
+
register_schema('workers', {
|
|
38
|
+
'vfs_event_log': {
|
|
39
|
+
'filepath': 'TEXT PRIMARY KEY',
|
|
40
|
+
'mutation_type': 'TEXT',
|
|
41
|
+
'timestamp': 'REAL'
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
@hooks.on('system_boot', priority=90)
|
|
45
|
+
def init_declarative_schemas(**kwargs):
|
|
46
|
+
"""Automatically provisions schemas and boots workspaces across all tenants."""
|
|
47
|
+
from akasa.utils import get_all_workspace_ids
|
|
48
|
+
for ws_id in get_all_workspace_ids():
|
|
49
|
+
for ext_name, schema in _REGISTERED_SCHEMAS.items():
|
|
50
|
+
apply_declarative_schema(ext_name, schema, ws_id)
|
|
51
|
+
hooks.emit('workspace_boot', workspace_id=ws_id)
|
|
52
|
+
|
|
53
|
+
@hooks.on('workspace_shutdown')
|
|
54
|
+
def close_workspace_connections(workspace_id=None, **kwargs):
|
|
55
|
+
"""Evicts and closes thread-local SQLite connections for an unmounting tenant workspace."""
|
|
56
|
+
if not workspace_id or not hasattr(_local, 'connections'):
|
|
57
|
+
return
|
|
58
|
+
keys_to_close = [k for k in _local.connections.keys() if k[0] == workspace_id]
|
|
59
|
+
for key in keys_to_close:
|
|
60
|
+
try:
|
|
61
|
+
_local.connections[key].close()
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
del _local.connections[key]
|
|
65
|
+
def get_connection(db_name, workspace_id=None):
|
|
66
|
+
"""
|
|
67
|
+
Returns a thread-local SQLite connection.
|
|
68
|
+
Keys connection by (workspace_id, db_name) to support stateless multi-tenancy.
|
|
69
|
+
Strictly enforces WAL mode and busy timeouts to prevent concurrent database locks.
|
|
70
|
+
"""
|
|
71
|
+
if not hasattr(_local, 'connections'):
|
|
72
|
+
_local.connections = {}
|
|
73
|
+
if not workspace_id:
|
|
74
|
+
from akasa.utils import sniff_tenant_id
|
|
75
|
+
workspace_id = sniff_tenant_id()
|
|
76
|
+
|
|
77
|
+
# Key the connection by tenant
|
|
78
|
+
cache_key = (workspace_id, db_name)
|
|
79
|
+
|
|
80
|
+
# True LRU: Pop and re-insert to move the accessed key to the end of the dictionary
|
|
81
|
+
if cache_key in _local.connections:
|
|
82
|
+
conn = _local.connections.pop(cache_key)
|
|
83
|
+
_local.connections[cache_key] = conn
|
|
84
|
+
return conn
|
|
85
|
+
|
|
86
|
+
cfg_path, _ = get_workspace_physics(workspace_id)
|
|
87
|
+
db_dir = Path(cfg_path).parent.joinpath("ext", db_name, "db")
|
|
88
|
+
db_path = db_dir.joinpath(f"{db_name}.db").as_posix()
|
|
89
|
+
|
|
90
|
+
# Phase 3: LRU Eviction Policy (Max 5 Workspaces to prevent WAL lock exhaustion)
|
|
91
|
+
if len(_local.connections) >= 5:
|
|
92
|
+
oldest_key = list(_local.connections.keys())[0]
|
|
93
|
+
try:
|
|
94
|
+
_local.connections[oldest_key].close()
|
|
95
|
+
except Exception: pass
|
|
96
|
+
del _local.connections[oldest_key]
|
|
97
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
def _connect():
|
|
100
|
+
conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
101
|
+
conn.row_factory = sqlite3.Row
|
|
102
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
103
|
+
conn.execute("PRAGMA synchronous=NORMAL;")
|
|
104
|
+
conn.execute("PRAGMA busy_timeout=5000;")
|
|
105
|
+
conn.commit()
|
|
106
|
+
cursor = conn.execute("PRAGMA quick_check(1);")
|
|
107
|
+
res = cursor.fetchone()
|
|
108
|
+
if res and res[0] != "ok":
|
|
109
|
+
raise sqlite3.DatabaseError(f"Database disk image is malformed: {res[0]}")
|
|
110
|
+
return conn
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
conn = _connect()
|
|
114
|
+
except sqlite3.DatabaseError as e:
|
|
115
|
+
if "malformed" in str(e).lower() or "corrupt" in str(e).lower():
|
|
116
|
+
print(f"⚠️ [Akasa DB] Malformed database detected at '{db_path}'. Healing database...")
|
|
117
|
+
try:
|
|
118
|
+
sqlite3.connect(db_path).close()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
import time, shutil
|
|
122
|
+
corrupt_path = f"{db_path}.corrupt_{int(time.time())}"
|
|
123
|
+
try:
|
|
124
|
+
if os.path.exists(db_path):
|
|
125
|
+
shutil.move(db_path, corrupt_path)
|
|
126
|
+
for ext in ["-wal", "-shm"]:
|
|
127
|
+
if os.path.exists(db_path + ext):
|
|
128
|
+
os.remove(db_path + ext)
|
|
129
|
+
except Exception as move_err:
|
|
130
|
+
print(f"⚠️ [Akasa DB] Failed to move corrupt db: {move_err}")
|
|
131
|
+
|
|
132
|
+
conn = _connect()
|
|
133
|
+
if db_name in _REGISTERED_SCHEMAS:
|
|
134
|
+
apply_declarative_schema(db_name, _REGISTERED_SCHEMAS[db_name], workspace_id)
|
|
135
|
+
else:
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
_local.connections[cache_key] = conn
|
|
139
|
+
return conn
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import queue
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
class EventBus:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.listeners = []
|
|
8
|
+
self.lock = threading.Lock()
|
|
9
|
+
|
|
10
|
+
def subscribe(self):
|
|
11
|
+
q = queue.Queue(maxsize=100)
|
|
12
|
+
with self.lock:
|
|
13
|
+
self.listeners.append(q)
|
|
14
|
+
return q
|
|
15
|
+
|
|
16
|
+
def unsubscribe(self, q):
|
|
17
|
+
with self.lock:
|
|
18
|
+
if q in self.listeners:
|
|
19
|
+
self.listeners.remove(q)
|
|
20
|
+
|
|
21
|
+
def emit_sse(self, event_type: str, payload: dict):
|
|
22
|
+
msg = f"event: {event_type}\ndata: {json.dumps(payload)}\n\n"
|
|
23
|
+
with self.lock:
|
|
24
|
+
for q in self.listeners:
|
|
25
|
+
try:
|
|
26
|
+
q.put_nowait(msg)
|
|
27
|
+
except queue.Full:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
# Global SSE Publisher
|
|
31
|
+
sse_bus = EventBus()
|