pretensor 0.1.0a0__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.
- pretensor/__init__.py +50 -0
- pretensor/cli/__init__.py +1 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +122 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +120 -0
- pretensor/cli/commands/index.py +551 -0
- pretensor/cli/commands/list.py +65 -0
- pretensor/cli/commands/quickstart.py +171 -0
- pretensor/cli/commands/reindex.py +581 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +129 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +156 -0
- pretensor/cli/config_file.py +412 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +81 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +145 -0
- pretensor/connectors/SKILL.md +16 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +463 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +91 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +921 -0
- pretensor/connectors/registry.py +51 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +840 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +268 -0
- pretensor/core/dsn_crypto.py +40 -0
- pretensor/core/ids.py +68 -0
- pretensor/core/portable_export.py +253 -0
- pretensor/core/registry.py +204 -0
- pretensor/core/schema.py +368 -0
- pretensor/core/store.py +1343 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +298 -0
- pretensor/enrichment/dbt/metadata.py +263 -0
- pretensor/enrichment/dbt/pipeline.py +77 -0
- pretensor/enrichment/dbt/resolution.py +101 -0
- pretensor/enrichment/dbt/signals.py +305 -0
- pretensor/entities/__init__.py +27 -0
- pretensor/entities/builder.py +63 -0
- pretensor/entities/classifier.py +359 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/graph_models/__init__.py +14 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +75 -0
- pretensor/graph_models/relationship.py +27 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +335 -0
- pretensor/intelligence/clustering.py +162 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +198 -0
- pretensor/intelligence/graph_export.py +139 -0
- pretensor/intelligence/heuristic.py +540 -0
- pretensor/intelligence/join_paths/__init__.py +130 -0
- pretensor/intelligence/join_paths/on_demand.py +516 -0
- pretensor/intelligence/join_paths/storage.py +70 -0
- pretensor/intelligence/llm_infer.py +78 -0
- pretensor/intelligence/llm_runtime.py +60 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +254 -0
- pretensor/intelligence/schema_classification.py +306 -0
- pretensor/intelligence/scoring.py +66 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +161 -0
- pretensor/introspection/__init__.py +6 -0
- pretensor/introspection/inspector.py +5 -0
- pretensor/introspection/models/__init__.py +0 -0
- pretensor/introspection/models/base.py +5 -0
- pretensor/introspection/models/config.py +236 -0
- pretensor/introspection/models/dsn.py +442 -0
- pretensor/introspection/models/plan.py +116 -0
- pretensor/introspection/models/schema.py +10 -0
- pretensor/introspection/models/semantic.py +121 -0
- pretensor/introspection/models/validation.py +116 -0
- pretensor/introspection/snapshot.py +46 -0
- pretensor/mcp/__init__.py +16 -0
- pretensor/mcp/config_json.py +24 -0
- pretensor/mcp/payload_types.py +248 -0
- pretensor/mcp/resources/__init__.py +19 -0
- pretensor/mcp/resources/markdown.py +420 -0
- pretensor/mcp/server.py +723 -0
- pretensor/mcp/service.py +47 -0
- pretensor/mcp/service_context.py +126 -0
- pretensor/mcp/service_registry.py +242 -0
- pretensor/mcp/tool_registry.py +126 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/compile_metric.py +87 -0
- pretensor/mcp/tools/context.py +894 -0
- pretensor/mcp/tools/cypher.py +333 -0
- pretensor/mcp/tools/detect_changes.py +226 -0
- pretensor/mcp/tools/impact.py +165 -0
- pretensor/mcp/tools/list.py +110 -0
- pretensor/mcp/tools/schema.py +128 -0
- pretensor/mcp/tools/search.py +108 -0
- pretensor/mcp/tools/traverse.py +753 -0
- pretensor/mcp/tools/validate_sql.py +74 -0
- pretensor/observability.py +203 -0
- pretensor/search/__init__.py +6 -0
- pretensor/search/base.py +80 -0
- pretensor/search/index.py +435 -0
- pretensor/semantic/__init__.py +24 -0
- pretensor/semantic/base.py +123 -0
- pretensor/semantic/compiler.py +346 -0
- pretensor/semantic/yaml_layer.py +182 -0
- pretensor/skills/__init__.py +5 -0
- pretensor/skills/generator.py +235 -0
- pretensor/staleness/__init__.py +15 -0
- pretensor/staleness/graph_patcher.py +355 -0
- pretensor/staleness/impact_analyzer.py +162 -0
- pretensor/staleness/snapshot_store.py +38 -0
- pretensor/validation/__init__.py +9 -0
- pretensor/validation/query_validator.py +436 -0
- pretensor/visibility/__init__.py +23 -0
- pretensor/visibility/config.py +126 -0
- pretensor/visibility/filter.py +143 -0
- pretensor/visibility/kuzu_helpers.py +32 -0
- pretensor/visibility/runtime.py +36 -0
- pretensor/visibility/sync_grants.py +188 -0
- pretensor-0.1.0a0.dist-info/METADATA +175 -0
- pretensor-0.1.0a0.dist-info/RECORD +138 -0
- pretensor-0.1.0a0.dist-info/WHEEL +4 -0
- pretensor-0.1.0a0.dist-info/entry_points.txt +2 -0
- pretensor-0.1.0a0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""``pretensor index`` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from pretensor.cli import constants as cli_constants
|
|
15
|
+
from pretensor.cli.config_file import (
|
|
16
|
+
get_cli_config,
|
|
17
|
+
resolve_optional_path_option,
|
|
18
|
+
resolve_optional_str_option,
|
|
19
|
+
resolve_path_option,
|
|
20
|
+
)
|
|
21
|
+
from pretensor.cli.dbt_enrichment import (
|
|
22
|
+
apply_dbt_enrichment_cli,
|
|
23
|
+
preload_dbt_manifest,
|
|
24
|
+
)
|
|
25
|
+
from pretensor.cli.paths import (
|
|
26
|
+
default_connection_name,
|
|
27
|
+
graph_file_for_connection,
|
|
28
|
+
keystore_path,
|
|
29
|
+
unified_graph_path,
|
|
30
|
+
)
|
|
31
|
+
from pretensor.connectors.inspect import inspect
|
|
32
|
+
from pretensor.core.builder import GraphBuilder
|
|
33
|
+
from pretensor.core.dsn_crypto import DSNEncryptor
|
|
34
|
+
from pretensor.core.registry import GraphRegistry
|
|
35
|
+
from pretensor.core.store import KuzuStore
|
|
36
|
+
from pretensor.introspection.models.dsn import (
|
|
37
|
+
connection_config_from_source,
|
|
38
|
+
connection_config_from_url,
|
|
39
|
+
dsn_from_source,
|
|
40
|
+
registry_dialect_for,
|
|
41
|
+
validate_source_env_vars,
|
|
42
|
+
)
|
|
43
|
+
from pretensor.observability import log_timed_operation
|
|
44
|
+
from pretensor.skills.generator import SkillGenerator
|
|
45
|
+
from pretensor.staleness.snapshot_store import SnapshotStore
|
|
46
|
+
from pretensor.visibility.config import load_visibility_config, merge_profile_into_base
|
|
47
|
+
from pretensor.visibility.filter import VisibilityFilter
|
|
48
|
+
|
|
49
|
+
_PROFILE_INDEX = os.environ.get("PRETENSOR_PROFILE_INDEX", "").lower() not in (
|
|
50
|
+
"",
|
|
51
|
+
"0",
|
|
52
|
+
"false",
|
|
53
|
+
"no",
|
|
54
|
+
)
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register_index_command(app: typer.Typer, *, console: Console) -> None:
|
|
59
|
+
@app.command("index")
|
|
60
|
+
def index_command(
|
|
61
|
+
dsn: str | None = typer.Argument(
|
|
62
|
+
None,
|
|
63
|
+
help=(
|
|
64
|
+
"Database DSN URL (postgresql://, postgres://, snowflake://, or bigquery://). "
|
|
65
|
+
"Mutually exclusive with --source and --all."
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
source: str | None = typer.Option(
|
|
69
|
+
None,
|
|
70
|
+
"--source",
|
|
71
|
+
"-s",
|
|
72
|
+
help="Named source from .pretensor/config.yaml sources: block.",
|
|
73
|
+
),
|
|
74
|
+
all_sources: bool = typer.Option(
|
|
75
|
+
False,
|
|
76
|
+
"--all",
|
|
77
|
+
help="Index all sources defined in .pretensor/config.yaml.",
|
|
78
|
+
),
|
|
79
|
+
dialect: str | None = typer.Option(
|
|
80
|
+
None,
|
|
81
|
+
"--dialect",
|
|
82
|
+
help=(
|
|
83
|
+
"Force connector: postgres, postgresql, snowflake, or bigquery "
|
|
84
|
+
"(overrides URL scheme). Only used with DSN argument."
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
name: str | None = typer.Option(
|
|
88
|
+
None,
|
|
89
|
+
"--name",
|
|
90
|
+
"-n",
|
|
91
|
+
help="Logical connection name (defaults to the database name from the DSN).",
|
|
92
|
+
),
|
|
93
|
+
state_dir: Path = typer.Option(
|
|
94
|
+
cli_constants.DEFAULT_STATE_DIR,
|
|
95
|
+
"--state-dir",
|
|
96
|
+
help="Directory for registry.json and graph files.",
|
|
97
|
+
file_okay=False,
|
|
98
|
+
dir_okay=True,
|
|
99
|
+
writable=True,
|
|
100
|
+
resolve_path=True,
|
|
101
|
+
),
|
|
102
|
+
unified: bool = typer.Option(
|
|
103
|
+
False,
|
|
104
|
+
"--unified",
|
|
105
|
+
help="Merge into a single Kuzu file (graphs/unified.kuzu) for cross-DB linking.",
|
|
106
|
+
),
|
|
107
|
+
skills_target: str = typer.Option(
|
|
108
|
+
"claude",
|
|
109
|
+
"--skills-target",
|
|
110
|
+
help=(
|
|
111
|
+
"Where to write the generated skill: claude (.claude/skills/), "
|
|
112
|
+
"cursor (.cursor/rules/), all (both), or a file path."
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
visibility_file: Path | None = typer.Option(
|
|
116
|
+
None,
|
|
117
|
+
"--visibility",
|
|
118
|
+
help="Path to visibility.yml (default: <state-dir>/visibility.yml).",
|
|
119
|
+
file_okay=True,
|
|
120
|
+
dir_okay=False,
|
|
121
|
+
resolve_path=True,
|
|
122
|
+
),
|
|
123
|
+
profile: str | None = typer.Option(
|
|
124
|
+
None,
|
|
125
|
+
"--profile",
|
|
126
|
+
help="Named visibility profile from visibility.yml (merged at index time).",
|
|
127
|
+
),
|
|
128
|
+
dbt_manifest: Path | None = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--dbt-manifest",
|
|
131
|
+
help=(
|
|
132
|
+
"Path to dbt target/manifest.json. Run `dbt compile` or `dbt run` first "
|
|
133
|
+
"so the manifest exists; then lineage, descriptions/tags, and exposure "
|
|
134
|
+
"signals are merged into the graph after indexing."
|
|
135
|
+
),
|
|
136
|
+
file_okay=True,
|
|
137
|
+
dir_okay=False,
|
|
138
|
+
resolve_path=True,
|
|
139
|
+
),
|
|
140
|
+
dbt_sources: Path | None = typer.Option(
|
|
141
|
+
None,
|
|
142
|
+
"--dbt-sources",
|
|
143
|
+
help=(
|
|
144
|
+
"Path to dbt sources.json (e.g. from `dbt source freshness`). "
|
|
145
|
+
"Defaults to target/sources.json next to the manifest when that file exists."
|
|
146
|
+
),
|
|
147
|
+
file_okay=True,
|
|
148
|
+
dir_okay=False,
|
|
149
|
+
resolve_path=True,
|
|
150
|
+
),
|
|
151
|
+
ctx: typer.Context = typer.Option(
|
|
152
|
+
None,
|
|
153
|
+
hidden=True,
|
|
154
|
+
),
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Introspect a database and write a Kuzu graph with explicit FK edges."""
|
|
157
|
+
cli_config = get_cli_config(ctx)
|
|
158
|
+
|
|
159
|
+
# --- mutual exclusion -------------------------------------------------
|
|
160
|
+
modes = sum([dsn is not None, source is not None, all_sources])
|
|
161
|
+
if modes == 0:
|
|
162
|
+
console.print(
|
|
163
|
+
"[red]Provide a DSN argument, --source <name>, or --all.[/red]"
|
|
164
|
+
)
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
if modes > 1:
|
|
167
|
+
console.print(
|
|
168
|
+
"[red]DSN argument, --source, and --all are mutually exclusive.[/red]"
|
|
169
|
+
)
|
|
170
|
+
raise typer.Exit(1)
|
|
171
|
+
|
|
172
|
+
state_dir = resolve_path_option(
|
|
173
|
+
ctx,
|
|
174
|
+
param_name="state_dir",
|
|
175
|
+
cli_value=state_dir,
|
|
176
|
+
config_value=cli_config.state_dir,
|
|
177
|
+
)
|
|
178
|
+
visibility_file = resolve_optional_path_option(
|
|
179
|
+
ctx,
|
|
180
|
+
param_name="visibility_file",
|
|
181
|
+
cli_value=visibility_file,
|
|
182
|
+
config_value=cli_config.visibility.path,
|
|
183
|
+
)
|
|
184
|
+
profile = resolve_optional_str_option(
|
|
185
|
+
ctx,
|
|
186
|
+
param_name="profile",
|
|
187
|
+
cli_value=profile,
|
|
188
|
+
config_value=cli_config.visibility.profile,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# --- --all: iterate all configured sources ----------------------------
|
|
192
|
+
if all_sources:
|
|
193
|
+
if not cli_config.sources:
|
|
194
|
+
console.print(
|
|
195
|
+
"[red]No sources defined in config.[/red] "
|
|
196
|
+
"Add a `sources:` section to .pretensor/config.yaml."
|
|
197
|
+
)
|
|
198
|
+
raise typer.Exit(1)
|
|
199
|
+
results: list[tuple[str, bool, str]] = []
|
|
200
|
+
for src_name, src_cfg in cli_config.sources.items():
|
|
201
|
+
console.print(
|
|
202
|
+
f"\n[bold]{'─' * 40}[/bold]\n"
|
|
203
|
+
f"[bold blue]Source:[/bold blue] {src_name}\n"
|
|
204
|
+
)
|
|
205
|
+
missing_vars = validate_source_env_vars(src_cfg)
|
|
206
|
+
if missing_vars:
|
|
207
|
+
msg = f"missing env vars: {', '.join(missing_vars)}"
|
|
208
|
+
console.print(f"[yellow]Skipping {src_name}:[/yellow] {msg}")
|
|
209
|
+
logger.warning("Skipping source %s: %s", src_name, msg)
|
|
210
|
+
results.append((src_name, False, f"skipped ({msg})"))
|
|
211
|
+
continue
|
|
212
|
+
try:
|
|
213
|
+
src_config = connection_config_from_source(src_name, src_cfg)
|
|
214
|
+
src_dsn = dsn_from_source(src_name, src_cfg)
|
|
215
|
+
_run_index(
|
|
216
|
+
console=console,
|
|
217
|
+
cli_config=cli_config,
|
|
218
|
+
dsn=src_dsn,
|
|
219
|
+
connection_name=src_name,
|
|
220
|
+
config=src_config,
|
|
221
|
+
state_dir=state_dir,
|
|
222
|
+
unified=unified,
|
|
223
|
+
skills_target=skills_target,
|
|
224
|
+
visibility_file=visibility_file,
|
|
225
|
+
profile=profile,
|
|
226
|
+
dbt_manifest=dbt_manifest,
|
|
227
|
+
dbt_sources=dbt_sources,
|
|
228
|
+
)
|
|
229
|
+
results.append((src_name, True, "ok"))
|
|
230
|
+
except typer.Exit:
|
|
231
|
+
logger.warning("Source %s exited with failure", src_name)
|
|
232
|
+
results.append((src_name, False, "failed"))
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.exception("Error indexing source %s", src_name)
|
|
235
|
+
console.print(f"[red]Error indexing {src_name}:[/red] {e}")
|
|
236
|
+
results.append((src_name, False, str(e)))
|
|
237
|
+
|
|
238
|
+
console.print(f"\n[bold]{'─' * 40}[/bold]")
|
|
239
|
+
console.print("[bold]Summary:[/bold]")
|
|
240
|
+
failed = 0
|
|
241
|
+
for src_name, ok, msg in results:
|
|
242
|
+
status = "[green]✓[/green]" if ok else "[red]✗[/red]"
|
|
243
|
+
console.print(f" {status} {src_name}: {msg}")
|
|
244
|
+
if not ok:
|
|
245
|
+
failed += 1
|
|
246
|
+
if failed:
|
|
247
|
+
raise typer.Exit(1)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
# --- --source: single named source ------------------------------------
|
|
251
|
+
if source is not None:
|
|
252
|
+
if source not in cli_config.sources:
|
|
253
|
+
available = ", ".join(cli_config.sources) or "(none)"
|
|
254
|
+
console.print(
|
|
255
|
+
f"[red]Unknown source {source!r}.[/red] "
|
|
256
|
+
f"Available: {available}"
|
|
257
|
+
)
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
src_cfg = cli_config.sources[source]
|
|
260
|
+
try:
|
|
261
|
+
config = connection_config_from_source(source, src_cfg)
|
|
262
|
+
except ValueError as e:
|
|
263
|
+
console.print(f"[red]Invalid source config:[/red] {e}")
|
|
264
|
+
raise typer.Exit(1) from e
|
|
265
|
+
try:
|
|
266
|
+
dsn_str = dsn_from_source(source, src_cfg)
|
|
267
|
+
except ValueError as e:
|
|
268
|
+
console.print(f"[red]Cannot build DSN from source:[/red] {e}")
|
|
269
|
+
raise typer.Exit(1) from e
|
|
270
|
+
connection_name = source
|
|
271
|
+
_run_index(
|
|
272
|
+
console=console,
|
|
273
|
+
cli_config=cli_config,
|
|
274
|
+
dsn=dsn_str,
|
|
275
|
+
connection_name=connection_name,
|
|
276
|
+
config=config,
|
|
277
|
+
state_dir=state_dir,
|
|
278
|
+
unified=unified,
|
|
279
|
+
skills_target=skills_target,
|
|
280
|
+
visibility_file=visibility_file,
|
|
281
|
+
profile=profile,
|
|
282
|
+
dbt_manifest=dbt_manifest,
|
|
283
|
+
dbt_sources=dbt_sources,
|
|
284
|
+
)
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
# --- DSN argument (original path) -------------------------------------
|
|
288
|
+
assert dsn is not None
|
|
289
|
+
name = resolve_optional_str_option(
|
|
290
|
+
ctx,
|
|
291
|
+
param_name="name",
|
|
292
|
+
cli_value=name,
|
|
293
|
+
config_value=cli_config.connection_defaults.name,
|
|
294
|
+
)
|
|
295
|
+
dialect = resolve_optional_str_option(
|
|
296
|
+
ctx,
|
|
297
|
+
param_name="dialect",
|
|
298
|
+
cli_value=dialect,
|
|
299
|
+
config_value=cli_config.connection_defaults.dialect,
|
|
300
|
+
)
|
|
301
|
+
try:
|
|
302
|
+
connection_name = name or default_connection_name(dsn)
|
|
303
|
+
except ValueError as e:
|
|
304
|
+
console.print(f"[red]Invalid DSN:[/red] {e}")
|
|
305
|
+
raise typer.Exit(1) from e
|
|
306
|
+
try:
|
|
307
|
+
config = connection_config_from_url(
|
|
308
|
+
dsn, connection_name, dialect_override=dialect
|
|
309
|
+
)
|
|
310
|
+
except ValueError as e:
|
|
311
|
+
console.print(f"[red]Invalid DSN:[/red] {e}")
|
|
312
|
+
raise typer.Exit(1) from e
|
|
313
|
+
_run_index(
|
|
314
|
+
console=console,
|
|
315
|
+
cli_config=cli_config,
|
|
316
|
+
dsn=dsn,
|
|
317
|
+
connection_name=connection_name,
|
|
318
|
+
config=config,
|
|
319
|
+
state_dir=state_dir,
|
|
320
|
+
unified=unified,
|
|
321
|
+
skills_target=skills_target,
|
|
322
|
+
visibility_file=visibility_file,
|
|
323
|
+
profile=profile,
|
|
324
|
+
dbt_manifest=dbt_manifest,
|
|
325
|
+
dbt_sources=dbt_sources,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _run_index(
|
|
330
|
+
*,
|
|
331
|
+
console: Console,
|
|
332
|
+
cli_config: object,
|
|
333
|
+
dsn: str,
|
|
334
|
+
connection_name: str,
|
|
335
|
+
config: object,
|
|
336
|
+
state_dir: Path,
|
|
337
|
+
unified: bool,
|
|
338
|
+
skills_target: str,
|
|
339
|
+
visibility_file: Path | None,
|
|
340
|
+
profile: str | None,
|
|
341
|
+
dbt_manifest: Path | None,
|
|
342
|
+
dbt_sources: Path | None,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Core indexing logic shared by DSN, --source, and --all paths."""
|
|
345
|
+
from pretensor.introspection.models.config import ConnectionConfig as _CC
|
|
346
|
+
|
|
347
|
+
assert isinstance(config, _CC)
|
|
348
|
+
|
|
349
|
+
total_started = time.perf_counter()
|
|
350
|
+
sd = Path(state_dir)
|
|
351
|
+
vis_path = (
|
|
352
|
+
Path(visibility_file)
|
|
353
|
+
if visibility_file is not None
|
|
354
|
+
else sd / "visibility.yml"
|
|
355
|
+
)
|
|
356
|
+
try:
|
|
357
|
+
vis_cfg = merge_profile_into_base(load_visibility_config(vis_path), profile)
|
|
358
|
+
except ValueError as e:
|
|
359
|
+
console.print(f"[red]{e}[/red]")
|
|
360
|
+
raise typer.Exit(1) from e
|
|
361
|
+
visibility_filter = (
|
|
362
|
+
VisibilityFilter.from_config(vis_cfg)
|
|
363
|
+
if vis_cfg.hidden_schemas
|
|
364
|
+
or vis_cfg.hidden_tables
|
|
365
|
+
or vis_cfg.hidden_columns
|
|
366
|
+
or vis_cfg.allowed_schemas
|
|
367
|
+
or vis_cfg.allowed_tables
|
|
368
|
+
else None
|
|
369
|
+
)
|
|
370
|
+
preloaded_manifest = None
|
|
371
|
+
preloaded_sources: Path | None = None
|
|
372
|
+
if dbt_manifest is not None:
|
|
373
|
+
preloaded_manifest, preloaded_sources = preload_dbt_manifest(
|
|
374
|
+
dbt_manifest, dbt_sources, console=console
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
sd.mkdir(parents=True, exist_ok=True)
|
|
379
|
+
except PermissionError as e:
|
|
380
|
+
console.print(
|
|
381
|
+
f"[red]Cannot create state directory {sd}:[/red] {e}\n"
|
|
382
|
+
"Check write permissions or use --state-dir to specify a writable path."
|
|
383
|
+
)
|
|
384
|
+
raise typer.Exit(1) from e
|
|
385
|
+
|
|
386
|
+
console.print(f"[bold blue]Introspecting[/bold blue] '{connection_name}'...")
|
|
387
|
+
_t_inspect = time.perf_counter() if _PROFILE_INDEX else 0.0
|
|
388
|
+
try:
|
|
389
|
+
with log_timed_operation(
|
|
390
|
+
logger,
|
|
391
|
+
event="index.inspect",
|
|
392
|
+
connection_name=connection_name,
|
|
393
|
+
dialect=str(config.type),
|
|
394
|
+
):
|
|
395
|
+
snapshot = inspect(config)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
console.print(
|
|
398
|
+
f"[red]Cannot connect to database:[/red] {e}\n"
|
|
399
|
+
"Check that the DSN is correct and the database is reachable."
|
|
400
|
+
)
|
|
401
|
+
raise typer.Exit(1) from e
|
|
402
|
+
if _PROFILE_INDEX:
|
|
403
|
+
console.print(
|
|
404
|
+
f"[dim][profile] inspect: {time.perf_counter() - _t_inspect:.2f}s[/dim]"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
graph_path = (
|
|
408
|
+
unified_graph_path(sd)
|
|
409
|
+
if unified
|
|
410
|
+
else graph_file_for_connection(sd, connection_name)
|
|
411
|
+
)
|
|
412
|
+
graph_path.parent.mkdir(parents=True, exist_ok=True)
|
|
413
|
+
|
|
414
|
+
if unified:
|
|
415
|
+
if graph_path.exists():
|
|
416
|
+
try:
|
|
417
|
+
store = KuzuStore(graph_path)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
console.print(
|
|
420
|
+
f"[red]Cannot open graph file {graph_path}:[/red] {e}\n"
|
|
421
|
+
"The file may be corrupt. Delete it and re-run `pretensor index`."
|
|
422
|
+
)
|
|
423
|
+
raise typer.Exit(1) from e
|
|
424
|
+
try:
|
|
425
|
+
store.ensure_schema()
|
|
426
|
+
store.clear_connection_subgraph(connection_name)
|
|
427
|
+
finally:
|
|
428
|
+
store.close()
|
|
429
|
+
else:
|
|
430
|
+
if graph_path.exists():
|
|
431
|
+
try:
|
|
432
|
+
graph_path.unlink()
|
|
433
|
+
except PermissionError as e:
|
|
434
|
+
console.print(
|
|
435
|
+
f"[red]Cannot remove existing graph file {graph_path}:[/red] {e}"
|
|
436
|
+
)
|
|
437
|
+
raise typer.Exit(1) from e
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
store = KuzuStore(graph_path)
|
|
441
|
+
except Exception as e:
|
|
442
|
+
console.print(
|
|
443
|
+
f"[red]Cannot create graph file {graph_path}:[/red] {e}\n"
|
|
444
|
+
"Check disk space and permissions."
|
|
445
|
+
)
|
|
446
|
+
raise typer.Exit(1) from e
|
|
447
|
+
try:
|
|
448
|
+
_t_build = time.perf_counter() if _PROFILE_INDEX else 0.0
|
|
449
|
+
with log_timed_operation(
|
|
450
|
+
logger,
|
|
451
|
+
event="index.graph_build",
|
|
452
|
+
connection_name=connection_name,
|
|
453
|
+
replace_mode="connection" if unified else "full",
|
|
454
|
+
):
|
|
455
|
+
GraphBuilder().build(
|
|
456
|
+
snapshot,
|
|
457
|
+
store,
|
|
458
|
+
replace_mode="connection" if unified else "full",
|
|
459
|
+
visibility_filter=visibility_filter,
|
|
460
|
+
)
|
|
461
|
+
if _PROFILE_INDEX:
|
|
462
|
+
console.print(
|
|
463
|
+
f"[dim][profile] build (incl. intelligence layer): {time.perf_counter() - _t_build:.2f}s[/dim]"
|
|
464
|
+
)
|
|
465
|
+
if preloaded_manifest is not None:
|
|
466
|
+
apply_dbt_enrichment_cli(
|
|
467
|
+
manifest=preloaded_manifest,
|
|
468
|
+
sources_path=preloaded_sources,
|
|
469
|
+
store=store,
|
|
470
|
+
connection_name=connection_name,
|
|
471
|
+
console=console,
|
|
472
|
+
)
|
|
473
|
+
tc_rows = store.query_all_rows(
|
|
474
|
+
"""
|
|
475
|
+
MATCH (t:SchemaTable {connection_name: $cn})
|
|
476
|
+
RETURN count(*)
|
|
477
|
+
""",
|
|
478
|
+
{"cn": connection_name},
|
|
479
|
+
)
|
|
480
|
+
table_count = (
|
|
481
|
+
int(tc_rows[0][0]) if tc_rows and tc_rows[0][0] is not None else 0
|
|
482
|
+
)
|
|
483
|
+
finally:
|
|
484
|
+
store.close()
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
reg = GraphRegistry(sd / cli_constants.REGISTRY_FILENAME).load()
|
|
488
|
+
except Exception as e:
|
|
489
|
+
console.print(
|
|
490
|
+
f"[red]Cannot read registry file:[/red] {e}\n"
|
|
491
|
+
"The registry.json may be corrupt. Delete it and re-run `pretensor index`."
|
|
492
|
+
)
|
|
493
|
+
raise typer.Exit(1) from e
|
|
494
|
+
indexed_at = datetime.now(timezone.utc)
|
|
495
|
+
enc: DSNEncryptor | None = None
|
|
496
|
+
encrypt = keystore_path(sd).exists()
|
|
497
|
+
if encrypt:
|
|
498
|
+
enc = DSNEncryptor(keystore_path(sd))
|
|
499
|
+
reg.upsert(
|
|
500
|
+
connection_name=connection_name,
|
|
501
|
+
database=snapshot.database,
|
|
502
|
+
dsn=dsn,
|
|
503
|
+
graph_path=graph_path,
|
|
504
|
+
unified_graph_path=graph_path if unified else None,
|
|
505
|
+
indexed_at=indexed_at,
|
|
506
|
+
encrypt_dsn=encrypt,
|
|
507
|
+
encryptor=enc,
|
|
508
|
+
dialect=registry_dialect_for(config.type),
|
|
509
|
+
table_count=table_count,
|
|
510
|
+
dbt_manifest_path=str(dbt_manifest) if dbt_manifest is not None else None,
|
|
511
|
+
)
|
|
512
|
+
try:
|
|
513
|
+
reg.save()
|
|
514
|
+
except PermissionError as e:
|
|
515
|
+
console.print(
|
|
516
|
+
f"[red]Cannot write registry file:[/red] {e}\n"
|
|
517
|
+
"Check write permissions on the state directory."
|
|
518
|
+
)
|
|
519
|
+
raise typer.Exit(1) from e
|
|
520
|
+
|
|
521
|
+
snap_store = SnapshotStore(sd)
|
|
522
|
+
snap_store.save(connection_name, snapshot)
|
|
523
|
+
|
|
524
|
+
entry = reg.get(connection_name)
|
|
525
|
+
if entry is not None:
|
|
526
|
+
skill_store = KuzuStore(graph_path)
|
|
527
|
+
try:
|
|
528
|
+
skill_paths = SkillGenerator.write_for_index(
|
|
529
|
+
store=skill_store,
|
|
530
|
+
entry=entry,
|
|
531
|
+
skills_target=skills_target,
|
|
532
|
+
)
|
|
533
|
+
finally:
|
|
534
|
+
skill_store.close()
|
|
535
|
+
for p in skill_paths:
|
|
536
|
+
console.print(f"[green]Skill written:[/green] {p}")
|
|
537
|
+
|
|
538
|
+
console.print(f"[green]Graph written:[/green] {graph_path}")
|
|
539
|
+
console.print(f"[green]Registry updated:[/green] {reg.path}")
|
|
540
|
+
logger.info(
|
|
541
|
+
"index completed in %.2fms",
|
|
542
|
+
(time.perf_counter() - total_started) * 1000,
|
|
543
|
+
extra={
|
|
544
|
+
"event": "index.total",
|
|
545
|
+
"status": "ok",
|
|
546
|
+
"duration_ms": (time.perf_counter() - total_started) * 1000,
|
|
547
|
+
"connection_name": connection_name,
|
|
548
|
+
"unified": unified,
|
|
549
|
+
"table_count": table_count,
|
|
550
|
+
},
|
|
551
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""``pretensor list`` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from pretensor.cli import constants as cli_constants
|
|
11
|
+
from pretensor.cli.config_file import get_cli_config, resolve_path_option
|
|
12
|
+
from pretensor.core.registry import GraphRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_list_command(app: typer.Typer, *, console: Console) -> None:
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_command(
|
|
18
|
+
state_dir: Path = typer.Option(
|
|
19
|
+
cli_constants.DEFAULT_STATE_DIR,
|
|
20
|
+
"--state-dir",
|
|
21
|
+
help="Directory containing registry.json.",
|
|
22
|
+
file_okay=False,
|
|
23
|
+
dir_okay=True,
|
|
24
|
+
resolve_path=True,
|
|
25
|
+
),
|
|
26
|
+
ctx: typer.Context = typer.Option(
|
|
27
|
+
None,
|
|
28
|
+
hidden=True,
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""List indexed connections from the registry."""
|
|
32
|
+
cli_config = get_cli_config(ctx)
|
|
33
|
+
state_dir = resolve_path_option(
|
|
34
|
+
ctx,
|
|
35
|
+
param_name="state_dir",
|
|
36
|
+
cli_value=state_dir,
|
|
37
|
+
config_value=cli_config.state_dir,
|
|
38
|
+
)
|
|
39
|
+
path = state_dir / cli_constants.REGISTRY_FILENAME
|
|
40
|
+
if not path.exists():
|
|
41
|
+
console.print("[dim]No registry found.[/dim]")
|
|
42
|
+
raise typer.Exit(0)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
reg = GraphRegistry(path).load()
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(
|
|
48
|
+
f"[red]Cannot read registry file {path}:[/red] {e}\n"
|
|
49
|
+
"The registry.json may be corrupt. Delete it and re-run `pretensor index`."
|
|
50
|
+
)
|
|
51
|
+
raise typer.Exit(1) from e
|
|
52
|
+
entries = reg.list_entries()
|
|
53
|
+
if not entries:
|
|
54
|
+
console.print("[dim]Registry is empty.[/dim]")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
for entry in entries:
|
|
58
|
+
ts = entry.last_indexed_at.isoformat()
|
|
59
|
+
tc = entry.table_count
|
|
60
|
+
tc_s = f" tables={tc}" if tc is not None else ""
|
|
61
|
+
console.print(
|
|
62
|
+
f"[bold]{entry.connection_name}[/bold] "
|
|
63
|
+
f"db={entry.database} indexed={ts}{tc_s}\n"
|
|
64
|
+
f" graph: {entry.unified_graph_path or entry.graph_path}"
|
|
65
|
+
)
|