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.
Files changed (46) hide show
  1. ai_data_platform/__about__.py +3 -0
  2. ai_data_platform/__init__.py +25 -0
  3. ai_data_platform/api/__init__.py +1 -0
  4. ai_data_platform/api/app.py +118 -0
  5. ai_data_platform/cli.py +408 -0
  6. ai_data_platform/config.py +156 -0
  7. ai_data_platform/connectors/__init__.py +61 -0
  8. ai_data_platform/connectors/base.py +128 -0
  9. ai_data_platform/connectors/duckdb_conn.py +120 -0
  10. ai_data_platform/connectors/files.py +111 -0
  11. ai_data_platform/connectors/placeholders.py +57 -0
  12. ai_data_platform/connectors/sql_db.py +134 -0
  13. ai_data_platform/core/__init__.py +1 -0
  14. ai_data_platform/core/exceptions.py +84 -0
  15. ai_data_platform/core/logging.py +62 -0
  16. ai_data_platform/core/paths.py +48 -0
  17. ai_data_platform/docs/__init__.py +5 -0
  18. ai_data_platform/docs/generator.py +69 -0
  19. ai_data_platform/generator/__init__.py +5 -0
  20. ai_data_platform/generator/engine.py +328 -0
  21. ai_data_platform/generator/samplers.py +553 -0
  22. ai_data_platform/generator/writers.py +62 -0
  23. ai_data_platform/mcp/__init__.py +1 -0
  24. ai_data_platform/mcp/server.py +285 -0
  25. ai_data_platform/metadata/__init__.py +5 -0
  26. ai_data_platform/metadata/catalog.py +431 -0
  27. ai_data_platform/metadata/models.py +148 -0
  28. ai_data_platform/metadata/scan.py +121 -0
  29. ai_data_platform/profiler/__init__.py +5 -0
  30. ai_data_platform/profiler/profiler.py +258 -0
  31. ai_data_platform/quality/__init__.py +5 -0
  32. ai_data_platform/quality/checks.py +243 -0
  33. ai_data_platform/sdk.py +210 -0
  34. ai_data_platform/semantic/__init__.py +5 -0
  35. ai_data_platform/semantic/builder.py +210 -0
  36. ai_data_platform/spec.py +517 -0
  37. ai_data_platform/sql/__init__.py +5 -0
  38. ai_data_platform/sql/assistant.py +126 -0
  39. ai_data_platform/sql/providers.py +152 -0
  40. ai_data_platform/ui/__init__.py +1 -0
  41. ai_data_platform/ui/static/index.html +173 -0
  42. ai_data_platform-0.1.0.dist-info/METADATA +505 -0
  43. ai_data_platform-0.1.0.dist-info/RECORD +46 -0
  44. ai_data_platform-0.1.0.dist-info/WHEEL +4 -0
  45. ai_data_platform-0.1.0.dist-info/entry_points.txt +2 -0
  46. ai_data_platform-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the package version."""
2
+
3
+ __version__ = "0.1.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
@@ -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()