vclient 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +15 -0
- src/__main__.py +5 -0
- src/cache.py +159 -0
- src/cli.py +262 -0
- src/codegen.py +674 -0
- src/inferrer.py +302 -0
- src/sampler.py +231 -0
- vclient-0.2.0.dist-info/METADATA +130 -0
- vclient-0.2.0.dist-info/RECORD +12 -0
- vclient-0.2.0.dist-info/WHEEL +5 -0
- vclient-0.2.0.dist-info/entry_points.txt +2 -0
- vclient-0.2.0.dist-info/top_level.txt +1 -0
src/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Dynamic API Adapter - Auto-discover and adapt to API schemas."""
|
|
2
|
+
|
|
3
|
+
from .inferrer import SchemaInferrer, infer_property_schema, infer_object_schema
|
|
4
|
+
from .sampler import EndpointSampler
|
|
5
|
+
from .codegen import ClientGenerator
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SchemaInferrer",
|
|
9
|
+
"EndpointSampler",
|
|
10
|
+
"ClientGenerator",
|
|
11
|
+
"infer_property_schema",
|
|
12
|
+
"infer_object_schema",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
src/__main__.py
ADDED
src/cache.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""SQLite-based schema caching."""
|
|
2
|
+
import sqlite3
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
DEFAULT_DB_PATH = Path.home() / ".api-adapter" / "cache.db"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SchemaCache:
|
|
12
|
+
"""Persistent schema cache using SQLite."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, db_path: str | Path | None = None) -> None:
|
|
15
|
+
self.db_path = Path(db_path) if db_path else DEFAULT_DB_PATH
|
|
16
|
+
self._conn: sqlite3.Connection | None = None
|
|
17
|
+
|
|
18
|
+
def __enter__(self) -> "SchemaCache":
|
|
19
|
+
self.connect()
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __exit__(self, *args) -> None:
|
|
23
|
+
self.close()
|
|
24
|
+
|
|
25
|
+
def connect(self) -> None:
|
|
26
|
+
"""Open connection and initialize schema."""
|
|
27
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
self._conn = sqlite3.connect(str(self.db_path))
|
|
29
|
+
self._conn.row_factory = sqlite3.Row
|
|
30
|
+
self._init_schema()
|
|
31
|
+
|
|
32
|
+
def close(self) -> None:
|
|
33
|
+
"""Commit and close connection."""
|
|
34
|
+
if self._conn:
|
|
35
|
+
self._conn.commit()
|
|
36
|
+
self._conn.close()
|
|
37
|
+
self._conn = None
|
|
38
|
+
|
|
39
|
+
def _init_schema(self) -> None:
|
|
40
|
+
"""Create tables if they don't exist."""
|
|
41
|
+
if not self._conn:
|
|
42
|
+
raise RuntimeError("Not connected to database")
|
|
43
|
+
|
|
44
|
+
cursor = self._conn.cursor()
|
|
45
|
+
|
|
46
|
+
cursor.execute("""
|
|
47
|
+
CREATE TABLE IF NOT EXISTS schema_snapshots (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
base_url TEXT NOT NULL,
|
|
50
|
+
captured_at TEXT NOT NULL,
|
|
51
|
+
schema_json TEXT NOT NULL
|
|
52
|
+
)
|
|
53
|
+
""")
|
|
54
|
+
|
|
55
|
+
cursor.execute("""
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_url
|
|
57
|
+
ON schema_snapshots(base_url)
|
|
58
|
+
""")
|
|
59
|
+
|
|
60
|
+
cursor.execute("""
|
|
61
|
+
CREATE TABLE IF NOT EXISTS drift_events (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
base_url TEXT NOT NULL,
|
|
64
|
+
detected_at TEXT NOT NULL,
|
|
65
|
+
old_snapshot_id INTEGER NOT NULL REFERENCES schema_snapshots(id),
|
|
66
|
+
new_snapshot_id INTEGER NOT NULL REFERENCES schema_snapshots(id),
|
|
67
|
+
similarity REAL NOT NULL,
|
|
68
|
+
diff_json TEXT NOT NULL
|
|
69
|
+
)
|
|
70
|
+
""")
|
|
71
|
+
|
|
72
|
+
self._conn.commit()
|
|
73
|
+
|
|
74
|
+
def save_snapshot(self, base_url: str, schema: Dict[str, Any]) -> int:
|
|
75
|
+
"""Save schema snapshot and return row id."""
|
|
76
|
+
if not self._conn:
|
|
77
|
+
raise RuntimeError("Not connected to database")
|
|
78
|
+
|
|
79
|
+
cursor = self._conn.cursor()
|
|
80
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
81
|
+
schema_json = json.dumps(schema, default=str)
|
|
82
|
+
|
|
83
|
+
cursor.execute(
|
|
84
|
+
"INSERT INTO schema_snapshots (base_url, captured_at, schema_json) VALUES (?, ?, ?)",
|
|
85
|
+
(base_url, now, schema_json)
|
|
86
|
+
)
|
|
87
|
+
self._conn.commit()
|
|
88
|
+
return cursor.lastrowid
|
|
89
|
+
|
|
90
|
+
def latest_snapshot(self, base_url: str) -> Optional[Dict[str, Any]]:
|
|
91
|
+
"""Get most recent snapshot for base_url."""
|
|
92
|
+
if not self._conn:
|
|
93
|
+
raise RuntimeError("Not connected to database")
|
|
94
|
+
|
|
95
|
+
cursor = self._conn.cursor()
|
|
96
|
+
cursor.execute(
|
|
97
|
+
"SELECT schema_json FROM schema_snapshots WHERE base_url=? ORDER BY captured_at DESC LIMIT 1",
|
|
98
|
+
(base_url,)
|
|
99
|
+
)
|
|
100
|
+
row = cursor.fetchone()
|
|
101
|
+
if row:
|
|
102
|
+
return json.loads(row[0])
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def all_snapshots(self, base_url: str) -> list[Dict[str, Any]]:
|
|
106
|
+
"""Get all snapshots for base_url ordered by captured_at DESC."""
|
|
107
|
+
if not self._conn:
|
|
108
|
+
raise RuntimeError("Not connected to database")
|
|
109
|
+
|
|
110
|
+
cursor = self._conn.cursor()
|
|
111
|
+
cursor.execute(
|
|
112
|
+
"SELECT schema_json FROM schema_snapshots WHERE base_url=? ORDER BY captured_at DESC",
|
|
113
|
+
(base_url,)
|
|
114
|
+
)
|
|
115
|
+
return [json.loads(row[0]) for row in cursor.fetchall()]
|
|
116
|
+
|
|
117
|
+
def save_drift_event(
|
|
118
|
+
self,
|
|
119
|
+
base_url: str,
|
|
120
|
+
old_id: int,
|
|
121
|
+
new_id: int,
|
|
122
|
+
similarity: float,
|
|
123
|
+
diff: Dict[str, Any],
|
|
124
|
+
) -> int:
|
|
125
|
+
"""Save drift event and return row id."""
|
|
126
|
+
if not self._conn:
|
|
127
|
+
raise RuntimeError("Not connected to database")
|
|
128
|
+
|
|
129
|
+
cursor = self._conn.cursor()
|
|
130
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
131
|
+
diff_json = json.dumps(diff, default=str)
|
|
132
|
+
|
|
133
|
+
cursor.execute(
|
|
134
|
+
"INSERT INTO drift_events (base_url, detected_at, old_snapshot_id, new_snapshot_id, similarity, diff_json) "
|
|
135
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
136
|
+
(base_url, now, old_id, new_id, similarity, diff_json)
|
|
137
|
+
)
|
|
138
|
+
self._conn.commit()
|
|
139
|
+
return cursor.lastrowid
|
|
140
|
+
|
|
141
|
+
def drift_history(self, base_url: str) -> list[Dict[str, Any]]:
|
|
142
|
+
"""Get all drift events for base_url ordered by detected_at DESC."""
|
|
143
|
+
if not self._conn:
|
|
144
|
+
raise RuntimeError("Not connected to database")
|
|
145
|
+
|
|
146
|
+
cursor = self._conn.cursor()
|
|
147
|
+
cursor.execute(
|
|
148
|
+
"SELECT id, detected_at, similarity, diff_json FROM drift_events WHERE base_url=? ORDER BY detected_at DESC",
|
|
149
|
+
(base_url,)
|
|
150
|
+
)
|
|
151
|
+
results = []
|
|
152
|
+
for row in cursor.fetchall():
|
|
153
|
+
results.append({
|
|
154
|
+
"id": row[0],
|
|
155
|
+
"detected_at": row[1],
|
|
156
|
+
"similarity": row[2],
|
|
157
|
+
"diff": json.loads(row[3])
|
|
158
|
+
})
|
|
159
|
+
return results
|
src/cli.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Command-line interface for API Adapter."""
|
|
2
|
+
import click
|
|
3
|
+
import json
|
|
4
|
+
import asyncio
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .sampler import EndpointSampler
|
|
9
|
+
from .inferrer import SchemaInferrer, compute_drift_diff
|
|
10
|
+
from .codegen import ClientGenerator
|
|
11
|
+
from .cache import SchemaCache
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
def cli():
|
|
16
|
+
"""Dynamic API Adapter - Infer schemas from live APIs."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cli.command()
|
|
21
|
+
@click.argument("url")
|
|
22
|
+
@click.option("--output", "-o", default="schema.json", help="Output file for inferred schema")
|
|
23
|
+
@click.option("--gen-python", is_flag=True, help="Generate Python client")
|
|
24
|
+
@click.option("--gen-typescript", is_flag=True, help="Generate TypeScript client")
|
|
25
|
+
@click.option("--gen-go", is_flag=True, help="Generate Go client")
|
|
26
|
+
@click.option("--gen-rust", is_flag=True, help="Generate Rust client")
|
|
27
|
+
@click.option("--gen-java", is_flag=True, help="Generate Java client")
|
|
28
|
+
@click.option("--gen-ruby", is_flag=True, help="Generate Ruby client")
|
|
29
|
+
@click.option("--timeout", "-t", default=10, help="Request timeout in seconds")
|
|
30
|
+
@click.option("--max-endpoints", "-e", default=50, help="Maximum endpoints to sample")
|
|
31
|
+
@click.option("--no-cache", is_flag=True, default=False, help="Skip cache read/write")
|
|
32
|
+
@click.option("--cache-db", default=None, help="Path to SQLite cache DB")
|
|
33
|
+
def infer(
|
|
34
|
+
url: str,
|
|
35
|
+
output: str,
|
|
36
|
+
gen_python: bool,
|
|
37
|
+
gen_typescript: bool,
|
|
38
|
+
gen_go: bool,
|
|
39
|
+
gen_rust: bool,
|
|
40
|
+
gen_java: bool,
|
|
41
|
+
gen_ruby: bool,
|
|
42
|
+
timeout: int,
|
|
43
|
+
max_endpoints: int,
|
|
44
|
+
no_cache: bool,
|
|
45
|
+
cache_db: Optional[str],
|
|
46
|
+
):
|
|
47
|
+
"""Infer API schema from a live API.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
api-adapter infer https://api.example.com --output schema.json --gen-python
|
|
51
|
+
"""
|
|
52
|
+
click.echo(f"🔍 Discovering endpoints from {url}...")
|
|
53
|
+
|
|
54
|
+
sampler = EndpointSampler(url, timeout=timeout, max_endpoints=max_endpoints)
|
|
55
|
+
responses = sampler.run()
|
|
56
|
+
|
|
57
|
+
click.echo(f"✅ Sampled {len(responses)} endpoints")
|
|
58
|
+
|
|
59
|
+
# Infer schema
|
|
60
|
+
click.echo("📊 Inferring schema...")
|
|
61
|
+
inferrer = SchemaInferrer(base_url=url)
|
|
62
|
+
|
|
63
|
+
for key, response in responses.items():
|
|
64
|
+
method, endpoint = key.split(" ", 1)
|
|
65
|
+
inferrer.add_response(endpoint, method, response)
|
|
66
|
+
|
|
67
|
+
schema = inferrer.to_dict()
|
|
68
|
+
|
|
69
|
+
# Save schema
|
|
70
|
+
output_path = Path(output)
|
|
71
|
+
output_path.write_text(inferrer.to_json())
|
|
72
|
+
click.echo(f"💾 Schema saved to {output}")
|
|
73
|
+
|
|
74
|
+
# Save to cache if not disabled
|
|
75
|
+
if not no_cache:
|
|
76
|
+
try:
|
|
77
|
+
with SchemaCache(db_path=cache_db) as cache:
|
|
78
|
+
snapshot_id = cache.save_snapshot(url, schema)
|
|
79
|
+
click.echo(f"📦 Snapshot cached (id={snapshot_id})")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
click.echo(f"⚠️ Cache write failed: {e}")
|
|
82
|
+
|
|
83
|
+
# Generate clients if requested
|
|
84
|
+
if gen_python:
|
|
85
|
+
models_code = ClientGenerator.generate_python_models(url, schema["endpoints"])
|
|
86
|
+
models_file = Path("models.py")
|
|
87
|
+
models_file.write_text(models_code)
|
|
88
|
+
click.echo(f"🏗️ Python models generated: {models_file}")
|
|
89
|
+
|
|
90
|
+
client_code = ClientGenerator.generate_python(url, schema["endpoints"])
|
|
91
|
+
python_file = Path("client.py")
|
|
92
|
+
python_file.write_text(client_code)
|
|
93
|
+
click.echo(f"🐍 Python client generated: {python_file}")
|
|
94
|
+
|
|
95
|
+
if gen_typescript:
|
|
96
|
+
schemas_code = ClientGenerator.generate_typescript_schemas(url, schema["endpoints"])
|
|
97
|
+
schemas_file = Path("schemas.ts")
|
|
98
|
+
schemas_file.write_text(schemas_code)
|
|
99
|
+
click.echo(f"🔐 TypeScript schemas generated: {schemas_file}")
|
|
100
|
+
|
|
101
|
+
client_code = ClientGenerator.generate_typescript(url, schema["endpoints"])
|
|
102
|
+
ts_file = Path("client.ts")
|
|
103
|
+
ts_file.write_text(client_code)
|
|
104
|
+
click.echo(f"📘 TypeScript client generated: {ts_file}")
|
|
105
|
+
|
|
106
|
+
if gen_go:
|
|
107
|
+
client_code = ClientGenerator.generate_go(url, schema["endpoints"])
|
|
108
|
+
go_file = Path("client.go")
|
|
109
|
+
go_file.write_text(client_code)
|
|
110
|
+
click.echo(f"🐹 Go client generated: {go_file}")
|
|
111
|
+
|
|
112
|
+
if gen_rust:
|
|
113
|
+
client_code = ClientGenerator.generate_rust(url, schema["endpoints"])
|
|
114
|
+
rs_file = Path("client.rs")
|
|
115
|
+
rs_file.write_text(client_code)
|
|
116
|
+
click.echo(f"🦀 Rust client generated: {rs_file}")
|
|
117
|
+
|
|
118
|
+
if gen_java:
|
|
119
|
+
client_code = ClientGenerator.generate_java(url, schema["endpoints"])
|
|
120
|
+
java_file = Path("Client.java")
|
|
121
|
+
java_file.write_text(client_code)
|
|
122
|
+
click.echo(f"☕ Java client generated: {java_file}")
|
|
123
|
+
|
|
124
|
+
if gen_ruby:
|
|
125
|
+
client_code = ClientGenerator.generate_ruby(url, schema["endpoints"])
|
|
126
|
+
rb_file = Path("client.rb")
|
|
127
|
+
rb_file.write_text(client_code)
|
|
128
|
+
click.echo(f"💎 Ruby client generated: {rb_file}")
|
|
129
|
+
|
|
130
|
+
click.echo("✨ Done!")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@cli.command()
|
|
134
|
+
@click.argument("schema_file")
|
|
135
|
+
@click.option("--python", "output_python", help="Output file for Python client")
|
|
136
|
+
@click.option("--typescript", "output_typescript", help="Output file for TypeScript client")
|
|
137
|
+
@click.option("--go", "output_go", help="Output file for Go client")
|
|
138
|
+
@click.option("--rust", "output_rust", help="Output file for Rust client")
|
|
139
|
+
@click.option("--java", "output_java", help="Output file for Java client")
|
|
140
|
+
@click.option("--ruby", "output_ruby", help="Output file for Ruby client")
|
|
141
|
+
def codegen(
|
|
142
|
+
schema_file: str,
|
|
143
|
+
output_python: Optional[str],
|
|
144
|
+
output_typescript: Optional[str],
|
|
145
|
+
output_go: Optional[str],
|
|
146
|
+
output_rust: Optional[str],
|
|
147
|
+
output_java: Optional[str],
|
|
148
|
+
output_ruby: Optional[str],
|
|
149
|
+
):
|
|
150
|
+
"""Generate clients from an inferred schema.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
api-adapter codegen schema.json --python client.py --typescript client.ts
|
|
154
|
+
"""
|
|
155
|
+
schema_path = Path(schema_file)
|
|
156
|
+
if not schema_path.exists():
|
|
157
|
+
click.echo(f"❌ Schema file not found: {schema_file}", err=True)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
with open(schema_path) as f:
|
|
161
|
+
schema = json.load(f)
|
|
162
|
+
|
|
163
|
+
base_url = schema.get("base_url", "https://api.example.com")
|
|
164
|
+
endpoints = schema.get("endpoints", {})
|
|
165
|
+
|
|
166
|
+
if output_python:
|
|
167
|
+
client_code = ClientGenerator.generate_python(base_url, endpoints)
|
|
168
|
+
Path(output_python).write_text(client_code)
|
|
169
|
+
click.echo(f"🐍 Python client: {output_python}")
|
|
170
|
+
|
|
171
|
+
if output_typescript:
|
|
172
|
+
client_code = ClientGenerator.generate_typescript(base_url, endpoints)
|
|
173
|
+
Path(output_typescript).write_text(client_code)
|
|
174
|
+
click.echo(f"📘 TypeScript client: {output_typescript}")
|
|
175
|
+
|
|
176
|
+
if output_go:
|
|
177
|
+
client_code = ClientGenerator.generate_go(base_url, endpoints)
|
|
178
|
+
Path(output_go).write_text(client_code)
|
|
179
|
+
click.echo(f"🐹 Go client: {output_go}")
|
|
180
|
+
|
|
181
|
+
if output_rust:
|
|
182
|
+
client_code = ClientGenerator.generate_rust(base_url, endpoints)
|
|
183
|
+
Path(output_rust).write_text(client_code)
|
|
184
|
+
click.echo(f"🦀 Rust client: {output_rust}")
|
|
185
|
+
|
|
186
|
+
if output_java:
|
|
187
|
+
client_code = ClientGenerator.generate_java(base_url, endpoints)
|
|
188
|
+
Path(output_java).write_text(client_code)
|
|
189
|
+
click.echo(f"☕ Java client: {output_java}")
|
|
190
|
+
|
|
191
|
+
if output_ruby:
|
|
192
|
+
client_code = ClientGenerator.generate_ruby(base_url, endpoints)
|
|
193
|
+
Path(output_ruby).write_text(client_code)
|
|
194
|
+
click.echo(f"💎 Ruby client: {output_ruby}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@cli.command()
|
|
198
|
+
@click.argument("url")
|
|
199
|
+
@click.argument("schema_file")
|
|
200
|
+
@click.option("--threshold", default=0.9, help="Similarity threshold for drift detection")
|
|
201
|
+
@click.option("--no-cache", is_flag=True, default=False, help="Skip cache write")
|
|
202
|
+
@click.option("--cache-db", default=None, help="Path to SQLite cache DB")
|
|
203
|
+
def drift(url: str, schema_file: str, threshold: float, no_cache: bool, cache_db: Optional[str]):
|
|
204
|
+
"""Detect schema drift from previous inference.
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
api-adapter drift https://api.example.com schema.json
|
|
208
|
+
"""
|
|
209
|
+
schema_path = Path(schema_file)
|
|
210
|
+
if not schema_path.exists():
|
|
211
|
+
click.echo(f"❌ Schema file not found: {schema_file}", err=True)
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
with open(schema_path) as f:
|
|
215
|
+
old_schema = json.load(f)
|
|
216
|
+
|
|
217
|
+
click.echo(f"🔍 Re-inferring schema from {url}...")
|
|
218
|
+
sampler = EndpointSampler(url)
|
|
219
|
+
responses = sampler.run()
|
|
220
|
+
|
|
221
|
+
inferrer = SchemaInferrer(base_url=url)
|
|
222
|
+
for key, response in responses.items():
|
|
223
|
+
method, endpoint = key.split(" ", 1)
|
|
224
|
+
inferrer.add_response(endpoint, method, response)
|
|
225
|
+
|
|
226
|
+
new_schema = inferrer.to_dict()
|
|
227
|
+
|
|
228
|
+
# Compute drift diff using threshold
|
|
229
|
+
diff = compute_drift_diff(old_schema, new_schema)
|
|
230
|
+
similarity = diff["similarity"]
|
|
231
|
+
|
|
232
|
+
if similarity < threshold:
|
|
233
|
+
click.echo(f"⚠️ Schema drift detected! Similarity: {similarity:.2%} (threshold: {threshold:.0%})")
|
|
234
|
+
if diff["added_endpoints"]:
|
|
235
|
+
click.echo(f" Added endpoints: {', '.join(diff['added_endpoints'])}")
|
|
236
|
+
if diff["removed_endpoints"]:
|
|
237
|
+
click.echo(f" Removed endpoints: {', '.join(diff['removed_endpoints'])}")
|
|
238
|
+
if diff["added_fields"]:
|
|
239
|
+
click.echo(f" Added fields: {len(diff['added_fields'])}")
|
|
240
|
+
if diff["removed_fields"]:
|
|
241
|
+
click.echo(f" Removed fields: {len(diff['removed_fields'])}")
|
|
242
|
+
if diff["changed_types"]:
|
|
243
|
+
click.echo(f" Changed types: {len(diff['changed_types'])}")
|
|
244
|
+
else:
|
|
245
|
+
click.echo(f"✅ No significant drift (similarity: {similarity:.2%}, threshold: {threshold:.0%})")
|
|
246
|
+
|
|
247
|
+
# Save drift event to cache if not disabled
|
|
248
|
+
if not no_cache and similarity < threshold:
|
|
249
|
+
try:
|
|
250
|
+
with SchemaCache(db_path=cache_db) as cache:
|
|
251
|
+
old_snap = cache.latest_snapshot(url)
|
|
252
|
+
if old_snap:
|
|
253
|
+
old_id = 1 # In real usage, would query the ID
|
|
254
|
+
new_id = cache.save_snapshot(url, new_schema)
|
|
255
|
+
cache.save_drift_event(url, old_id, new_id, similarity, diff)
|
|
256
|
+
click.echo(f"📦 Drift event cached")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
click.echo(f"⚠️ Cache write failed: {e}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
cli()
|