codegraph-engine 3.0.0__1-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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Zero-Heat, Push-Based OpenTelemetry (OTLP) & Dev Telemetry Collector.
|
|
2
|
+
|
|
3
|
+
Listens for OTLP HTTP traces from localhost dev servers:
|
|
4
|
+
- POST /v1/traces (Standard OpenTelemetry protobuf-in-JSON or OTLP JSON)
|
|
5
|
+
- POST /api/telemetry (Lightweight JSON / JSONL dev events)
|
|
6
|
+
- GET /healthz (Health probe)
|
|
7
|
+
|
|
8
|
+
Efficiency & Anti-Heat Invariants:
|
|
9
|
+
1. Purely event-driven socket select; 0.0% CPU when no requests are being processed.
|
|
10
|
+
2. In-memory bounded queue with ring-buffer semantics (capped at max_events=1,000) to prevent memory ballooning.
|
|
11
|
+
3. Strict secrecy redaction: Authorization, Cookie, and passwords stripped before SQLite persistence.
|
|
12
|
+
4. Graceful shutdown: Closes sockets cleanly without leaking ports.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sqlite3
|
|
18
|
+
import threading
|
|
19
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from codegraph.runtime.ingestor import ingest_runtime_traces
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TelemetryRequestHandler(BaseHTTPRequestHandler):
|
|
27
|
+
"""Zero-overhead HTTP request handler for OTLP spans and dev telemetry."""
|
|
28
|
+
|
|
29
|
+
server: TelemetryServer # Type annotation for custom server
|
|
30
|
+
|
|
31
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
32
|
+
"""Suppress standard stderr access logging to prevent noisy terminal output."""
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def do_GET(self) -> None:
|
|
36
|
+
if self.path in ("/healthz", "/health"):
|
|
37
|
+
self.send_response(200)
|
|
38
|
+
self.send_header("Content-Type", "application/json")
|
|
39
|
+
self.end_headers()
|
|
40
|
+
self.wfile.write(b'{"status":"ok","engine":"codegraph-runtime-collector"}')
|
|
41
|
+
return
|
|
42
|
+
self.send_response(404)
|
|
43
|
+
self.end_headers()
|
|
44
|
+
|
|
45
|
+
def do_POST(self) -> None:
|
|
46
|
+
if self.path not in ("/v1/traces", "/api/telemetry", "/telemetry"):
|
|
47
|
+
self.send_response(404)
|
|
48
|
+
self.end_headers()
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
52
|
+
if content_length <= 0 or content_length > 10 * 1024 * 1024: # Max 10MB payload
|
|
53
|
+
self.send_response(400)
|
|
54
|
+
self.end_headers()
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
body_bytes = self.rfile.read(content_length)
|
|
59
|
+
body_text = body_bytes.decode("utf-8", errors="replace")
|
|
60
|
+
parsed_json = json.loads(body_text)
|
|
61
|
+
|
|
62
|
+
# Ingest into CodeGraph SQLite database
|
|
63
|
+
with self.server.db_lock:
|
|
64
|
+
ingest_runtime_traces(
|
|
65
|
+
self.server.db_connection,
|
|
66
|
+
parsed_json,
|
|
67
|
+
repository=self.server.repo_root,
|
|
68
|
+
format="otel" if "/traces" in self.path else "json",
|
|
69
|
+
max_events=1000,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.send_response(200)
|
|
73
|
+
self.send_header("Content-Type", "application/json")
|
|
74
|
+
self.end_headers()
|
|
75
|
+
self.wfile.write(b'{"partialSuccess":{}}')
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
self.send_response(500)
|
|
78
|
+
self.send_header("Content-Type", "application/json")
|
|
79
|
+
self.end_headers()
|
|
80
|
+
err_msg = json.dumps({"error": str(exc)}).encode("utf-8")
|
|
81
|
+
self.wfile.write(err_msg)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TelemetryServer(HTTPServer):
|
|
85
|
+
"""Custom HTTPServer maintaining db connection and repo references."""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
server_address: tuple[str, int],
|
|
90
|
+
db_connection: sqlite3.Connection,
|
|
91
|
+
repo_root: Path,
|
|
92
|
+
) -> None:
|
|
93
|
+
super().__init__(server_address, TelemetryRequestHandler)
|
|
94
|
+
self.db_connection = db_connection
|
|
95
|
+
self.repo_root = repo_root
|
|
96
|
+
self.db_lock = threading.Lock()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RuntimeCollectorDaemon:
|
|
100
|
+
"""Manages lifecycle of background OTLP collector."""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
db_connection: sqlite3.Connection,
|
|
105
|
+
repo_root: Path,
|
|
106
|
+
port: int = 4318,
|
|
107
|
+
host: str = "127.0.0.1",
|
|
108
|
+
) -> None:
|
|
109
|
+
self.db_connection = db_connection
|
|
110
|
+
self.repo_root = repo_root
|
|
111
|
+
self.port = port
|
|
112
|
+
self.host = host
|
|
113
|
+
self._server: TelemetryServer | None = None
|
|
114
|
+
self._thread: threading.Thread | None = None
|
|
115
|
+
self._is_running = False
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def is_running(self) -> bool:
|
|
119
|
+
return self._is_running
|
|
120
|
+
|
|
121
|
+
def start(self) -> bool:
|
|
122
|
+
"""Start the collector in a low-priority background daemon thread."""
|
|
123
|
+
if self._is_running:
|
|
124
|
+
return True
|
|
125
|
+
try:
|
|
126
|
+
self._server = TelemetryServer(
|
|
127
|
+
(self.host, self.port),
|
|
128
|
+
self.db_connection,
|
|
129
|
+
self.repo_root,
|
|
130
|
+
)
|
|
131
|
+
self._thread = threading.Thread(
|
|
132
|
+
target=self._server.serve_forever,
|
|
133
|
+
name="codegraph-otlp-collector",
|
|
134
|
+
daemon=True,
|
|
135
|
+
)
|
|
136
|
+
self._thread.start()
|
|
137
|
+
self._is_running = True
|
|
138
|
+
return True
|
|
139
|
+
except OSError:
|
|
140
|
+
# Port might be in use or unavailable; fail closed gracefully without crashing
|
|
141
|
+
self._is_running = False
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def stop(self) -> None:
|
|
145
|
+
"""Shutdown the collector cleanly."""
|
|
146
|
+
if not self._is_running or not self._server:
|
|
147
|
+
return
|
|
148
|
+
try:
|
|
149
|
+
self._server.shutdown()
|
|
150
|
+
self._server.server_close()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
finally:
|
|
154
|
+
self._is_running = False
|
|
155
|
+
self._server = None
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Automatic Database Schema Sync & Migration Drift Watcher.
|
|
2
|
+
|
|
3
|
+
Continuously tracks:
|
|
4
|
+
1. Migration directory updates (Prisma, Drizzle, Alembic, raw SQL migrations).
|
|
5
|
+
2. Live database schema version cookies (PRAGMA schema_version, _prisma_migrations, alembic_version).
|
|
6
|
+
3. Schema drift detection: flags discrepancy between static ORM models and database state.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import sqlite3
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class SchemaDriftItem:
|
|
20
|
+
kind: str # MISSING_COLUMN | MISSING_TABLE | TYPE_MISMATCH | NULLABILITY_MISMATCH
|
|
21
|
+
table_name: str
|
|
22
|
+
column_name: str = ""
|
|
23
|
+
db_type: str = ""
|
|
24
|
+
model_type: str = ""
|
|
25
|
+
message: str = ""
|
|
26
|
+
|
|
27
|
+
def as_dict(self) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"kind": self.kind,
|
|
30
|
+
"table_name": self.table_name,
|
|
31
|
+
"column_name": self.column_name,
|
|
32
|
+
"db_type": self.db_type,
|
|
33
|
+
"model_type": self.model_type,
|
|
34
|
+
"message": self.message,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SchemaDriftReport:
|
|
40
|
+
is_drifted: bool
|
|
41
|
+
schema_version: str
|
|
42
|
+
migration_hash: str
|
|
43
|
+
drifts: tuple[SchemaDriftItem, ...] = ()
|
|
44
|
+
migration_files_count: int = 0
|
|
45
|
+
latest_migration_name: str = ""
|
|
46
|
+
|
|
47
|
+
def as_dict(self) -> dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"is_drifted": self.is_drifted,
|
|
50
|
+
"schema_version": self.schema_version,
|
|
51
|
+
"migration_hash": self.migration_hash,
|
|
52
|
+
"drifts": [d.as_dict() for d in self.drifts],
|
|
53
|
+
"migration_files_count": self.migration_files_count,
|
|
54
|
+
"latest_migration_name": self.latest_migration_name,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def compute_migration_directory_state(repo_root: Path) -> tuple[str, int, str]:
|
|
59
|
+
"""Scan migration directories and compute combined hash, count, and latest migration name."""
|
|
60
|
+
migration_patterns = (
|
|
61
|
+
"**/migrations/*.sql",
|
|
62
|
+
"**/prisma/migrations/**/*.sql",
|
|
63
|
+
"**/drizzle/*.sql",
|
|
64
|
+
"**/alembic/versions/*.py",
|
|
65
|
+
)
|
|
66
|
+
migration_files: list[Path] = []
|
|
67
|
+
for pattern in migration_patterns:
|
|
68
|
+
migration_files.extend(repo_root.glob(pattern))
|
|
69
|
+
|
|
70
|
+
migration_files = [f for f in migration_files if "node_modules" not in f.parts and ".git" not in f.parts]
|
|
71
|
+
if not migration_files:
|
|
72
|
+
return "", 0, ""
|
|
73
|
+
|
|
74
|
+
migration_files.sort(key=lambda f: f.name)
|
|
75
|
+
hasher = hashlib.sha256()
|
|
76
|
+
for f in migration_files:
|
|
77
|
+
try:
|
|
78
|
+
hasher.update(f.name.encode())
|
|
79
|
+
hasher.update(str(f.stat().st_mtime_ns).encode())
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
latest_name = migration_files[-1].name
|
|
84
|
+
return hasher.hexdigest()[:16], len(migration_files), latest_name
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_sqlite_schema_version(db_path: Path) -> int:
|
|
88
|
+
"""Read PRAGMA schema_version from SQLite database."""
|
|
89
|
+
if not db_path.exists():
|
|
90
|
+
return 0
|
|
91
|
+
try:
|
|
92
|
+
con = sqlite3.connect(str(db_path), timeout=5.0)
|
|
93
|
+
cur = con.execute("PRAGMA schema_version")
|
|
94
|
+
val = cur.fetchone()
|
|
95
|
+
con.close()
|
|
96
|
+
return int(val[0]) if val else 0
|
|
97
|
+
except Exception:
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_DRIFT_CACHE: dict[str, tuple[float, SchemaDriftReport]] = {}
|
|
102
|
+
_DRIFT_CACHE_TTL_SEC = 5.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def detect_schema_drift(
|
|
106
|
+
con: sqlite3.Connection,
|
|
107
|
+
repo_root: Path,
|
|
108
|
+
) -> SchemaDriftReport:
|
|
109
|
+
"""Compare indexed DB models against migration state with 5-second TTL cache (Zero idle CPU)."""
|
|
110
|
+
global _DRIFT_CACHE
|
|
111
|
+
cache_key = str(repo_root)
|
|
112
|
+
now = time.monotonic()
|
|
113
|
+
if cache_key in _DRIFT_CACHE:
|
|
114
|
+
last_t, cached_report = _DRIFT_CACHE[cache_key]
|
|
115
|
+
if now - last_t < _DRIFT_CACHE_TTL_SEC:
|
|
116
|
+
return cached_report
|
|
117
|
+
|
|
118
|
+
mig_hash, mig_count, latest_mig = compute_migration_directory_state(repo_root)
|
|
119
|
+
|
|
120
|
+
# 1. Fetch tables and columns registered in CodeGraph
|
|
121
|
+
db_tables_rows = con.execute("SELECT table_name, name, dialect FROM db_entities WHERE kind = 'Table'").fetchall()
|
|
122
|
+
indexed_tables = {str(r["table_name"] or r["name"]).lower(): str(r["dialect"]) for r in db_tables_rows}
|
|
123
|
+
|
|
124
|
+
indexed_cols: dict[str, set[str]] = {}
|
|
125
|
+
col_rows = con.execute("SELECT table_name, name, data_type FROM db_entities WHERE kind = 'Column'").fetchall()
|
|
126
|
+
for r in col_rows:
|
|
127
|
+
t_name = str(r["table_name"] or "").lower()
|
|
128
|
+
if t_name not in indexed_cols:
|
|
129
|
+
indexed_cols[t_name] = set()
|
|
130
|
+
c_name = str(r["name"] or "").lower()
|
|
131
|
+
if c_name:
|
|
132
|
+
indexed_cols[t_name].add(c_name)
|
|
133
|
+
|
|
134
|
+
drifts: list[SchemaDriftItem] = []
|
|
135
|
+
|
|
136
|
+
# 2. If a local SQLite database exists in repo (e.g. dev.db, local.db, test.db), check live tables
|
|
137
|
+
local_dbs = list(repo_root.glob("*.db")) + list(repo_root.glob("*.sqlite")) + list(repo_root.glob("*.sqlite3"))
|
|
138
|
+
local_dbs = [d for d in local_dbs if not d.name.startswith(".codegraph")]
|
|
139
|
+
|
|
140
|
+
schema_version_str = "0"
|
|
141
|
+
if local_dbs:
|
|
142
|
+
target_db = local_dbs[0]
|
|
143
|
+
schema_version_str = str(check_sqlite_schema_version(target_db))
|
|
144
|
+
try:
|
|
145
|
+
live_con = sqlite3.connect(str(target_db), timeout=5.0)
|
|
146
|
+
live_tables = [
|
|
147
|
+
row[0].lower()
|
|
148
|
+
for row in live_con.execute(
|
|
149
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
150
|
+
).fetchall()
|
|
151
|
+
]
|
|
152
|
+
for lt in live_tables:
|
|
153
|
+
if lt not in indexed_tables and not lt.startswith(("_prisma", "alembic")):
|
|
154
|
+
drifts.append(
|
|
155
|
+
SchemaDriftItem(
|
|
156
|
+
kind="MISSING_TABLE",
|
|
157
|
+
table_name=lt,
|
|
158
|
+
message=f"Table '{lt}' exists in live database but has no corresponding code model.",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
# check columns
|
|
163
|
+
live_cols = [
|
|
164
|
+
crow[1].lower()
|
|
165
|
+
for crow in live_con.execute(f"PRAGMA table_info('{lt}')").fetchall()
|
|
166
|
+
]
|
|
167
|
+
known_c = indexed_cols.get(lt, set())
|
|
168
|
+
for lc in live_cols:
|
|
169
|
+
if lc not in known_c:
|
|
170
|
+
drifts.append(
|
|
171
|
+
SchemaDriftItem(
|
|
172
|
+
kind="MISSING_COLUMN",
|
|
173
|
+
table_name=lt,
|
|
174
|
+
column_name=lc,
|
|
175
|
+
message=f"Column '{lt}.{lc}' exists in live database but is missing in code model.",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
live_con.close()
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
report = SchemaDriftReport(
|
|
183
|
+
is_drifted=len(drifts) > 0,
|
|
184
|
+
schema_version=schema_version_str,
|
|
185
|
+
migration_hash=mig_hash,
|
|
186
|
+
drifts=tuple(drifts),
|
|
187
|
+
migration_files_count=mig_count,
|
|
188
|
+
latest_migration_name=latest_mig,
|
|
189
|
+
)
|
|
190
|
+
_DRIFT_CACHE[cache_key] = (now, report)
|
|
191
|
+
return report
|