ai-data-platform 0.1.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.
- ai_data_platform/__about__.py +3 -0
- ai_data_platform/__init__.py +25 -0
- ai_data_platform/api/__init__.py +1 -0
- ai_data_platform/api/app.py +118 -0
- ai_data_platform/cli.py +408 -0
- ai_data_platform/config.py +156 -0
- ai_data_platform/connectors/__init__.py +61 -0
- ai_data_platform/connectors/base.py +128 -0
- ai_data_platform/connectors/duckdb_conn.py +120 -0
- ai_data_platform/connectors/files.py +111 -0
- ai_data_platform/connectors/placeholders.py +57 -0
- ai_data_platform/connectors/sql_db.py +134 -0
- ai_data_platform/core/__init__.py +1 -0
- ai_data_platform/core/exceptions.py +84 -0
- ai_data_platform/core/logging.py +62 -0
- ai_data_platform/core/paths.py +48 -0
- ai_data_platform/docs/__init__.py +5 -0
- ai_data_platform/docs/generator.py +69 -0
- ai_data_platform/generator/__init__.py +5 -0
- ai_data_platform/generator/engine.py +328 -0
- ai_data_platform/generator/samplers.py +553 -0
- ai_data_platform/generator/writers.py +62 -0
- ai_data_platform/mcp/__init__.py +1 -0
- ai_data_platform/mcp/server.py +285 -0
- ai_data_platform/metadata/__init__.py +5 -0
- ai_data_platform/metadata/catalog.py +431 -0
- ai_data_platform/metadata/models.py +148 -0
- ai_data_platform/metadata/scan.py +121 -0
- ai_data_platform/profiler/__init__.py +5 -0
- ai_data_platform/profiler/profiler.py +258 -0
- ai_data_platform/quality/__init__.py +5 -0
- ai_data_platform/quality/checks.py +243 -0
- ai_data_platform/sdk.py +210 -0
- ai_data_platform/semantic/__init__.py +5 -0
- ai_data_platform/semantic/builder.py +210 -0
- ai_data_platform/spec.py +517 -0
- ai_data_platform/sql/__init__.py +5 -0
- ai_data_platform/sql/assistant.py +126 -0
- ai_data_platform/sql/providers.py +152 -0
- ai_data_platform/ui/__init__.py +1 -0
- ai_data_platform/ui/static/index.html +173 -0
- ai_data_platform-0.1.0.dist-info/METADATA +505 -0
- ai_data_platform-0.1.0.dist-info/RECORD +46 -0
- ai_data_platform-0.1.0.dist-info/WHEEL +4 -0
- ai_data_platform-0.1.0.dist-info/entry_points.txt +2 -0
- ai_data_platform-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""ai-data-platform: local-first AI data platform.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
from ai_data_platform import ADPClient, __version__
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from ai_data_platform.__about__ import __version__
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
14
|
+
from ai_data_platform.sdk import ADPClient
|
|
15
|
+
|
|
16
|
+
__all__ = ["ADPClient", "__version__"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __getattr__(name: str) -> Any:
|
|
20
|
+
"""Lazy import to keep `import ai_data_platform` fast and side-effect free."""
|
|
21
|
+
if name == "ADPClient":
|
|
22
|
+
from ai_data_platform.sdk import ADPClient
|
|
23
|
+
|
|
24
|
+
return ADPClient
|
|
25
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Local FastAPI application."""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Local REST API — a thin layer over ADPClient (same backend as CLI/MCP).
|
|
2
|
+
|
|
3
|
+
Binds to localhost by default; this is a single-user local console, not a
|
|
4
|
+
multi-tenant service (that's the platform's job).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI, HTTPException
|
|
13
|
+
from fastapi.responses import HTMLResponse
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ai_data_platform.__about__ import __version__
|
|
17
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
18
|
+
from ai_data_platform.sdk import ADPClient
|
|
19
|
+
|
|
20
|
+
UI_HTML = Path(__file__).parent.parent / "ui" / "static" / "index.html"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GenerateRequest(BaseModel):
|
|
24
|
+
rows: int | None = Field(default=None, ge=1, le=10_000_000)
|
|
25
|
+
tables: list[str] | None = None
|
|
26
|
+
seed: int | None = None
|
|
27
|
+
output_format: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SQLRequest(BaseModel):
|
|
31
|
+
question: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SemanticRequest(BaseModel):
|
|
35
|
+
name: str = "default"
|
|
36
|
+
format: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProfileRequest(BaseModel):
|
|
40
|
+
source: str | None = None
|
|
41
|
+
sample_rows: int = Field(default=10_000, ge=100, le=1_000_000)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_app(project_path: str = ".") -> FastAPI:
|
|
45
|
+
app = FastAPI(
|
|
46
|
+
title="ai-data-platform",
|
|
47
|
+
version=__version__,
|
|
48
|
+
description="Local AI data platform API. Single-user; do not expose publicly.",
|
|
49
|
+
)
|
|
50
|
+
client = ADPClient(project_path)
|
|
51
|
+
|
|
52
|
+
def _run(fn: Any, *args: Any, **kwargs: Any) -> Any:
|
|
53
|
+
try:
|
|
54
|
+
return fn(*args, **kwargs)
|
|
55
|
+
except ADPError as e:
|
|
56
|
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
57
|
+
|
|
58
|
+
@app.get("/health")
|
|
59
|
+
def health() -> dict[str, str]:
|
|
60
|
+
return {"status": "ok", "version": __version__}
|
|
61
|
+
|
|
62
|
+
@app.get("/sources")
|
|
63
|
+
def sources() -> list[dict[str, Any]]:
|
|
64
|
+
return _run(client.catalog.list_sources)
|
|
65
|
+
|
|
66
|
+
@app.get("/metadata/tables")
|
|
67
|
+
def tables() -> list[dict[str, Any]]:
|
|
68
|
+
return _run(client.list_tables)
|
|
69
|
+
|
|
70
|
+
@app.get("/metadata/tables/{table}")
|
|
71
|
+
def table_detail(table: str) -> dict[str, Any]:
|
|
72
|
+
return _run(client.get_table, table)
|
|
73
|
+
|
|
74
|
+
@app.get("/metadata/search")
|
|
75
|
+
def search(q: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
76
|
+
return _run(client.search_metadata, q, limit)
|
|
77
|
+
|
|
78
|
+
@app.post("/scan")
|
|
79
|
+
def scan() -> list[dict[str, Any]]:
|
|
80
|
+
return _run(client.scan)
|
|
81
|
+
|
|
82
|
+
@app.post("/profile/run")
|
|
83
|
+
def profile(req: ProfileRequest) -> list[dict[str, Any]]:
|
|
84
|
+
return _run(client.profile, req.source, sample_rows=req.sample_rows)
|
|
85
|
+
|
|
86
|
+
@app.post("/generate-data")
|
|
87
|
+
def generate(req: GenerateRequest) -> dict[str, Any]:
|
|
88
|
+
return _run(
|
|
89
|
+
client.generate_data,
|
|
90
|
+
req.rows,
|
|
91
|
+
tables=req.tables,
|
|
92
|
+
seed=req.seed,
|
|
93
|
+
output_format=req.output_format,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@app.post("/semantic-model")
|
|
97
|
+
def semantic(req: SemanticRequest) -> dict[str, Any]:
|
|
98
|
+
return _run(client.create_semantic_model, req.name, req.format)
|
|
99
|
+
|
|
100
|
+
@app.post("/quality-check")
|
|
101
|
+
def quality() -> dict[str, Any]:
|
|
102
|
+
return _run(client.quality_check)
|
|
103
|
+
|
|
104
|
+
@app.post("/sql")
|
|
105
|
+
def sql(req: SQLRequest) -> dict[str, Any]:
|
|
106
|
+
return _run(client.generate_sql, req.question)
|
|
107
|
+
|
|
108
|
+
@app.post("/docs/generate")
|
|
109
|
+
def docs() -> dict[str, str]:
|
|
110
|
+
return {"markdown": _run(client.generate_docs)}
|
|
111
|
+
|
|
112
|
+
@app.get("/", response_class=HTMLResponse)
|
|
113
|
+
def index() -> str:
|
|
114
|
+
if UI_HTML.exists():
|
|
115
|
+
return UI_HTML.read_text(encoding="utf-8")
|
|
116
|
+
return "<h1>ai-data-platform</h1><p>UI asset missing; API is live at /docs</p>"
|
|
117
|
+
|
|
118
|
+
return app
|
ai_data_platform/cli.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""adp — the ai-data-platform CLI (Typer).
|
|
2
|
+
|
|
3
|
+
Thin presentation layer: every command delegates to ADPClient.
|
|
4
|
+
Heavy imports happen inside commands to keep `adp --help` fast.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ai_data_platform.__about__ import __version__
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
name="adp",
|
|
20
|
+
help="AI Data Platform: catalog, profile, generate, model, query — local-first.",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
pretty_exceptions_show_locals=False,
|
|
23
|
+
)
|
|
24
|
+
console = Console()
|
|
25
|
+
err_console = Console(stderr=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _client(path: str = ".") -> object:
|
|
29
|
+
from ai_data_platform.sdk import ADPClient
|
|
30
|
+
|
|
31
|
+
return ADPClient(path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fail(e: Exception) -> None:
|
|
35
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
36
|
+
raise typer.Exit(code=1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.callback()
|
|
40
|
+
def _version_callback() -> None:
|
|
41
|
+
"""AI Data Platform CLI."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def version() -> None:
|
|
46
|
+
"""Print the installed version."""
|
|
47
|
+
console.print(f"ai-data-platform {__version__}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def init(
|
|
52
|
+
name: str = typer.Option(None, "--name", "-n", help="Project name (default: directory name)."),
|
|
53
|
+
path: str = typer.Option(".", "--path", help="Project directory."),
|
|
54
|
+
force: bool = typer.Option(False, "--force", help="Overwrite an existing adp.yaml."),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Initialize a project: create adp.yaml and the local catalog directory."""
|
|
57
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
58
|
+
from ai_data_platform.sdk import ADPClient
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
cfg_path = ADPClient(path).init(name, force=force)
|
|
62
|
+
console.print(f"[green]✓[/green] Created {cfg_path}")
|
|
63
|
+
console.print("Next: [bold]adp connect[/bold] to add a data source.")
|
|
64
|
+
except ADPError as e:
|
|
65
|
+
_fail(e)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command()
|
|
69
|
+
def connect(
|
|
70
|
+
name: str = typer.Option(..., "--name", "-n", help="Source name."),
|
|
71
|
+
type_: str = typer.Option(..., "--type", "-t", help="csv|parquet|duckdb|postgres|mysql|…"),
|
|
72
|
+
path: str = typer.Option(None, "--path", help="File/directory path (csv, parquet, duckdb)."),
|
|
73
|
+
dsn: str = typer.Option(
|
|
74
|
+
None, "--dsn", help='DSN, e.g. "postgresql+psycopg://u:${PGPASSWORD}@h/db"'
|
|
75
|
+
),
|
|
76
|
+
schema: str = typer.Option(None, "--schema", help="Database schema (postgres/mysql)."),
|
|
77
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
78
|
+
no_test: bool = typer.Option(False, "--no-test", help="Skip the connection test."),
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Add a data source to adp.yaml (tests the connection by default)."""
|
|
81
|
+
from ai_data_platform.config import SourceConfig
|
|
82
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
83
|
+
from ai_data_platform.sdk import ADPClient
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
source = SourceConfig(name=name, type=type_, path=path, dsn=dsn, schema=schema) # type: ignore[arg-type]
|
|
87
|
+
result = ADPClient(project).add_source(source, test=not no_test)
|
|
88
|
+
if result.get("ok"):
|
|
89
|
+
console.print(
|
|
90
|
+
f"[green]✓[/green] Source [bold]{name}[/bold] added. {result.get('message', '')}"
|
|
91
|
+
)
|
|
92
|
+
console.print("Next: [bold]adp scan[/bold] to build the metadata catalog.")
|
|
93
|
+
else:
|
|
94
|
+
_fail(Exception(f"connection test failed: {result.get('message')}"))
|
|
95
|
+
except ADPError as e:
|
|
96
|
+
_fail(e)
|
|
97
|
+
except Exception as e: # pydantic validation
|
|
98
|
+
_fail(e)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command()
|
|
102
|
+
def scan(
|
|
103
|
+
source: str = typer.Option(None, "--source", "-s", help="Scan one source (default: all)."),
|
|
104
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Discover schemas, tables, columns, and relationship candidates."""
|
|
107
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
108
|
+
from ai_data_platform.sdk import ADPClient
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
summaries = ADPClient(project).scan(source)
|
|
112
|
+
t = Table(title="Scan results")
|
|
113
|
+
for col in ("source", "tables", "columns", "fk_candidates"):
|
|
114
|
+
t.add_column(col)
|
|
115
|
+
for s in summaries:
|
|
116
|
+
t.add_row(s["source"], str(s["tables"]), str(s["columns"]), str(s["fk_candidates"]))
|
|
117
|
+
console.print(t)
|
|
118
|
+
console.print("Next: [bold]adp profile[/bold] for statistics, PII, and key detection.")
|
|
119
|
+
except ADPError as e:
|
|
120
|
+
_fail(e)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command()
|
|
124
|
+
def profile(
|
|
125
|
+
source: str = typer.Option(None, "--source", "-s", help="Profile one source (default: all)."),
|
|
126
|
+
sample_rows: int = typer.Option(10_000, "--sample-rows", help="Sample budget per table."),
|
|
127
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Profile tables: stats, distributions, PII, PK/FK confirmation."""
|
|
130
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
131
|
+
from ai_data_platform.sdk import ADPClient
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
summaries = ADPClient(project).profile(source, sample_rows=sample_rows)
|
|
135
|
+
t = Table(title="Profile results")
|
|
136
|
+
t.add_column("table")
|
|
137
|
+
t.add_column("rows sampled")
|
|
138
|
+
t.add_column("pk candidates")
|
|
139
|
+
t.add_column("pii columns")
|
|
140
|
+
for s in summaries:
|
|
141
|
+
t.add_row(
|
|
142
|
+
s["table"],
|
|
143
|
+
str(s["rows_sampled"]),
|
|
144
|
+
", ".join(s["pk_candidates"]) or "—",
|
|
145
|
+
", ".join(s["pii_columns"]) or "—",
|
|
146
|
+
)
|
|
147
|
+
console.print(t)
|
|
148
|
+
except ADPError as e:
|
|
149
|
+
_fail(e)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.command("apply-spec")
|
|
153
|
+
def apply_spec(
|
|
154
|
+
spec: str = typer.Argument(..., help="Path to a dataset spec YAML."),
|
|
155
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Register a declarative dataset spec — generate with NO seed data.
|
|
158
|
+
|
|
159
|
+
Declare tables, columns, category weights, numeric ranges, and foreign keys
|
|
160
|
+
in YAML; then run `adp generate-data` directly (no scan/profile needed).
|
|
161
|
+
"""
|
|
162
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
163
|
+
from ai_data_platform.sdk import ADPClient
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
result = ADPClient(project).apply_spec(spec)
|
|
167
|
+
console.print(
|
|
168
|
+
f"[green]✓[/green] Spec applied: {result['tables']} table(s), "
|
|
169
|
+
f"{result['columns']} column(s), {result['relationships']} relationship(s)."
|
|
170
|
+
)
|
|
171
|
+
console.print("Next: [bold]adp generate-data --rows N[/bold]")
|
|
172
|
+
except ADPError as e:
|
|
173
|
+
_fail(e)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.command("generate-data")
|
|
177
|
+
def generate_data(
|
|
178
|
+
rows: int = typer.Option(None, "--rows", "-r", help="Default rows per table."),
|
|
179
|
+
rows_per_table: str = typer.Option(
|
|
180
|
+
None,
|
|
181
|
+
"--rows-per-table",
|
|
182
|
+
help='Per-table counts: "products=20,customers=1000,transactions=100000"',
|
|
183
|
+
),
|
|
184
|
+
tables: str = typer.Option(None, "--tables", help="Comma-separated subset of tables."),
|
|
185
|
+
seed: int = typer.Option(None, "--seed", help="Deterministic seed."),
|
|
186
|
+
output: str = typer.Option(None, "--output", "-o", help="csv|parquet|duckdb|sql"),
|
|
187
|
+
output_dir: str = typer.Option(None, "--output-dir", help="Output directory."),
|
|
188
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Generate synthetic data from the catalog (FK-safe, seeded, profile-driven)."""
|
|
191
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
192
|
+
from ai_data_platform.sdk import ADPClient
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
rpt: dict[str, int] | None = None
|
|
196
|
+
if rows_per_table:
|
|
197
|
+
try:
|
|
198
|
+
rpt = {
|
|
199
|
+
kv.split("=")[0].strip(): int(kv.split("=")[1])
|
|
200
|
+
for kv in rows_per_table.split(",")
|
|
201
|
+
}
|
|
202
|
+
except (IndexError, ValueError):
|
|
203
|
+
_fail(Exception('--rows-per-table format: "table=count,table2=count"'))
|
|
204
|
+
result = ADPClient(project).generate_data(
|
|
205
|
+
rows,
|
|
206
|
+
tables=[t.strip() for t in tables.split(",")] if tables else None,
|
|
207
|
+
seed=seed,
|
|
208
|
+
rows_per_table=rpt,
|
|
209
|
+
output_format=output,
|
|
210
|
+
output_dir=output_dir,
|
|
211
|
+
)
|
|
212
|
+
t = Table(title=f"Generated (seed={result['seed']}, format={result['format']})")
|
|
213
|
+
t.add_column("table")
|
|
214
|
+
t.add_column("rows")
|
|
215
|
+
t.add_column("path")
|
|
216
|
+
for name, info in result["tables"].items():
|
|
217
|
+
t.add_row(name, str(info["rows"]), info["path"])
|
|
218
|
+
console.print(t)
|
|
219
|
+
console.print("Next: [bold]adp quality-check[/bold] to validate the output.")
|
|
220
|
+
except ADPError as e:
|
|
221
|
+
_fail(e)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@app.command("quality-check")
|
|
225
|
+
def quality_check(
|
|
226
|
+
data_dir: str = typer.Option(None, "--data-dir", help="Directory of csv/parquet to check."),
|
|
227
|
+
report: str = typer.Option(None, "--report", help="Write a Markdown report to this path."),
|
|
228
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Run auto-derived quality checks and print the quality score."""
|
|
231
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
232
|
+
from ai_data_platform.quality.checks import report_to_markdown
|
|
233
|
+
from ai_data_platform.sdk import ADPClient
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
client = ADPClient(project)
|
|
237
|
+
rep = client.quality_check(data_dir)
|
|
238
|
+
color = (
|
|
239
|
+
"green"
|
|
240
|
+
if rep["quality_score"] >= 90
|
|
241
|
+
else "yellow"
|
|
242
|
+
if rep["quality_score"] >= 70
|
|
243
|
+
else "red"
|
|
244
|
+
)
|
|
245
|
+
console.print(f"Quality score: [{color}]{rep['quality_score']}/100[/{color}]")
|
|
246
|
+
for cat, v in rep["category_scores"].items():
|
|
247
|
+
console.print(f" {cat}: {v}")
|
|
248
|
+
for t in rep["tables"]:
|
|
249
|
+
failed = [c for c in t["checks"] if not c["passed"]]
|
|
250
|
+
if failed:
|
|
251
|
+
console.print(f"[yellow]{t['table']}[/yellow]: {len(failed)} failing check(s)")
|
|
252
|
+
for c in failed:
|
|
253
|
+
console.print(
|
|
254
|
+
f" ✗ {c['rule_type']} {c['params'].get('column', '')}: {c['evidence']}"
|
|
255
|
+
)
|
|
256
|
+
if report:
|
|
257
|
+
from ai_data_platform.core.paths import safe_write_text
|
|
258
|
+
|
|
259
|
+
path = safe_write_text(client.root, report, report_to_markdown(rep))
|
|
260
|
+
console.print(f"Report written to {path}")
|
|
261
|
+
except ADPError as e:
|
|
262
|
+
_fail(e)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.command("semantic-model")
|
|
266
|
+
def semantic_model(
|
|
267
|
+
name: str = typer.Option("default", "--name", help="Semantic model name."),
|
|
268
|
+
fmt: str = typer.Option(None, "--format", "-f", help="generic|cube"),
|
|
269
|
+
out: str = typer.Option(None, "--out", "-o", help="Write YAML to this path."),
|
|
270
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Build a semantic model (facts, dimensions, measures, joins) as YAML."""
|
|
273
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
274
|
+
from ai_data_platform.sdk import ADPClient
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
client = ADPClient(project)
|
|
278
|
+
result = client.create_semantic_model(name, fmt)
|
|
279
|
+
if out:
|
|
280
|
+
from ai_data_platform.core.paths import safe_write_text
|
|
281
|
+
|
|
282
|
+
path = safe_write_text(client.root, out, result["rendered"])
|
|
283
|
+
console.print(f"[green]✓[/green] Semantic model ({result['format']}) -> {path}")
|
|
284
|
+
else:
|
|
285
|
+
console.print(result["rendered"])
|
|
286
|
+
except ADPError as e:
|
|
287
|
+
_fail(e)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@app.command()
|
|
291
|
+
def sql(
|
|
292
|
+
question: str = typer.Argument(..., help="Natural-language question."),
|
|
293
|
+
execute: bool = typer.Option(
|
|
294
|
+
False, "--execute", help="Execute against a duckdb output/source."
|
|
295
|
+
),
|
|
296
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Ask a question; get a read-only SQL query (and optionally run it on DuckDB)."""
|
|
299
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
300
|
+
from ai_data_platform.sdk import ADPClient
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
client = ADPClient(project)
|
|
304
|
+
result = client.generate_sql(question)
|
|
305
|
+
console.print(f"[bold]SQL[/bold] (confidence {result['confidence']:.2f}):")
|
|
306
|
+
console.print(result["sql"])
|
|
307
|
+
if result["explanation"]:
|
|
308
|
+
console.print(f"[dim]{result['explanation']}[/dim]")
|
|
309
|
+
if execute:
|
|
310
|
+
import duckdb
|
|
311
|
+
|
|
312
|
+
from ai_data_platform.core.paths import safe_resolve
|
|
313
|
+
|
|
314
|
+
db = safe_resolve(client.root, Path(client.config.output_dir) / "generated.duckdb")
|
|
315
|
+
if not db.exists():
|
|
316
|
+
_fail(
|
|
317
|
+
Exception(
|
|
318
|
+
f"No DuckDB file at {db}. Generate with `adp generate-data -o duckdb` first."
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
with duckdb.connect(str(db), read_only=True) as con:
|
|
322
|
+
console.print(con.execute(result["sql"]).pl())
|
|
323
|
+
except ADPError as e:
|
|
324
|
+
_fail(e)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command()
|
|
328
|
+
def docs(
|
|
329
|
+
out: str = typer.Option("docs/data-dictionary.md", "--out", "-o", help="Output path."),
|
|
330
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Generate a Markdown data dictionary from the catalog."""
|
|
333
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
334
|
+
from ai_data_platform.core.paths import safe_write_text
|
|
335
|
+
from ai_data_platform.sdk import ADPClient
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
client = ADPClient(project)
|
|
339
|
+
path = safe_write_text(client.root, out, client.generate_docs())
|
|
340
|
+
console.print(f"[green]✓[/green] Data dictionary -> {path}")
|
|
341
|
+
except ADPError as e:
|
|
342
|
+
_fail(e)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@app.command()
|
|
346
|
+
def tables(
|
|
347
|
+
search: str = typer.Option(None, "--search", "-q", help="Search tables/columns."),
|
|
348
|
+
as_json: bool = typer.Option(False, "--json", help="JSON output."),
|
|
349
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
350
|
+
) -> None:
|
|
351
|
+
"""List or search cataloged tables."""
|
|
352
|
+
from ai_data_platform.core.exceptions import ADPError
|
|
353
|
+
from ai_data_platform.sdk import ADPClient
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
client = ADPClient(project)
|
|
357
|
+
rows = client.search_metadata(search) if search else client.list_tables()
|
|
358
|
+
if as_json:
|
|
359
|
+
console.print_json(json.dumps(rows))
|
|
360
|
+
return
|
|
361
|
+
t = Table(title="Catalog")
|
|
362
|
+
if rows:
|
|
363
|
+
for col in rows[0]:
|
|
364
|
+
t.add_column(str(col))
|
|
365
|
+
for r in rows:
|
|
366
|
+
t.add_row(*(str(v) if v is not None else "—" for v in r.values()))
|
|
367
|
+
console.print(t)
|
|
368
|
+
except ADPError as e:
|
|
369
|
+
_fail(e)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@app.command("mcp-server")
|
|
373
|
+
def mcp_server(
|
|
374
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Start the MCP server (stdio) for Claude, Cursor, Windsurf, VS Code."""
|
|
377
|
+
try:
|
|
378
|
+
from ai_data_platform.mcp.server import run_server
|
|
379
|
+
except ImportError:
|
|
380
|
+
_fail(Exception("MCP support requires the mcp extra: pip install 'ai-data-platform[mcp]'"))
|
|
381
|
+
return
|
|
382
|
+
run_server(project)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@app.command()
|
|
386
|
+
def ui(
|
|
387
|
+
host: str = typer.Option(
|
|
388
|
+
"127.0.0.1", "--host", help="Bind address (localhost only by default)."
|
|
389
|
+
),
|
|
390
|
+
port: int = typer.Option(8765, "--port", "-p"),
|
|
391
|
+
project: str = typer.Option(".", "--project", help="Project directory."),
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Start the local API + web console."""
|
|
394
|
+
import uvicorn
|
|
395
|
+
|
|
396
|
+
from ai_data_platform.api.app import create_app
|
|
397
|
+
|
|
398
|
+
if host not in ("127.0.0.1", "localhost"):
|
|
399
|
+
err_console.print(
|
|
400
|
+
"[yellow]Warning:[/yellow] binding beyond localhost exposes your catalog "
|
|
401
|
+
"to the network."
|
|
402
|
+
)
|
|
403
|
+
console.print(f"ADP console: http://{host}:{port}")
|
|
404
|
+
uvicorn.run(create_app(project), host=host, port=port, log_level="warning")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
app()
|