development-engine-vector 0.3.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.
@@ -0,0 +1,137 @@
1
+ """dev.kernel.selfcheck — the engine knows itself.
2
+
3
+ Checks:
4
+ version — version file exists and is valid semver, matches __version__
5
+ install_path — editable install of dev resolves to this source directory
6
+ db — dev.db accessible with WAL mode (soft — OK if not yet created)
7
+ tools — required tools (git, python3) are on PATH
8
+ dependencies — all required imports load without error
9
+ """
10
+
11
+ import importlib
12
+ import re
13
+ import shutil
14
+ import sqlite3
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ from dev.kernel.paths import DB_PATH
21
+
22
+ PACKAGE_DIR = Path(__file__).resolve().parent.parent # dev/
23
+ SOURCE_DIR = PACKAGE_DIR.parent # source/
24
+ VERSION_FILE = SOURCE_DIR / "version"
25
+
26
+ REQUIRED_TOOLS = ["git", "python3"]
27
+ OPTIONAL_TOOLS = ["twine"]
28
+ REQUIRED_IMPORTS = ["click", "sqlite3"]
29
+
30
+
31
+ # ── result builders ──────────────────────────────────────────────────────────
32
+
33
+ def _ok(name: str, message: str, details: Optional[str] = None) -> dict:
34
+ r = {"name": name, "passed": True, "message": message}
35
+ if details:
36
+ r["details"] = details
37
+ return r
38
+
39
+
40
+ def _fail(name: str, message: str, details: Optional[str] = None) -> dict:
41
+ r = {"name": name, "passed": False, "message": message}
42
+ if details:
43
+ r["details"] = details
44
+ return r
45
+
46
+
47
+ # ── individual checks ─────────────────────────────────────────────────────────
48
+
49
+ def check_version() -> dict:
50
+ if not VERSION_FILE.exists():
51
+ return _fail("version", "version file not found")
52
+ version = VERSION_FILE.read_text().strip()
53
+ if not re.fullmatch(r"\d+\.\d+\.\d+", version):
54
+ return _fail("version", f"version is not valid semver: {version!r}")
55
+ try:
56
+ from dev import __version__
57
+ if __version__ != version:
58
+ return _fail(
59
+ "version",
60
+ f"version file ({version}) does not match __version__ ({__version__})")
61
+ except (ImportError, AttributeError):
62
+ pass
63
+ return _ok("version", f"version is valid semver: {version}")
64
+
65
+
66
+ def check_install_path() -> dict:
67
+ try:
68
+ result = subprocess.run(
69
+ [sys.executable, "-c",
70
+ "import dev, pathlib; "
71
+ "print(pathlib.Path(dev.__file__).parent.parent.resolve())"],
72
+ capture_output=True, text=True,
73
+ )
74
+ if result.returncode != 0:
75
+ return _fail("install_path", "dev not importable — editable install broken")
76
+ install_dir = Path(result.stdout.strip()).resolve()
77
+ if install_dir == SOURCE_DIR:
78
+ return _ok("install_path", f"editable install → {install_dir}")
79
+ return _fail(
80
+ "install_path",
81
+ f"install points to wrong path: {install_dir} (expected {SOURCE_DIR})")
82
+ except Exception as exc:
83
+ return _fail("install_path", f"install_path check error: {exc}")
84
+
85
+
86
+ def check_db() -> dict:
87
+ if not DB_PATH.exists():
88
+ return _ok("db", "dev.db not yet created (will be on first run)")
89
+ try:
90
+ conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True, timeout=5)
91
+ row = conn.execute("PRAGMA journal_mode").fetchone()
92
+ conn.close()
93
+ mode = row[0] if row else "unknown"
94
+ if mode != "wal":
95
+ return _fail("db", f"DB accessible but journal_mode={mode} (expected wal)")
96
+ return _ok("db", "dev.db present and accessible (WAL mode)")
97
+ except sqlite3.DatabaseError as exc:
98
+ return _fail("db", f"DB is corrupt or unreadable: {exc}")
99
+
100
+
101
+ def check_tools() -> dict:
102
+ missing = [t for t in REQUIRED_TOOLS if not shutil.which(t)]
103
+ if missing:
104
+ return _fail("tools", f"Required tools missing: {', '.join(missing)}")
105
+ optional_missing = [t for t in OPTIONAL_TOOLS if not shutil.which(t)]
106
+ opt_details: Optional[str] = (
107
+ f"Optional tools not found: {', '.join(optional_missing)}"
108
+ if optional_missing else None
109
+ )
110
+ return _ok("tools", "All required tools present", opt_details)
111
+
112
+
113
+ def check_dependencies() -> dict:
114
+ failed = []
115
+ for mod in REQUIRED_IMPORTS:
116
+ try:
117
+ importlib.import_module(mod)
118
+ except ImportError:
119
+ failed.append(mod)
120
+ if failed:
121
+ return _fail("dependencies", f"Missing imports: {', '.join(failed)}")
122
+ return _ok("dependencies", f"All {len(REQUIRED_IMPORTS)} required imports load")
123
+
124
+
125
+ # ── runner ────────────────────────────────────────────────────────────────────
126
+
127
+ def run_all() -> dict:
128
+ """Run all selfchecks. Returns summary dict with checks list."""
129
+ checks = [
130
+ check_version(),
131
+ check_install_path(),
132
+ check_db(),
133
+ check_tools(),
134
+ check_dependencies(),
135
+ ]
136
+ passed = sum(1 for c in checks if c["passed"])
137
+ return {"checks": checks, "passed": passed, "failed": len(checks) - passed}
dev/ui/__init__.py ADDED
File without changes
dev/ui/actions.py ADDED
@@ -0,0 +1,47 @@
1
+ import subprocess
2
+ import sys
3
+ import threading
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Dict
7
+
8
+ ACTION_STATE: Dict[str, Any] = {}
9
+ ACTION_LOCK = threading.Lock()
10
+
11
+ # vet.py lives at control/scripts/vet.py relative to source/
12
+ _VET_SCRIPT = Path(__file__).resolve().parents[3] / "control" / "scripts" / "vet.py"
13
+
14
+
15
+ def run_action_background(action_id, action_name):
16
+ """Execute a control action in a background thread."""
17
+ with ACTION_LOCK:
18
+ ACTION_STATE[action_id] = {
19
+ "status": "running",
20
+ "action": action_name,
21
+ "started_at": datetime.now().isoformat(),
22
+ "output": "",
23
+ }
24
+
25
+ try:
26
+ if action_name == "vet":
27
+ result = subprocess.run(
28
+ [sys.executable, str(_VET_SCRIPT)],
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=300,
32
+ )
33
+ else:
34
+ result = None
35
+
36
+ with ACTION_LOCK:
37
+ if result:
38
+ ACTION_STATE[action_id]["status"] = "completed" if result.returncode == 0 else "failed"
39
+ ACTION_STATE[action_id]["output"] = result.stdout + result.stderr
40
+ ACTION_STATE[action_id]["returncode"] = result.returncode
41
+ ACTION_STATE[action_id]["completed_at"] = datetime.now().isoformat()
42
+
43
+ except Exception as e:
44
+ with ACTION_LOCK:
45
+ ACTION_STATE[action_id]["status"] = "error"
46
+ ACTION_STATE[action_id]["output"] = str(e)
47
+ ACTION_STATE[action_id]["completed_at"] = datetime.now().isoformat()
dev/ui/db/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ from .base import get_db, query_rows, query_one, safe_rows, safe_one, table_exists, execute_stmt
2
+ from .overview import get_overview
3
+ from .runs import get_runs, get_run_detail
4
+ from .health import get_health
5
+ from .checks import get_checks
6
+ from .manifest import get_manifest
7
+ from .identity import get_identity
8
+ from .events import get_events
9
+
10
+ __all__ = [
11
+ "get_db",
12
+ "query_rows",
13
+ "query_one",
14
+ "safe_rows",
15
+ "safe_one",
16
+ "table_exists",
17
+ "execute_stmt",
18
+ "get_overview",
19
+ "get_runs",
20
+ "get_run_detail",
21
+ "get_health",
22
+ "get_checks",
23
+ "get_manifest",
24
+ "get_identity",
25
+ "get_events",
26
+ ]
dev/ui/db/base.py ADDED
@@ -0,0 +1,75 @@
1
+ import os
2
+ import sqlite3
3
+ from pathlib import Path
4
+
5
+ # Control DB: adjacent to source/ in the intel system layout.
6
+ # Overridable via DEV_CONTROL_DB env var for non-standard installs.
7
+ _DEFAULT = Path(__file__).resolve().parents[4] / "control" / "data" / "control.db"
8
+ CONTROL_DB_PATH = Path(os.environ.get("DEV_CONTROL_DB", str(_DEFAULT)))
9
+
10
+
11
+ def get_db():
12
+ """Get a WAL connection to the control database."""
13
+ conn = sqlite3.connect(str(CONTROL_DB_PATH), timeout=10)
14
+ conn.row_factory = sqlite3.Row
15
+ conn.execute("PRAGMA journal_mode=WAL")
16
+ conn.execute("PRAGMA synchronous=NORMAL")
17
+ return conn
18
+
19
+
20
+ def query_rows(sql, params=()):
21
+ """Execute SELECT and return rows as dicts."""
22
+ try:
23
+ conn = get_db()
24
+ cursor = conn.execute(sql, params)
25
+ rows = [dict(row) for row in cursor.fetchall()]
26
+ conn.close()
27
+ return rows
28
+ except Exception as e:
29
+ return {"error": str(e)}
30
+
31
+
32
+ def query_one(sql, params=()):
33
+ """Execute SELECT and return single row or None."""
34
+ rows = query_rows(sql, params)
35
+ if isinstance(rows, dict) and "error" in rows:
36
+ return rows
37
+ return rows[0] if rows else None
38
+
39
+
40
+ def safe_rows(rows):
41
+ """Normalize query_rows output to an array for APIs."""
42
+ if isinstance(rows, dict) and "error" in rows:
43
+ return []
44
+ return rows or []
45
+
46
+
47
+ def safe_one(row):
48
+ """Normalize query_one output to a dict or None."""
49
+ if isinstance(row, dict) and "error" in row:
50
+ return None
51
+ return row
52
+
53
+
54
+ def table_exists(table_name):
55
+ """Return True if a table exists in the control database."""
56
+ try:
57
+ row = query_one(
58
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
59
+ (table_name,)
60
+ )
61
+ return bool(row)
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ def execute_stmt(sql, params=()):
67
+ """Execute INSERT/UPDATE/DELETE statement."""
68
+ try:
69
+ conn = get_db()
70
+ conn.execute(sql, params)
71
+ conn.commit()
72
+ conn.close()
73
+ return {"ok": True}
74
+ except Exception as e:
75
+ return {"error": str(e)}
dev/ui/db/checks.py ADDED
@@ -0,0 +1,16 @@
1
+ from .base import query_rows, safe_rows
2
+
3
+
4
+ def get_checks():
5
+ """Return all check definitions."""
6
+ try:
7
+ rows = safe_rows(query_rows(
8
+ """
9
+ SELECT id, name, kind, command, workdir, enabled, order_idx, timeout, added_at
10
+ FROM checks
11
+ ORDER BY order_idx, id
12
+ """
13
+ ))
14
+ return {"checks": rows, "total": len(rows)}
15
+ except Exception as e:
16
+ return {"error": str(e)}
dev/ui/db/events.py ADDED
@@ -0,0 +1,18 @@
1
+ from .base import query_rows, safe_rows
2
+
3
+
4
+ def get_events(limit=100):
5
+ """Return recent events."""
6
+ try:
7
+ rows = safe_rows(query_rows(
8
+ """
9
+ SELECT id, occurred_at, kind, actor, subject, detail
10
+ FROM events
11
+ ORDER BY id DESC
12
+ LIMIT ?
13
+ """,
14
+ (limit,),
15
+ ))
16
+ return {"events": rows, "total": len(rows)}
17
+ except Exception as e:
18
+ return {"error": str(e)}
dev/ui/db/health.py ADDED
@@ -0,0 +1,34 @@
1
+ from .base import query_rows, safe_rows
2
+
3
+
4
+ def get_health(check_name=None, limit=200):
5
+ """Return health check results, optionally filtered by check name."""
6
+ try:
7
+ if check_name:
8
+ rows = safe_rows(query_rows(
9
+ """
10
+ SELECT h.id, h.run_at, h.check_name, h.passed, h.message,
11
+ r.id as run_id, r.exit_code as run_exit_code
12
+ FROM health h
13
+ LEFT JOIN runs r ON r.started_at = h.run_at
14
+ WHERE h.check_name = ?
15
+ ORDER BY h.id DESC
16
+ LIMIT ?
17
+ """,
18
+ (check_name, limit),
19
+ ))
20
+ else:
21
+ rows = safe_rows(query_rows(
22
+ """
23
+ SELECT h.id, h.run_at, h.check_name, h.passed, h.message,
24
+ r.id as run_id, r.exit_code as run_exit_code
25
+ FROM health h
26
+ LEFT JOIN runs r ON r.started_at = h.run_at
27
+ ORDER BY h.id DESC
28
+ LIMIT ?
29
+ """,
30
+ (limit,),
31
+ ))
32
+ return {"health": rows, "total": len(rows)}
33
+ except Exception as e:
34
+ return {"error": str(e)}
dev/ui/db/identity.py ADDED
@@ -0,0 +1,12 @@
1
+ from .base import query_rows, safe_rows
2
+
3
+
4
+ def get_identity():
5
+ """Return all identity key-value records."""
6
+ try:
7
+ rows = safe_rows(query_rows(
8
+ "SELECT id, key, value, updated_at FROM identity ORDER BY key"
9
+ ))
10
+ return {"identity": rows, "total": len(rows)}
11
+ except Exception as e:
12
+ return {"error": str(e)}
dev/ui/db/manifest.py ADDED
@@ -0,0 +1,35 @@
1
+ from .base import query_rows, safe_rows
2
+
3
+
4
+ def get_manifest(q=None, limit=200, offset=0):
5
+ """Return manifest entries, optionally filtered by filename/path."""
6
+ try:
7
+ total_rows = query_rows("SELECT COUNT(*) as total FROM manifest")
8
+ total = total_rows[0]["total"] if total_rows and not isinstance(total_rows, dict) else 0
9
+
10
+ if q:
11
+ rows = safe_rows(query_rows(
12
+ """
13
+ SELECT id, relative_path, filename, extension, size_bytes,
14
+ file_type, is_tracked, is_gitignored, is_binary, mtime
15
+ FROM manifest
16
+ WHERE relative_path LIKE ? OR filename LIKE ?
17
+ ORDER BY relative_path
18
+ LIMIT ? OFFSET ?
19
+ """,
20
+ (f"%{q}%", f"%{q}%", limit, offset),
21
+ ))
22
+ else:
23
+ rows = safe_rows(query_rows(
24
+ """
25
+ SELECT id, relative_path, filename, extension, size_bytes,
26
+ file_type, is_tracked, is_gitignored, is_binary, mtime
27
+ FROM manifest
28
+ ORDER BY relative_path
29
+ LIMIT ? OFFSET ?
30
+ """,
31
+ (limit, offset),
32
+ ))
33
+ return {"files": rows, "total": total}
34
+ except Exception as e:
35
+ return {"error": str(e)}
dev/ui/db/overview.py ADDED
@@ -0,0 +1,47 @@
1
+ from .base import query_one, query_rows, safe_one, safe_rows, table_exists
2
+
3
+
4
+ def get_overview():
5
+ """Dashboard overview stats."""
6
+ try:
7
+ has_runs = table_exists("runs")
8
+ has_health = table_exists("health")
9
+ has_checks = table_exists("checks")
10
+ has_manifest = table_exists("manifest")
11
+
12
+ stats = query_one(f"""
13
+ SELECT
14
+ {("(SELECT COUNT(*) FROM runs)" if has_runs else "0")} as total_runs,
15
+ {("(SELECT COUNT(*) FROM checks WHERE enabled=1)" if has_checks else "0")} as enabled_checks,
16
+ {("(SELECT COUNT(*) FROM manifest)" if has_manifest else "0")} as manifest_files,
17
+ {("(SELECT exit_code FROM runs ORDER BY id DESC LIMIT 1)" if has_runs else "NULL")} as last_exit_code,
18
+ {("(SELECT finished_at FROM runs ORDER BY id DESC LIMIT 1)" if has_runs else "NULL")} as last_run_at,
19
+ {("(SELECT notes FROM runs ORDER BY id DESC LIMIT 1)" if has_runs else "NULL")} as last_notes,
20
+ {("(SELECT COUNT(*) FROM runs WHERE exit_code=0)" if has_runs else "0")} as clean_runs,
21
+ {("(SELECT COUNT(*) FROM runs WHERE exit_code!=0 AND exit_code IS NOT NULL)" if has_runs else "0")} as failed_runs
22
+ """)
23
+
24
+ recent_runs = safe_rows(query_rows("""
25
+ SELECT id, started_at, finished_at, trigger, exit_code, notes
26
+ FROM runs
27
+ ORDER BY id DESC
28
+ LIMIT 10
29
+ """)) if has_runs else []
30
+
31
+ check_pass_rates = safe_rows(query_rows("""
32
+ SELECT h.check_name,
33
+ COUNT(*) as total,
34
+ SUM(h.passed) as passed,
35
+ ROUND(100.0 * SUM(h.passed) / COUNT(*), 1) as pass_rate
36
+ FROM health h
37
+ GROUP BY h.check_name
38
+ ORDER BY h.check_name
39
+ """)) if has_health else []
40
+
41
+ return {
42
+ "stats": dict(safe_one(stats)) if safe_one(stats) else {},
43
+ "recent_runs": recent_runs,
44
+ "check_pass_rates": check_pass_rates,
45
+ }
46
+ except Exception as e:
47
+ return {"error": str(e)}
dev/ui/db/runs.py ADDED
@@ -0,0 +1,47 @@
1
+ from .base import query_rows, safe_rows
2
+
3
+
4
+ def get_runs(limit=50, offset=0):
5
+ """Return paginated vet run history."""
6
+ try:
7
+ total_row = query_rows("SELECT COUNT(*) as total FROM runs")
8
+ total = total_row[0]["total"] if total_row and not isinstance(total_row, dict) else 0
9
+
10
+ rows = safe_rows(query_rows(
11
+ """
12
+ SELECT id, started_at, finished_at, trigger, exit_code, notes
13
+ FROM runs
14
+ ORDER BY id DESC
15
+ LIMIT ? OFFSET ?
16
+ """,
17
+ (limit, offset),
18
+ ))
19
+ return {"runs": rows, "total": total}
20
+ except Exception as e:
21
+ return {"error": str(e)}
22
+
23
+
24
+ def get_run_detail(run_id):
25
+ """Return a single run with its per-check health rows."""
26
+ try:
27
+ run_rows = query_rows(
28
+ "SELECT id, started_at, finished_at, trigger, exit_code, notes FROM runs WHERE id=?",
29
+ (run_id,),
30
+ )
31
+ if not run_rows or isinstance(run_rows, dict):
32
+ return {"error": "run not found"}
33
+ run = run_rows[0]
34
+
35
+ health = safe_rows(query_rows(
36
+ """
37
+ SELECT h.check_name, h.passed, h.message
38
+ FROM health h
39
+ WHERE h.run_at = ?
40
+ ORDER BY h.check_name
41
+ """,
42
+ (run["started_at"],),
43
+ ))
44
+
45
+ return {"run": run, "health": health}
46
+ except Exception as e:
47
+ return {"error": str(e)}
dev/ui/routes.py ADDED
@@ -0,0 +1,114 @@
1
+ import json
2
+ import threading
3
+ import time
4
+ import traceback
5
+ from urllib.parse import parse_qs
6
+
7
+ from dev.ui.actions import run_action_background
8
+ from dev.ui.db import (
9
+ get_overview, get_runs, get_run_detail, get_health,
10
+ get_checks, get_manifest, get_identity, get_events,
11
+ query_rows,
12
+ )
13
+ from dev.ui.templates import INDEX_HTML, PAGE_REGISTRY_JS
14
+
15
+ from pathlib import Path
16
+
17
+ _static_dir = Path(__file__).parent / "static"
18
+ _CSS = (_static_dir / "web.css").read_text(encoding="utf-8")
19
+ _STATIC_JS = (_static_dir / "web.js").read_text(encoding="utf-8")
20
+ _APP_JS = PAGE_REGISTRY_JS + _STATIC_JS
21
+
22
+
23
+ def application(environ, start_response):
24
+ """WSGI application."""
25
+ method = environ["REQUEST_METHOD"]
26
+ path = environ["PATH_INFO"]
27
+ query = parse_qs(environ.get("QUERY_STRING", ""))
28
+
29
+ try:
30
+ if path == "/":
31
+ response = (
32
+ INDEX_HTML
33
+ .replace("{{STYLE_CSS}}", _CSS)
34
+ .replace("{{APP_JS}}", _APP_JS)
35
+ .encode("utf-8")
36
+ )
37
+ start_response("200 OK", [("Content-Type", "text/html; charset=utf-8")])
38
+ return [response]
39
+
40
+ elif path == "/api/overview":
41
+ data = get_overview()
42
+ start_response("200 OK", [("Content-Type", "application/json")])
43
+ return [json.dumps(data).encode("utf-8")]
44
+
45
+ elif path == "/api/runs":
46
+ limit = int(query.get("limit", ["50"])[0])
47
+ offset = int(query.get("offset", ["0"])[0])
48
+ data = get_runs(limit, offset)
49
+ start_response("200 OK", [("Content-Type", "application/json")])
50
+ return [json.dumps(data).encode("utf-8")]
51
+
52
+ elif path == "/api/run":
53
+ run_id = int(query.get("run_id", ["0"])[0])
54
+ data = get_run_detail(run_id)
55
+ start_response("200 OK", [("Content-Type", "application/json")])
56
+ return [json.dumps(data).encode("utf-8")]
57
+
58
+ elif path == "/api/health":
59
+ check_name = query.get("check", [None])[0]
60
+ data = get_health(check_name)
61
+ start_response("200 OK", [("Content-Type", "application/json")])
62
+ return [json.dumps(data).encode("utf-8")]
63
+
64
+ elif path == "/api/checks":
65
+ data = get_checks()
66
+ start_response("200 OK", [("Content-Type", "application/json")])
67
+ return [json.dumps(data).encode("utf-8")]
68
+
69
+ elif path == "/api/manifest":
70
+ q = query.get("q", [""])[0]
71
+ data = get_manifest(q if q else None)
72
+ start_response("200 OK", [("Content-Type", "application/json")])
73
+ return [json.dumps(data).encode("utf-8")]
74
+
75
+ elif path == "/api/identity":
76
+ data = get_identity()
77
+ start_response("200 OK", [("Content-Type", "application/json")])
78
+ return [json.dumps(data).encode("utf-8")]
79
+
80
+ elif path == "/api/events":
81
+ data = get_events()
82
+ start_response("200 OK", [("Content-Type", "application/json")])
83
+ return [json.dumps(data).encode("utf-8")]
84
+
85
+ elif path == "/api/action" and method == "POST":
86
+ body = environ["wsgi.input"].read()
87
+ payload = json.loads(body.decode("utf-8"))
88
+ action = payload.get("action", "")
89
+
90
+ action_id = f"{action}_{int(time.time() * 1000)}"
91
+ thread = threading.Thread(target=run_action_background, args=(action_id, action))
92
+ thread.daemon = True
93
+ thread.start()
94
+
95
+ data = {"ok": True, "action": action, "action_id": action_id, "message": f"Started {action}..."}
96
+ start_response("200 OK", [("Content-Type", "application/json")])
97
+ return [json.dumps(data).encode("utf-8")]
98
+
99
+ elif path == "/api/query" and method == "POST":
100
+ body = environ["wsgi.input"].read()
101
+ payload = json.loads(body.decode("utf-8"))
102
+ sql = payload.get("sql", "")
103
+ rows = query_rows(sql)
104
+ start_response("200 OK", [("Content-Type", "application/json")])
105
+ return [json.dumps({"rows": rows}).encode("utf-8")]
106
+
107
+ else:
108
+ start_response("404 Not Found", [("Content-Type", "text/plain")])
109
+ return [b"Not Found"]
110
+
111
+ except Exception as e:
112
+ traceback.print_exc()
113
+ start_response("500 Internal Server Error", [("Content-Type", "application/json")])
114
+ return [json.dumps({"error": str(e)}).encode("utf-8")]
File without changes