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.
Files changed (198) hide show
  1. pretensor/__init__.py +50 -0
  2. pretensor/benchmark/__init__.py +54 -0
  3. pretensor/benchmark/cli.py +294 -0
  4. pretensor/benchmark/fixtures.py +84 -0
  5. pretensor/benchmark/l1/__init__.py +23 -0
  6. pretensor/benchmark/l1/metrics.py +141 -0
  7. pretensor/benchmark/l1/pipeline.py +188 -0
  8. pretensor/benchmark/l1/runner.py +245 -0
  9. pretensor/benchmark/l2/__init__.py +27 -0
  10. pretensor/benchmark/l2/gold.py +236 -0
  11. pretensor/benchmark/l2/metrics.py +146 -0
  12. pretensor/benchmark/l2/pipeline.py +124 -0
  13. pretensor/benchmark/l2/runner.py +530 -0
  14. pretensor/benchmark/l3/__init__.py +73 -0
  15. pretensor/benchmark/l3/agent.py +316 -0
  16. pretensor/benchmark/l3/db.py +188 -0
  17. pretensor/benchmark/l3/gold.py +85 -0
  18. pretensor/benchmark/l3/llm_client.py +395 -0
  19. pretensor/benchmark/l3/mcp_client.py +357 -0
  20. pretensor/benchmark/l3/pretensor_runner.py +456 -0
  21. pretensor/benchmark/l3/prompt.py +132 -0
  22. pretensor/benchmark/l3/runner.py +358 -0
  23. pretensor/benchmark/l3/sql_equivalence.py +176 -0
  24. pretensor/benchmark/release_gate.py +448 -0
  25. pretensor/benchmark/results.py +298 -0
  26. pretensor/benchmark/runner.py +109 -0
  27. pretensor/cli/__init__.py +1 -0
  28. pretensor/cli/commands/_source_runner.py +147 -0
  29. pretensor/cli/commands/analyze.py +201 -0
  30. pretensor/cli/commands/connections/__init__.py +7 -0
  31. pretensor/cli/commands/connections/add_remove.py +126 -0
  32. pretensor/cli/commands/connections/register.py +12 -0
  33. pretensor/cli/commands/export.py +131 -0
  34. pretensor/cli/commands/index.py +559 -0
  35. pretensor/cli/commands/list.py +76 -0
  36. pretensor/cli/commands/quickstart.py +207 -0
  37. pretensor/cli/commands/reindex.py +646 -0
  38. pretensor/cli/commands/semantic.py +190 -0
  39. pretensor/cli/commands/serve.py +144 -0
  40. pretensor/cli/commands/sync_grants.py +149 -0
  41. pretensor/cli/commands/validate.py +176 -0
  42. pretensor/cli/config_file.py +442 -0
  43. pretensor/cli/constants.py +10 -0
  44. pretensor/cli/dbt_enrichment.py +96 -0
  45. pretensor/cli/main.py +109 -0
  46. pretensor/cli/paths.py +43 -0
  47. pretensor/cli/plugin.py +52 -0
  48. pretensor/config.py +226 -0
  49. pretensor/connectors/__init__.py +29 -0
  50. pretensor/connectors/base.py +165 -0
  51. pretensor/connectors/bigquery.py +468 -0
  52. pretensor/connectors/inspect.py +321 -0
  53. pretensor/connectors/lineage_sqlglot.py +97 -0
  54. pretensor/connectors/models.py +130 -0
  55. pretensor/connectors/mysql.py +402 -0
  56. pretensor/connectors/pg_array_parse.py +53 -0
  57. pretensor/connectors/postgres.py +938 -0
  58. pretensor/connectors/registry.py +93 -0
  59. pretensor/connectors/snapshot.py +244 -0
  60. pretensor/connectors/snowflake.py +908 -0
  61. pretensor/core/__init__.py +1 -0
  62. pretensor/core/builder.py +307 -0
  63. pretensor/core/dsn_crypto.py +51 -0
  64. pretensor/core/graph_schema_manager.py +246 -0
  65. pretensor/core/graph_store.py +1226 -0
  66. pretensor/core/ids.py +101 -0
  67. pretensor/core/portable_export.py +276 -0
  68. pretensor/core/query_runner.py +67 -0
  69. pretensor/core/registry.py +209 -0
  70. pretensor/core/schema.py +473 -0
  71. pretensor/core/secure_io.py +93 -0
  72. pretensor/core/store.py +469 -0
  73. pretensor/enrichment/__init__.py +1 -0
  74. pretensor/enrichment/analyze/__init__.py +0 -0
  75. pretensor/enrichment/analyze/classify.py +49 -0
  76. pretensor/enrichment/analyze/extract_python.py +196 -0
  77. pretensor/enrichment/analyze/parse.py +141 -0
  78. pretensor/enrichment/analyze/pipeline.py +195 -0
  79. pretensor/enrichment/analyze/summary.py +38 -0
  80. pretensor/enrichment/analyze/walker.py +98 -0
  81. pretensor/enrichment/analyze/writers.py +214 -0
  82. pretensor/enrichment/dbt/__init__.py +30 -0
  83. pretensor/enrichment/dbt/lineage.py +100 -0
  84. pretensor/enrichment/dbt/manifest.py +300 -0
  85. pretensor/enrichment/dbt/metadata.py +263 -0
  86. pretensor/enrichment/dbt/pipeline.py +77 -0
  87. pretensor/enrichment/dbt/resolution.py +101 -0
  88. pretensor/enrichment/dbt/signals.py +305 -0
  89. pretensor/entities/__init__.py +27 -0
  90. pretensor/entities/builder.py +63 -0
  91. pretensor/entities/classifier.py +383 -0
  92. pretensor/entities/llm_extract.py +66 -0
  93. pretensor/errors.py +35 -0
  94. pretensor/graph_models/__init__.py +17 -0
  95. pretensor/graph_models/base.py +11 -0
  96. pretensor/graph_models/consumer.py +71 -0
  97. pretensor/graph_models/edge.py +35 -0
  98. pretensor/graph_models/entity.py +21 -0
  99. pretensor/graph_models/node.py +79 -0
  100. pretensor/graph_models/relationship.py +33 -0
  101. pretensor/integrations/__init__.py +42 -0
  102. pretensor/integrations/_base.py +138 -0
  103. pretensor/integrations/google_adk.py +49 -0
  104. pretensor/integrations/langchain.py +55 -0
  105. pretensor/integrations/llamaindex.py +53 -0
  106. pretensor/intelligence/__init__.py +33 -0
  107. pretensor/intelligence/cluster_labeler.py +425 -0
  108. pretensor/intelligence/clustering.py +168 -0
  109. pretensor/intelligence/combining.py +32 -0
  110. pretensor/intelligence/discovery.py +114 -0
  111. pretensor/intelligence/embeddings.py +317 -0
  112. pretensor/intelligence/graph_export.py +200 -0
  113. pretensor/intelligence/heuristic.py +544 -0
  114. pretensor/intelligence/join_paths/__init__.py +130 -0
  115. pretensor/intelligence/join_paths/on_demand.py +516 -0
  116. pretensor/intelligence/join_paths/storage.py +70 -0
  117. pretensor/intelligence/llm_infer.py +78 -0
  118. pretensor/intelligence/llm_runtime.py +62 -0
  119. pretensor/intelligence/metric_templates.py +193 -0
  120. pretensor/intelligence/pipeline.py +364 -0
  121. pretensor/intelligence/role_exemplars.py +263 -0
  122. pretensor/intelligence/schema_classification.py +360 -0
  123. pretensor/intelligence/scoring.py +76 -0
  124. pretensor/intelligence/semantic.py +240 -0
  125. pretensor/intelligence/shadow_alias.py +101 -0
  126. pretensor/intelligence/statistical.py +50 -0
  127. pretensor/intelligence/steps.py +191 -0
  128. pretensor/intelligence/steps_embedding.py +168 -0
  129. pretensor/introspection/__init__.py +6 -0
  130. pretensor/introspection/inspector.py +5 -0
  131. pretensor/introspection/models/__init__.py +0 -0
  132. pretensor/introspection/models/base.py +5 -0
  133. pretensor/introspection/models/config.py +237 -0
  134. pretensor/introspection/models/dsn.py +550 -0
  135. pretensor/introspection/models/plan.py +116 -0
  136. pretensor/introspection/models/schema.py +10 -0
  137. pretensor/introspection/models/semantic.py +121 -0
  138. pretensor/introspection/models/validation.py +116 -0
  139. pretensor/introspection/snapshot.py +46 -0
  140. pretensor/mcp/__init__.py +16 -0
  141. pretensor/mcp/config_json.py +24 -0
  142. pretensor/mcp/payload_types.py +274 -0
  143. pretensor/mcp/resources/__init__.py +17 -0
  144. pretensor/mcp/resources/markdown.py +314 -0
  145. pretensor/mcp/server.py +285 -0
  146. pretensor/mcp/service.py +49 -0
  147. pretensor/mcp/service_context.py +142 -0
  148. pretensor/mcp/service_registry.py +294 -0
  149. pretensor/mcp/store_cache.py +43 -0
  150. pretensor/mcp/tool_registry.py +136 -0
  151. pretensor/mcp/tools/__init__.py +1 -0
  152. pretensor/mcp/tools/_rank.py +244 -0
  153. pretensor/mcp/tools/_timed.py +26 -0
  154. pretensor/mcp/tools/compile_metric.py +144 -0
  155. pretensor/mcp/tools/consumers.py +161 -0
  156. pretensor/mcp/tools/context.py +1121 -0
  157. pretensor/mcp/tools/cypher.py +509 -0
  158. pretensor/mcp/tools/detect_changes.py +254 -0
  159. pretensor/mcp/tools/impact.py +271 -0
  160. pretensor/mcp/tools/list.py +131 -0
  161. pretensor/mcp/tools/schema.py +170 -0
  162. pretensor/mcp/tools/search.py +316 -0
  163. pretensor/mcp/tools/semantic_search.py +282 -0
  164. pretensor/mcp/tools/traverse.py +1027 -0
  165. pretensor/mcp/tools/validate_sql.py +150 -0
  166. pretensor/observability.py +203 -0
  167. pretensor/py.typed +0 -0
  168. pretensor/quickstart/README.md +29 -0
  169. pretensor/quickstart/__init__.py +6 -0
  170. pretensor/quickstart/docker-compose.yml +18 -0
  171. pretensor/quickstart/pagila_data.sql +63 -0
  172. pretensor/quickstart/pagila_ddl.sql +92 -0
  173. pretensor/search/__init__.py +6 -0
  174. pretensor/search/base.py +80 -0
  175. pretensor/search/index.py +435 -0
  176. pretensor/semantic/__init__.py +24 -0
  177. pretensor/semantic/base.py +123 -0
  178. pretensor/semantic/compiler.py +487 -0
  179. pretensor/semantic/yaml_layer.py +180 -0
  180. pretensor/skills/__init__.py +5 -0
  181. pretensor/skills/generator.py +235 -0
  182. pretensor/staleness/__init__.py +15 -0
  183. pretensor/staleness/graph_patcher.py +355 -0
  184. pretensor/staleness/impact_analyzer.py +162 -0
  185. pretensor/staleness/snapshot_store.py +38 -0
  186. pretensor/validation/__init__.py +9 -0
  187. pretensor/validation/query_validator.py +436 -0
  188. pretensor/visibility/__init__.py +23 -0
  189. pretensor/visibility/config.py +126 -0
  190. pretensor/visibility/filter.py +143 -0
  191. pretensor/visibility/kuzu_helpers.py +32 -0
  192. pretensor/visibility/runtime.py +36 -0
  193. pretensor/visibility/sync_grants.py +188 -0
  194. pretensor-0.1.0.dist-info/METADATA +251 -0
  195. pretensor-0.1.0.dist-info/RECORD +198 -0
  196. pretensor-0.1.0.dist-info/WHEEL +4 -0
  197. pretensor-0.1.0.dist-info/entry_points.txt +2 -0
  198. 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,7 @@
1
+ """Database connection registration CLI commands (``add``, ``remove``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pretensor.cli.commands.connections.register import register_connection_commands
6
+
7
+ __all__ = ["register_connection_commands"]
@@ -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
+ )