pretensor 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.
- pretensor/__init__.py +50 -0
- pretensor/benchmark/__init__.py +54 -0
- pretensor/benchmark/cli.py +294 -0
- pretensor/benchmark/fixtures.py +84 -0
- pretensor/benchmark/l1/__init__.py +23 -0
- pretensor/benchmark/l1/metrics.py +141 -0
- pretensor/benchmark/l1/pipeline.py +188 -0
- pretensor/benchmark/l1/runner.py +245 -0
- pretensor/benchmark/l2/__init__.py +27 -0
- pretensor/benchmark/l2/gold.py +236 -0
- pretensor/benchmark/l2/metrics.py +146 -0
- pretensor/benchmark/l2/pipeline.py +124 -0
- pretensor/benchmark/l2/runner.py +530 -0
- pretensor/benchmark/l3/__init__.py +73 -0
- pretensor/benchmark/l3/agent.py +316 -0
- pretensor/benchmark/l3/db.py +188 -0
- pretensor/benchmark/l3/gold.py +85 -0
- pretensor/benchmark/l3/llm_client.py +395 -0
- pretensor/benchmark/l3/mcp_client.py +357 -0
- pretensor/benchmark/l3/pretensor_runner.py +456 -0
- pretensor/benchmark/l3/prompt.py +132 -0
- pretensor/benchmark/l3/runner.py +358 -0
- pretensor/benchmark/l3/sql_equivalence.py +176 -0
- pretensor/benchmark/release_gate.py +448 -0
- pretensor/benchmark/results.py +298 -0
- pretensor/benchmark/runner.py +109 -0
- pretensor/cli/__init__.py +1 -0
- pretensor/cli/commands/_source_runner.py +147 -0
- pretensor/cli/commands/analyze.py +201 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +126 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +131 -0
- pretensor/cli/commands/index.py +559 -0
- pretensor/cli/commands/list.py +76 -0
- pretensor/cli/commands/quickstart.py +207 -0
- pretensor/cli/commands/reindex.py +646 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +144 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +176 -0
- pretensor/cli/config_file.py +442 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +109 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +226 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +468 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +97 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/mysql.py +402 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +938 -0
- pretensor/connectors/registry.py +93 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +908 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +307 -0
- pretensor/core/dsn_crypto.py +51 -0
- pretensor/core/graph_schema_manager.py +246 -0
- pretensor/core/graph_store.py +1226 -0
- pretensor/core/ids.py +101 -0
- pretensor/core/portable_export.py +276 -0
- pretensor/core/query_runner.py +67 -0
- pretensor/core/registry.py +209 -0
- pretensor/core/schema.py +473 -0
- pretensor/core/secure_io.py +93 -0
- pretensor/core/store.py +469 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/analyze/__init__.py +0 -0
- pretensor/enrichment/analyze/classify.py +49 -0
- pretensor/enrichment/analyze/extract_python.py +196 -0
- pretensor/enrichment/analyze/parse.py +141 -0
- pretensor/enrichment/analyze/pipeline.py +195 -0
- pretensor/enrichment/analyze/summary.py +38 -0
- pretensor/enrichment/analyze/walker.py +98 -0
- pretensor/enrichment/analyze/writers.py +214 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +300 -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 +383 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/errors.py +35 -0
- pretensor/graph_models/__init__.py +17 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/consumer.py +71 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +79 -0
- pretensor/graph_models/relationship.py +33 -0
- pretensor/integrations/__init__.py +42 -0
- pretensor/integrations/_base.py +138 -0
- pretensor/integrations/google_adk.py +49 -0
- pretensor/integrations/langchain.py +55 -0
- pretensor/integrations/llamaindex.py +53 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +425 -0
- pretensor/intelligence/clustering.py +168 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +317 -0
- pretensor/intelligence/graph_export.py +200 -0
- pretensor/intelligence/heuristic.py +544 -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 +62 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +364 -0
- pretensor/intelligence/role_exemplars.py +263 -0
- pretensor/intelligence/schema_classification.py +360 -0
- pretensor/intelligence/scoring.py +76 -0
- pretensor/intelligence/semantic.py +240 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +191 -0
- pretensor/intelligence/steps_embedding.py +168 -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 +237 -0
- pretensor/introspection/models/dsn.py +550 -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 +274 -0
- pretensor/mcp/resources/__init__.py +17 -0
- pretensor/mcp/resources/markdown.py +314 -0
- pretensor/mcp/server.py +285 -0
- pretensor/mcp/service.py +49 -0
- pretensor/mcp/service_context.py +142 -0
- pretensor/mcp/service_registry.py +294 -0
- pretensor/mcp/store_cache.py +43 -0
- pretensor/mcp/tool_registry.py +136 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/_rank.py +244 -0
- pretensor/mcp/tools/_timed.py +26 -0
- pretensor/mcp/tools/compile_metric.py +144 -0
- pretensor/mcp/tools/consumers.py +161 -0
- pretensor/mcp/tools/context.py +1121 -0
- pretensor/mcp/tools/cypher.py +509 -0
- pretensor/mcp/tools/detect_changes.py +254 -0
- pretensor/mcp/tools/impact.py +271 -0
- pretensor/mcp/tools/list.py +131 -0
- pretensor/mcp/tools/schema.py +170 -0
- pretensor/mcp/tools/search.py +316 -0
- pretensor/mcp/tools/semantic_search.py +282 -0
- pretensor/mcp/tools/traverse.py +1027 -0
- pretensor/mcp/tools/validate_sql.py +150 -0
- pretensor/observability.py +203 -0
- pretensor/py.typed +0 -0
- pretensor/quickstart/README.md +29 -0
- pretensor/quickstart/__init__.py +6 -0
- pretensor/quickstart/docker-compose.yml +18 -0
- pretensor/quickstart/pagila_data.sql +63 -0
- pretensor/quickstart/pagila_ddl.sql +92 -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 +487 -0
- pretensor/semantic/yaml_layer.py +180 -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.0.dist-info/METADATA +251 -0
- pretensor-0.1.0.dist-info/RECORD +198 -0
- pretensor-0.1.0.dist-info/WHEEL +4 -0
- pretensor-0.1.0.dist-info/entry_points.txt +2 -0
- pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""``pretensor analyze`` — scan a repo for SQL and link code to graph tables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from pretensor.cli import constants as cli_constants
|
|
14
|
+
from pretensor.cli.config_file import get_cli_config, resolve_aliased_path_option
|
|
15
|
+
from pretensor.enrichment.analyze.pipeline import (
|
|
16
|
+
EmptyGraphError,
|
|
17
|
+
run_analyze_enrichment,
|
|
18
|
+
)
|
|
19
|
+
from pretensor.enrichment.analyze.summary import AnalyzeSummary
|
|
20
|
+
from pretensor.mcp.service_registry import (
|
|
21
|
+
load_registry,
|
|
22
|
+
open_store_for_entry,
|
|
23
|
+
release_store,
|
|
24
|
+
resolve_registry_entry,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = ["register_analyze_command"]
|
|
28
|
+
|
|
29
|
+
_EXIT_ERROR = 1
|
|
30
|
+
_MAX_DISPLAY_ROWS = 20
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_analyze_command(app: typer.Typer, *, console: Console) -> None:
|
|
34
|
+
"""Register ``pretensor analyze`` onto ``app``."""
|
|
35
|
+
|
|
36
|
+
@app.command("analyze")
|
|
37
|
+
def analyze_command(
|
|
38
|
+
path: Path = typer.Argument(
|
|
39
|
+
Path("."),
|
|
40
|
+
help="Repository root to scan (default: current directory).",
|
|
41
|
+
file_okay=False,
|
|
42
|
+
dir_okay=True,
|
|
43
|
+
resolve_path=True,
|
|
44
|
+
),
|
|
45
|
+
connection: str = typer.Option(
|
|
46
|
+
...,
|
|
47
|
+
"--connection",
|
|
48
|
+
help="Required. Connection whose tables SQL refs resolve against.",
|
|
49
|
+
),
|
|
50
|
+
service: str | None = typer.Option(
|
|
51
|
+
None,
|
|
52
|
+
"--service",
|
|
53
|
+
help="Consumer-service label (default: scanned directory basename).",
|
|
54
|
+
),
|
|
55
|
+
include: list[str] = typer.Option(
|
|
56
|
+
[],
|
|
57
|
+
"--include",
|
|
58
|
+
help="Gitignore-style glob to include (repeatable).",
|
|
59
|
+
),
|
|
60
|
+
exclude: list[str] = typer.Option(
|
|
61
|
+
[],
|
|
62
|
+
"--exclude",
|
|
63
|
+
help="Gitignore-style glob to exclude (repeatable).",
|
|
64
|
+
),
|
|
65
|
+
max_file_bytes: int = typer.Option(
|
|
66
|
+
1_000_000,
|
|
67
|
+
"--max-file-bytes",
|
|
68
|
+
help="Skip files larger than this many bytes.",
|
|
69
|
+
),
|
|
70
|
+
min_confidence: float = typer.Option(
|
|
71
|
+
0.6,
|
|
72
|
+
"--min-confidence",
|
|
73
|
+
help="Minimum confidence to emit a CONSUMES edge.",
|
|
74
|
+
),
|
|
75
|
+
default_schema: str = typer.Option(
|
|
76
|
+
"public",
|
|
77
|
+
"--default-schema",
|
|
78
|
+
help="Schema assumed for unqualified table names in scanned SQL.",
|
|
79
|
+
),
|
|
80
|
+
dry_run: bool = typer.Option(
|
|
81
|
+
False,
|
|
82
|
+
"--dry-run",
|
|
83
|
+
help="Parse and print the plan only; make no graph writes.",
|
|
84
|
+
),
|
|
85
|
+
as_json: bool = typer.Option(
|
|
86
|
+
False,
|
|
87
|
+
"--json",
|
|
88
|
+
help="Emit the machine-readable summary as JSON (suppresses the table).",
|
|
89
|
+
),
|
|
90
|
+
state_dir: Path = typer.Option(
|
|
91
|
+
cli_constants.DEFAULT_STATE_DIR,
|
|
92
|
+
"--state-dir",
|
|
93
|
+
help="Directory containing registry.json.",
|
|
94
|
+
file_okay=False,
|
|
95
|
+
dir_okay=True,
|
|
96
|
+
resolve_path=True,
|
|
97
|
+
),
|
|
98
|
+
graph_dir: Path | None = typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
"--graph-dir",
|
|
101
|
+
hidden=True,
|
|
102
|
+
help="Deprecated alias for --state-dir.",
|
|
103
|
+
file_okay=False,
|
|
104
|
+
dir_okay=True,
|
|
105
|
+
resolve_path=True,
|
|
106
|
+
),
|
|
107
|
+
ctx: typer.Context = typer.Option(None, hidden=True),
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Scan a repository for SQL string literals and link them to graph tables."""
|
|
110
|
+
cli_config = get_cli_config(ctx)
|
|
111
|
+
state_dir = resolve_aliased_path_option(
|
|
112
|
+
ctx,
|
|
113
|
+
primary_param="state_dir",
|
|
114
|
+
primary_value=state_dir,
|
|
115
|
+
alias_param="graph_dir",
|
|
116
|
+
alias_value=graph_dir,
|
|
117
|
+
config_value=cli_config.state_dir,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
service_name = service or path.name
|
|
121
|
+
|
|
122
|
+
reg = load_registry(state_dir)
|
|
123
|
+
entry = resolve_registry_entry(reg, connection)
|
|
124
|
+
if entry is None:
|
|
125
|
+
_empty_graph_error(console, connection)
|
|
126
|
+
raise typer.Exit(_EXIT_ERROR)
|
|
127
|
+
|
|
128
|
+
store = open_store_for_entry(entry)
|
|
129
|
+
try:
|
|
130
|
+
summary = run_analyze_enrichment(
|
|
131
|
+
path,
|
|
132
|
+
store,
|
|
133
|
+
entry.connection_name,
|
|
134
|
+
service_name=service_name,
|
|
135
|
+
includes=include,
|
|
136
|
+
excludes=exclude,
|
|
137
|
+
max_file_bytes=max_file_bytes,
|
|
138
|
+
min_confidence=min_confidence,
|
|
139
|
+
default_schema=default_schema,
|
|
140
|
+
dry_run=dry_run,
|
|
141
|
+
)
|
|
142
|
+
except EmptyGraphError:
|
|
143
|
+
_empty_graph_error(console, entry.connection_name)
|
|
144
|
+
raise typer.Exit(_EXIT_ERROR) from None
|
|
145
|
+
finally:
|
|
146
|
+
release_store(store)
|
|
147
|
+
|
|
148
|
+
if as_json:
|
|
149
|
+
typer.echo(json.dumps(dataclasses.asdict(summary), indent=2))
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
_render_summary(console, summary, service_name=service_name, dry_run=dry_run)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _empty_graph_error(console: Console, connection: str) -> None:
|
|
156
|
+
"""Print the hard-error hint; no auto-index is performed (by design)."""
|
|
157
|
+
console.print(
|
|
158
|
+
f"[red]The graph for connection '{connection}' is empty.[/red] Run:\n"
|
|
159
|
+
f" pretensor index --connection {connection}\n"
|
|
160
|
+
"first, then re-run pretensor analyze."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _render_summary(
|
|
165
|
+
console: Console,
|
|
166
|
+
summary: AnalyzeSummary,
|
|
167
|
+
*,
|
|
168
|
+
service_name: str,
|
|
169
|
+
dry_run: bool,
|
|
170
|
+
) -> None:
|
|
171
|
+
rows = sorted(summary.rows, key=lambda r: r.confidence, reverse=True)
|
|
172
|
+
shown = rows[:_MAX_DISPLAY_ROWS]
|
|
173
|
+
|
|
174
|
+
title = (
|
|
175
|
+
"pretensor analyze (dry run — no writes)" if dry_run else "pretensor analyze"
|
|
176
|
+
)
|
|
177
|
+
table = Table(title=title)
|
|
178
|
+
table.add_column("service")
|
|
179
|
+
table.add_column("table")
|
|
180
|
+
table.add_column("op")
|
|
181
|
+
table.add_column("file:line")
|
|
182
|
+
table.add_column("confidence", justify="right")
|
|
183
|
+
for r in shown:
|
|
184
|
+
line = (
|
|
185
|
+
f"{r.file_path}:{r.line_start}"
|
|
186
|
+
if r.line_start == r.line_end
|
|
187
|
+
else f"{r.file_path}:{r.line_start}-{r.line_end}"
|
|
188
|
+
)
|
|
189
|
+
table.add_row(r.service_name, r.table, r.op, line, f"{r.confidence:.2f}")
|
|
190
|
+
console.print(table)
|
|
191
|
+
|
|
192
|
+
if len(rows) > len(shown):
|
|
193
|
+
console.print(f"[dim]… showing top {len(shown)} of {len(rows)} edges[/dim]")
|
|
194
|
+
|
|
195
|
+
verb = "would write" if dry_run else "wrote"
|
|
196
|
+
console.print(
|
|
197
|
+
f"Scanned {summary.files_scanned} files, "
|
|
198
|
+
f"found {summary.sql_candidates_found} SQL candidates, "
|
|
199
|
+
f"{verb} {summary.consumers_written} consumers / {summary.edges_written} edges "
|
|
200
|
+
f"({summary.cross_connection_dropped} dropped: cross-connection)."
|
|
201
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""``add`` and ``remove`` cross-DB registry commands."""
|
|
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 (
|
|
12
|
+
get_cli_config,
|
|
13
|
+
resolve_optional_str_option,
|
|
14
|
+
resolve_path_option,
|
|
15
|
+
)
|
|
16
|
+
from pretensor.cli.paths import default_connection_name, unified_graph_path
|
|
17
|
+
from pretensor.core.dsn_crypto import DSNEncryptor
|
|
18
|
+
from pretensor.core.registry import GraphRegistry
|
|
19
|
+
from pretensor.introspection.models.dsn import (
|
|
20
|
+
connection_config_from_url,
|
|
21
|
+
redact_dsn,
|
|
22
|
+
registry_dialect_for,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _encryptor_for(state_dir: Path) -> DSNEncryptor:
|
|
27
|
+
return DSNEncryptor(state_dir / "keystore")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_add_remove_commands(app: typer.Typer) -> None:
|
|
31
|
+
@app.command("add")
|
|
32
|
+
def add_command(
|
|
33
|
+
dsn: str = typer.Argument(
|
|
34
|
+
..., help="Database DSN URL (postgresql:// or snowflake://)."
|
|
35
|
+
),
|
|
36
|
+
name: str | None = typer.Option(
|
|
37
|
+
None,
|
|
38
|
+
"--name",
|
|
39
|
+
"-n",
|
|
40
|
+
help="Logical connection name (default: DB name from DSN).",
|
|
41
|
+
),
|
|
42
|
+
state_dir: Path = typer.Option(
|
|
43
|
+
cli_constants.DEFAULT_STATE_DIR,
|
|
44
|
+
"--state-dir",
|
|
45
|
+
help="State directory for registry and graphs.",
|
|
46
|
+
file_okay=False,
|
|
47
|
+
dir_okay=True,
|
|
48
|
+
writable=True,
|
|
49
|
+
resolve_path=True,
|
|
50
|
+
),
|
|
51
|
+
ctx: typer.Context = typer.Option(None, hidden=True),
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Register a DSN (encrypted); run `pretensor index` to build the graph."""
|
|
54
|
+
cli_config = get_cli_config(ctx)
|
|
55
|
+
state_dir = resolve_path_option(
|
|
56
|
+
ctx,
|
|
57
|
+
param_name="state_dir",
|
|
58
|
+
cli_value=state_dir,
|
|
59
|
+
config_value=cli_config.state_dir,
|
|
60
|
+
)
|
|
61
|
+
name = resolve_optional_str_option(
|
|
62
|
+
ctx,
|
|
63
|
+
param_name="name",
|
|
64
|
+
cli_value=name,
|
|
65
|
+
config_value=cli_config.connection_defaults.name,
|
|
66
|
+
)
|
|
67
|
+
console = Console()
|
|
68
|
+
connection_name = name or default_connection_name(dsn)
|
|
69
|
+
enc = _encryptor_for(state_dir)
|
|
70
|
+
reg_path = state_dir / cli_constants.REGISTRY_FILENAME
|
|
71
|
+
reg = GraphRegistry(reg_path).load()
|
|
72
|
+
graph_path = unified_graph_path(state_dir)
|
|
73
|
+
cfg = connection_config_from_url(dsn, connection_name)
|
|
74
|
+
reg.upsert(
|
|
75
|
+
connection_name=connection_name,
|
|
76
|
+
database=cfg.database or connection_name,
|
|
77
|
+
dsn=dsn,
|
|
78
|
+
graph_path=graph_path,
|
|
79
|
+
unified_graph_path=graph_path,
|
|
80
|
+
encrypt_dsn=True,
|
|
81
|
+
encryptor=enc,
|
|
82
|
+
indexed_at=None,
|
|
83
|
+
dialect=registry_dialect_for(cfg.type),
|
|
84
|
+
)
|
|
85
|
+
reg.save()
|
|
86
|
+
console.print(
|
|
87
|
+
f"[green]Registered[/green] connection {connection_name!r} (DSN encrypted)."
|
|
88
|
+
)
|
|
89
|
+
# Mask the password in the hint so the DSN does not land in terminal
|
|
90
|
+
# scrollback, screen-shares or CI logs.
|
|
91
|
+
console.print(
|
|
92
|
+
f"Run: pretensor index '{redact_dsn(dsn)}' "
|
|
93
|
+
f"--name {connection_name} --unified"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@app.command("remove")
|
|
97
|
+
def remove_command(
|
|
98
|
+
name: str = typer.Argument(
|
|
99
|
+
..., help="Connection name to remove from registry."
|
|
100
|
+
),
|
|
101
|
+
state_dir: Path = typer.Option(
|
|
102
|
+
cli_constants.DEFAULT_STATE_DIR,
|
|
103
|
+
"--state-dir",
|
|
104
|
+
file_okay=False,
|
|
105
|
+
dir_okay=True,
|
|
106
|
+
resolve_path=True,
|
|
107
|
+
),
|
|
108
|
+
ctx: typer.Context = typer.Option(None, hidden=True),
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Remove a connection from the registry (does not delete the unified graph file)."""
|
|
111
|
+
cli_config = get_cli_config(ctx)
|
|
112
|
+
state_dir = resolve_path_option(
|
|
113
|
+
ctx,
|
|
114
|
+
param_name="state_dir",
|
|
115
|
+
cli_value=state_dir,
|
|
116
|
+
config_value=cli_config.state_dir,
|
|
117
|
+
)
|
|
118
|
+
console = Console()
|
|
119
|
+
reg_path = state_dir / cli_constants.REGISTRY_FILENAME
|
|
120
|
+
reg = GraphRegistry(reg_path).load()
|
|
121
|
+
if reg.get(name) is None:
|
|
122
|
+
console.print(f"[red]Unknown connection[/red] {name!r}")
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
reg.remove(name)
|
|
125
|
+
reg.save()
|
|
126
|
+
console.print(f"[green]Removed[/green] {name!r} from registry.")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Wire connection-management Typer commands onto the root app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from pretensor.cli.commands.connections.add_remove import register_add_remove_commands
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_connection_commands(app: typer.Typer) -> None:
|
|
11
|
+
"""Attach ``add`` and ``remove`` to ``app``."""
|
|
12
|
+
register_add_remove_commands(app)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""``pretensor export`` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from pretensor.cli import constants as cli_constants
|
|
12
|
+
from pretensor.cli.config_file import get_cli_config, resolve_aliased_path_option
|
|
13
|
+
from pretensor.core.portable_export import export_graph_payload
|
|
14
|
+
from pretensor.core.registry import DatabaseNotFoundError, GraphRegistry
|
|
15
|
+
from pretensor.core.store import KuzuStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register_export_command(app: typer.Typer, *, console: Console) -> None:
|
|
19
|
+
@app.command("export")
|
|
20
|
+
def export_command(
|
|
21
|
+
database: str = typer.Option(
|
|
22
|
+
...,
|
|
23
|
+
"--database",
|
|
24
|
+
"-d",
|
|
25
|
+
help="Registry connection name to export (see `pretensor list`).",
|
|
26
|
+
),
|
|
27
|
+
output: Path = typer.Option(
|
|
28
|
+
...,
|
|
29
|
+
"--output",
|
|
30
|
+
"-o",
|
|
31
|
+
help="Target JSON path for portable graph export.",
|
|
32
|
+
file_okay=True,
|
|
33
|
+
dir_okay=False,
|
|
34
|
+
writable=True,
|
|
35
|
+
resolve_path=True,
|
|
36
|
+
),
|
|
37
|
+
state_dir: Path = typer.Option(
|
|
38
|
+
cli_constants.DEFAULT_STATE_DIR,
|
|
39
|
+
"--state-dir",
|
|
40
|
+
help="Directory containing registry.json and graph files.",
|
|
41
|
+
file_okay=False,
|
|
42
|
+
dir_okay=True,
|
|
43
|
+
resolve_path=True,
|
|
44
|
+
),
|
|
45
|
+
graph_dir: Path | None = typer.Option(
|
|
46
|
+
None,
|
|
47
|
+
"--graph-dir",
|
|
48
|
+
hidden=True,
|
|
49
|
+
help="Deprecated alias for --state-dir.",
|
|
50
|
+
file_okay=False,
|
|
51
|
+
dir_okay=True,
|
|
52
|
+
resolve_path=True,
|
|
53
|
+
),
|
|
54
|
+
ctx: typer.Context = typer.Option(
|
|
55
|
+
None,
|
|
56
|
+
hidden=True,
|
|
57
|
+
),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Dump one indexed graph to portable pretty-printed JSON."""
|
|
60
|
+
cli_config = get_cli_config(ctx)
|
|
61
|
+
state_dir = resolve_aliased_path_option(
|
|
62
|
+
ctx,
|
|
63
|
+
primary_param="state_dir",
|
|
64
|
+
primary_value=state_dir,
|
|
65
|
+
alias_param="graph_dir",
|
|
66
|
+
alias_value=graph_dir,
|
|
67
|
+
config_value=cli_config.state_dir,
|
|
68
|
+
)
|
|
69
|
+
reg_path = state_dir / cli_constants.REGISTRY_FILENAME
|
|
70
|
+
if not reg_path.exists():
|
|
71
|
+
console.print("[red]No registry found.[/red] Run `pretensor index` first.")
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
try:
|
|
74
|
+
reg = GraphRegistry(reg_path).load()
|
|
75
|
+
entry = reg.require(database)
|
|
76
|
+
except DatabaseNotFoundError:
|
|
77
|
+
console.print(
|
|
78
|
+
f"[red]Unknown connection {database!r}.[/red] "
|
|
79
|
+
"Use `pretensor list` to discover available names."
|
|
80
|
+
)
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
console.print(
|
|
84
|
+
f"[red]Cannot read registry file {reg_path}:[/red] {exc}\n"
|
|
85
|
+
"The registry.json may be corrupt. Delete it and re-run `pretensor index`."
|
|
86
|
+
)
|
|
87
|
+
raise typer.Exit(1) from exc
|
|
88
|
+
graph_path = Path(entry.unified_graph_path or entry.graph_path)
|
|
89
|
+
if not graph_path.exists():
|
|
90
|
+
console.print(
|
|
91
|
+
f"[red]Graph file missing:[/red] {graph_path}\n"
|
|
92
|
+
"Re-run `pretensor index` before exporting."
|
|
93
|
+
)
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
try:
|
|
96
|
+
store = KuzuStore(graph_path)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
console.print(
|
|
99
|
+
f"[red]Cannot open graph file {graph_path}:[/red] {exc}\n"
|
|
100
|
+
"The file may be corrupt. Re-run `pretensor index`."
|
|
101
|
+
)
|
|
102
|
+
raise typer.Exit(1) from exc
|
|
103
|
+
try:
|
|
104
|
+
payload = export_graph_payload(
|
|
105
|
+
store,
|
|
106
|
+
connection_name=entry.connection_name,
|
|
107
|
+
database_name=entry.database,
|
|
108
|
+
graph_path=graph_path,
|
|
109
|
+
)
|
|
110
|
+
finally:
|
|
111
|
+
store.close()
|
|
112
|
+
try:
|
|
113
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
output.write_text(
|
|
115
|
+
json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
|
|
116
|
+
encoding="utf-8",
|
|
117
|
+
)
|
|
118
|
+
except PermissionError as exc:
|
|
119
|
+
console.print(f"[red]Cannot write export file {output}:[/red] {exc}")
|
|
120
|
+
raise typer.Exit(1) from exc
|
|
121
|
+
except OSError as exc:
|
|
122
|
+
console.print(f"[red]Cannot write export file {output}:[/red] {exc}")
|
|
123
|
+
raise typer.Exit(1) from exc
|
|
124
|
+
stats = payload["stats"]
|
|
125
|
+
console.print(f"[green]Graph exported:[/green] {output}")
|
|
126
|
+
console.print(
|
|
127
|
+
"[dim]"
|
|
128
|
+
f"node_types={stats['node_types']} edge_types={stats['edge_types']} "
|
|
129
|
+
f"nodes={stats['node_count']} edges={stats['edge_count']}"
|
|
130
|
+
"[/dim]"
|
|
131
|
+
)
|