vclient 0.2.0__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.
- vclient-0.2.0/PKG-INFO +130 -0
- vclient-0.2.0/README.md +95 -0
- vclient-0.2.0/pyproject.toml +58 -0
- vclient-0.2.0/setup.cfg +4 -0
- vclient-0.2.0/setup.py +48 -0
- vclient-0.2.0/src/__init__.py +15 -0
- vclient-0.2.0/src/__main__.py +5 -0
- vclient-0.2.0/src/cache.py +159 -0
- vclient-0.2.0/src/cli.py +262 -0
- vclient-0.2.0/src/codegen.py +674 -0
- vclient-0.2.0/src/inferrer.py +302 -0
- vclient-0.2.0/src/sampler.py +231 -0
- vclient-0.2.0/tests/test_cli.py +255 -0
- vclient-0.2.0/tests/test_codegen.py +199 -0
- vclient-0.2.0/tests/test_e2e.py +157 -0
- vclient-0.2.0/tests/test_inferrer.py +198 -0
- vclient-0.2.0/tests/test_sampler.py +103 -0
- vclient-0.2.0/vclient.egg-info/PKG-INFO +130 -0
- vclient-0.2.0/vclient.egg-info/SOURCES.txt +21 -0
- vclient-0.2.0/vclient.egg-info/dependency_links.txt +1 -0
- vclient-0.2.0/vclient.egg-info/entry_points.txt +2 -0
- vclient-0.2.0/vclient.egg-info/requires.txt +13 -0
- vclient-0.2.0/vclient.egg-info/top_level.txt +1 -0
vclient-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vclient
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Auto-discover API schemas and generate type-safe clients in 6 languages
|
|
5
|
+
Home-page: https://github.com/yc-trails/dynamic-api-adapter
|
|
6
|
+
Author: Dynamic API Adapter
|
|
7
|
+
Author-email: Dynamic API Adapter <dev@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/yc-trails/dynamic-api-adapter
|
|
10
|
+
Project-URL: Repository, https://github.com/yc-trails/dynamic-api-adapter
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: aiohttp>=3.9.5
|
|
22
|
+
Requires-Dist: pydantic>=2.7.0
|
|
23
|
+
Requires-Dist: click>=8.1.7
|
|
24
|
+
Requires-Dist: jinja2>=3.1.2
|
|
25
|
+
Requires-Dist: jsonschema>=4.21.1
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.4.4; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pyinstaller>=6.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: wheel>=0.42.0; extra == "dev"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
|
|
36
|
+
# vclient — Auto-Generate API Clients
|
|
37
|
+
|
|
38
|
+
Auto-discover OpenAPI endpoints and generate **type-safe client libraries in 6 languages** instantly.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
✨ **Single Command:**
|
|
43
|
+
```bash
|
|
44
|
+
vclient infer https://api.example.com
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Generates:
|
|
48
|
+
- 🐍 **Python** (httpx + Pydantic models)
|
|
49
|
+
- 📘 **TypeScript** (Fetch API + Zod validation)
|
|
50
|
+
- 🐹 **Go** (net/http)
|
|
51
|
+
- 🦀 **Rust** (reqwest + serde)
|
|
52
|
+
- ☕ **Java** (HttpClient)
|
|
53
|
+
- 💎 **Ruby** (Faraday)
|
|
54
|
+
|
|
55
|
+
## How It Works
|
|
56
|
+
|
|
57
|
+
1. **Discover** — Probe API endpoints and extract OpenAPI specs
|
|
58
|
+
2. **Infer** — Analyze responses → infer complete JSON schemas
|
|
59
|
+
3. **Cache** — Store schemas, detect drift on next run
|
|
60
|
+
4. **Generate** — Produce idiomatic clients for all 6 languages
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# via PyPI
|
|
66
|
+
pip install vclient
|
|
67
|
+
|
|
68
|
+
# via Homebrew (after setup)
|
|
69
|
+
brew install yc-trails/vclient/vclient
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Infer schema from live API
|
|
76
|
+
vclient infer https://be.vyonica.com --max-endpoints 25 \
|
|
77
|
+
--gen-python --gen-typescript --gen-go --gen-rust --gen-java --gen-ruby
|
|
78
|
+
|
|
79
|
+
# Generates: schema.json + client.py + client.ts + client.go + client.rs + Client.java + client.rb
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Generate from Existing Schema
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
vclient codegen schema.json \
|
|
86
|
+
--python client.py \
|
|
87
|
+
--typescript client.ts \
|
|
88
|
+
--go client.go \
|
|
89
|
+
--rust client.rs \
|
|
90
|
+
--java Client.java \
|
|
91
|
+
--ruby client.rb
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Architecture
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
vclient/
|
|
98
|
+
├── src/
|
|
99
|
+
│ ├── cli.py # Command-line interface
|
|
100
|
+
│ ├── sampler.py # Endpoint discovery + HTTP sampling
|
|
101
|
+
│ ├── inferrer.py # Schema inference from responses
|
|
102
|
+
│ ├── codegen.py # 6-language code generation
|
|
103
|
+
│ └── cache.py # SQLite schema caching + drift detection
|
|
104
|
+
├── tests/ # 55+ unit tests
|
|
105
|
+
├── test-clients/ # Generated examples (153 Vyonica endpoints)
|
|
106
|
+
└── Formula/vclient.rb # Homebrew formula
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Testing
|
|
110
|
+
|
|
111
|
+
All generators tested on real-world APIs:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Run test suite
|
|
115
|
+
pytest tests/ -v
|
|
116
|
+
|
|
117
|
+
# Test with Vyonica API (153 endpoints)
|
|
118
|
+
vclient infer https://be.vyonica.com --no-cache
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## What's Next
|
|
122
|
+
|
|
123
|
+
- [ ] Windows binary (Windows Subsystem for Linux compatible)
|
|
124
|
+
- [ ] Kotlin support (JVM ecosystem)
|
|
125
|
+
- [ ] GraphQL support
|
|
126
|
+
- [ ] API documentation generation
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
**Made for:** AI agents, SaaS integrators, enterprises calling legacy APIs without OpenAPI specs.
|
vclient-0.2.0/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# vclient — Auto-Generate API Clients
|
|
2
|
+
|
|
3
|
+
Auto-discover OpenAPI endpoints and generate **type-safe client libraries in 6 languages** instantly.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
✨ **Single Command:**
|
|
8
|
+
```bash
|
|
9
|
+
vclient infer https://api.example.com
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Generates:
|
|
13
|
+
- 🐍 **Python** (httpx + Pydantic models)
|
|
14
|
+
- 📘 **TypeScript** (Fetch API + Zod validation)
|
|
15
|
+
- 🐹 **Go** (net/http)
|
|
16
|
+
- 🦀 **Rust** (reqwest + serde)
|
|
17
|
+
- ☕ **Java** (HttpClient)
|
|
18
|
+
- 💎 **Ruby** (Faraday)
|
|
19
|
+
|
|
20
|
+
## How It Works
|
|
21
|
+
|
|
22
|
+
1. **Discover** — Probe API endpoints and extract OpenAPI specs
|
|
23
|
+
2. **Infer** — Analyze responses → infer complete JSON schemas
|
|
24
|
+
3. **Cache** — Store schemas, detect drift on next run
|
|
25
|
+
4. **Generate** — Produce idiomatic clients for all 6 languages
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# via PyPI
|
|
31
|
+
pip install vclient
|
|
32
|
+
|
|
33
|
+
# via Homebrew (after setup)
|
|
34
|
+
brew install yc-trails/vclient/vclient
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Infer schema from live API
|
|
41
|
+
vclient infer https://be.vyonica.com --max-endpoints 25 \
|
|
42
|
+
--gen-python --gen-typescript --gen-go --gen-rust --gen-java --gen-ruby
|
|
43
|
+
|
|
44
|
+
# Generates: schema.json + client.py + client.ts + client.go + client.rs + Client.java + client.rb
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Generate from Existing Schema
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
vclient codegen schema.json \
|
|
51
|
+
--python client.py \
|
|
52
|
+
--typescript client.ts \
|
|
53
|
+
--go client.go \
|
|
54
|
+
--rust client.rs \
|
|
55
|
+
--java Client.java \
|
|
56
|
+
--ruby client.rb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Architecture
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
vclient/
|
|
63
|
+
├── src/
|
|
64
|
+
│ ├── cli.py # Command-line interface
|
|
65
|
+
│ ├── sampler.py # Endpoint discovery + HTTP sampling
|
|
66
|
+
│ ├── inferrer.py # Schema inference from responses
|
|
67
|
+
│ ├── codegen.py # 6-language code generation
|
|
68
|
+
│ └── cache.py # SQLite schema caching + drift detection
|
|
69
|
+
├── tests/ # 55+ unit tests
|
|
70
|
+
├── test-clients/ # Generated examples (153 Vyonica endpoints)
|
|
71
|
+
└── Formula/vclient.rb # Homebrew formula
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Testing
|
|
75
|
+
|
|
76
|
+
All generators tested on real-world APIs:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Run test suite
|
|
80
|
+
pytest tests/ -v
|
|
81
|
+
|
|
82
|
+
# Test with Vyonica API (153 endpoints)
|
|
83
|
+
vclient infer https://be.vyonica.com --no-cache
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## What's Next
|
|
87
|
+
|
|
88
|
+
- [ ] Windows binary (Windows Subsystem for Linux compatible)
|
|
89
|
+
- [ ] Kotlin support (JVM ecosystem)
|
|
90
|
+
- [ ] GraphQL support
|
|
91
|
+
- [ ] API documentation generation
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
**Made for:** AI agents, SaaS integrators, enterprises calling legacy APIs without OpenAPI specs.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vclient"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Auto-discover API schemas and generate type-safe clients in 6 languages"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Dynamic API Adapter", email = "dev@example.com"}
|
|
13
|
+
]
|
|
14
|
+
license = {text = "MIT"}
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"aiohttp>=3.9.5",
|
|
27
|
+
"pydantic>=2.7.0",
|
|
28
|
+
"click>=8.1.7",
|
|
29
|
+
"jinja2>=3.1.2",
|
|
30
|
+
"jsonschema>=4.21.1",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.4.4",
|
|
36
|
+
"pytest-asyncio>=0.23.0",
|
|
37
|
+
"pytest-cov>=4.1.0",
|
|
38
|
+
"pyinstaller>=6.0.0",
|
|
39
|
+
"wheel>=0.42.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
vclient = "src.cli:cli"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/yc-trails/dynamic-api-adapter"
|
|
47
|
+
Repository = "https://github.com/yc-trails/dynamic-api-adapter"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools]
|
|
50
|
+
packages = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
|
|
56
|
+
[tool.pyinstaller]
|
|
57
|
+
# PyInstaller configuration
|
|
58
|
+
hidden_imports = ["jinja2"]
|
vclient-0.2.0/setup.cfg
ADDED
vclient-0.2.0/setup.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Setup configuration for Dynamic API Adapter."""
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
|
|
4
|
+
with open("README.md", "r", encoding="utf-8") as f:
|
|
5
|
+
long_description = f.read()
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="vclient",
|
|
9
|
+
version="0.2.0",
|
|
10
|
+
description="Auto-discover API schemas and generate type-safe clients in 6 languages",
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
author="Dynamic API Adapter",
|
|
14
|
+
url="https://github.com/yc-trails/dynamic-api-adapter",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
python_requires=">=3.10",
|
|
17
|
+
install_requires=[
|
|
18
|
+
"httpx>=0.27.0",
|
|
19
|
+
"aiohttp>=3.9.5",
|
|
20
|
+
"pydantic>=2.7.0",
|
|
21
|
+
"click>=8.1.7",
|
|
22
|
+
"jinja2>=3.1.2",
|
|
23
|
+
"jsonschema>=4.21.1",
|
|
24
|
+
],
|
|
25
|
+
extras_require={
|
|
26
|
+
"dev": [
|
|
27
|
+
"pytest>=7.4.4",
|
|
28
|
+
"pytest-asyncio>=0.23.0",
|
|
29
|
+
"pytest-cov>=4.1.0",
|
|
30
|
+
"pyinstaller>=6.0.0",
|
|
31
|
+
"wheel>=0.42.0",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
entry_points={
|
|
35
|
+
"console_scripts": [
|
|
36
|
+
"vclient=src.cli:cli",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
classifiers=[
|
|
40
|
+
"Development Status :: 3 - Alpha",
|
|
41
|
+
"Intended Audience :: Developers",
|
|
42
|
+
"License :: OSI Approved :: MIT License",
|
|
43
|
+
"Programming Language :: Python :: 3",
|
|
44
|
+
"Programming Language :: Python :: 3.10",
|
|
45
|
+
"Programming Language :: Python :: 3.11",
|
|
46
|
+
"Programming Language :: Python :: 3.12",
|
|
47
|
+
],
|
|
48
|
+
)
|
|
@@ -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"
|
|
@@ -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
|