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