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,52 @@
1
+ """CLI plugin discovery via Python entry_points.
2
+
3
+ Plugins declare themselves under the ``pretensor.cli_plugins`` entry-point
4
+ group in their ``pyproject.toml``::
5
+
6
+ [project.entry-points."pretensor.cli_plugins"]
7
+ my-plugin = "my_package.cli_plugin:register"
8
+
9
+ Each entry point must be a callable with the signature::
10
+
11
+ def register(app: typer.Typer) -> None: ...
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib.metadata
17
+ import logging
18
+
19
+ import typer
20
+
21
+ __all__ = ["discover_cli_plugins"]
22
+
23
+ _ENTRY_POINT_GROUP = "pretensor.cli_plugins"
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def discover_cli_plugins(app: typer.Typer) -> None:
29
+ """Scan the ``pretensor.cli_plugins`` entry-point group and register commands.
30
+
31
+ Each entry point must resolve to a callable that accepts a single
32
+ ``typer.Typer`` argument. Errors during load or registration are logged
33
+ and skipped so a broken plugin never prevents the CLI from starting.
34
+
35
+ Args:
36
+ app: The root Typer application to register plugin commands into.
37
+ """
38
+ eps = importlib.metadata.entry_points(group=_ENTRY_POINT_GROUP)
39
+ for ep in eps:
40
+ try:
41
+ register = ep.load()
42
+ except Exception:
43
+ logger.warning(
44
+ "pretensor: failed to load CLI plugin %r — skipping", ep.name, exc_info=True
45
+ )
46
+ continue
47
+ try:
48
+ register(app)
49
+ except Exception:
50
+ logger.warning(
51
+ "pretensor: failed to register CLI plugin %r — skipping", ep.name, exc_info=True
52
+ )
pretensor/config.py ADDED
@@ -0,0 +1,226 @@
1
+ """Graph package configuration (clustering, intelligence defaults)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from pretensor.core.store import KuzuStore
11
+ from pretensor.intelligence.combining import ConfidenceCombiner
12
+ from pretensor.intelligence.scoring import ScorerRegistry
13
+ from pretensor.search.base import BaseSearchIndex
14
+ from pretensor.semantic.base import SemanticLayer
15
+ from pretensor.semantic.yaml_layer import YamlSemanticLayer
16
+
17
+ __all__ = [
18
+ "EmbeddingsConfig",
19
+ "GraphConfig",
20
+ "PretensorConfig",
21
+ "load_semantic_layer",
22
+ ]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class EmbeddingsConfig:
27
+ """Opt-in toggles for the ``[embeddings]`` extra."""
28
+
29
+ index_tables: bool = False
30
+ """When True and ``[embeddings]`` is installed, ``_EmbeddingIndexStep``
31
+ computes one embedding per :class:`SchemaTable` during intelligence
32
+ indexing. Default ``False`` keeps the null path byte-identical to the
33
+ pre-embedding pipeline output."""
34
+
35
+ cluster_blend: float = 0.0
36
+ """Weight (in [0.0, 1.0]) for blending embedding cosine similarity into
37
+ existing FK / INFERRED_JOIN edge weights before community detection.
38
+
39
+ For each existing structural edge ``(a, b)`` where both endpoints
40
+ carry an embedding, the edge weight is incremented by
41
+ ``cluster_blend * cosine(emb_a, emb_b)``. No new edges are introduced
42
+ from cosine alone — the blend only nudges already-structurally-adjacent
43
+ pairs. Default ``0.0`` keeps cluster membership byte-identical to the
44
+ pre-embedding pipeline output (Invariant #6). A small positive value
45
+ (~0.25) lets domain-related tables with weak lexical overlap drift
46
+ into the same cluster.
47
+ """
48
+
49
+ join_threshold: float | None = None
50
+ """Cosine threshold for proposing ``INFERRED_JOIN`` candidates from
51
+ embedding similarity. ``None`` (default) disables the
52
+ embedding scorer entirely → null-path identical. When set (typical
53
+ value ~0.85), an ``EmbeddingRelationshipScorer`` is registered after
54
+ the heuristic scorer; pairs whose cosine ≥ threshold are emitted as
55
+ ``status="suggested"`` candidates with ``source="embedding"``. The
56
+ scorer applies a type-family compatibility gate per column pair
57
+ (reusing the heuristic's check) so high-cosine pairs with
58
+ incompatible types are dropped.
59
+ """
60
+
61
+ role_weight: float = 0.0
62
+ """Weight for the embedding-based role-classification vote.
63
+ ``0.0`` (default) skips the embedding signal entirely → null-path
64
+ identical. When > 0, ``classify_database_tables_async`` embeds each
65
+ table and adds ``role_weight * cosine(table, role_centroid)`` to
66
+ every role's heuristic score; the heuristic remains the primary
67
+ signal. Centroids are computed once per process from a small curated
68
+ exemplar set in ``pretensor.intelligence.role_exemplars``.
69
+ """
70
+
71
+ def __post_init__(self) -> None:
72
+ # Validate the float knobs at construction time so misuse surfaces
73
+ # close to its source rather than mid-pipeline. All three numeric
74
+ # toggles are checked here for one consistent contract on the
75
+ # dataclass; ``EmbeddingRelationshipScorer.__init__`` keeps its own
76
+ # belt-and-braces check for callers that construct the scorer
77
+ # directly with an out-of-band threshold.
78
+ if not 0.0 <= self.cluster_blend <= 1.0:
79
+ msg = (
80
+ f"EmbeddingsConfig.cluster_blend must be in [0.0, 1.0]; "
81
+ f"got {self.cluster_blend!r}"
82
+ )
83
+ raise ValueError(msg)
84
+ if not 0.0 <= self.role_weight <= 1.0:
85
+ msg = (
86
+ f"EmbeddingsConfig.role_weight must be in [0.0, 1.0]; "
87
+ f"got {self.role_weight!r}"
88
+ )
89
+ raise ValueError(msg)
90
+ if self.join_threshold is not None and not (
91
+ 0.0 <= self.join_threshold <= 1.0
92
+ ):
93
+ msg = (
94
+ f"EmbeddingsConfig.join_threshold must be in [0.0, 1.0] "
95
+ f"or None; got {self.join_threshold!r}"
96
+ )
97
+ raise ValueError(msg)
98
+
99
+
100
+ @dataclass(frozen=True, slots=True)
101
+ class GraphConfig:
102
+ """Tunable parameters for graph intelligence (clustering, join paths)."""
103
+
104
+ stale_index_warning_days: int = 7
105
+ """Warn in MCP responses when ``last_indexed`` is older than this many days."""
106
+
107
+ clustering_resolution_override: float | None = None
108
+ """If set, skip automatic resolution heuristic and use this value for Leiden."""
109
+
110
+ join_path_max_depth: int = 4
111
+ """Maximum hop count for precomputed intra-cluster join paths."""
112
+
113
+ min_cluster_size_merge: int = 3
114
+ """Clusters with fewer tables are merged into a neighbor cluster."""
115
+
116
+ collapse_shadow_aliases: bool = True
117
+ """Treat single-source views (``table_type='view'`` with exactly one
118
+ incoming ``LINEAGE`` edge) as transparent aliases of their base table.
119
+ When enabled, intelligence consumers (clustering, INFERRED_JOIN, impact,
120
+ traverse, context) suppress or pass through these aliases."""
121
+
122
+ lineage_in_max_for_alias: int = 1
123
+ """Maximum incoming LINEAGE edge count for a view to be considered a
124
+ shadow alias. The default of 1 matches pure 1:1 projections."""
125
+
126
+
127
+ def _default_scorer_registry() -> ScorerRegistry:
128
+ from pretensor.intelligence.heuristic import HeuristicScorer
129
+ from pretensor.intelligence.scoring import ScorerRegistry
130
+
131
+ return ScorerRegistry([HeuristicScorer()])
132
+
133
+
134
+ def _default_combiner() -> ConfidenceCombiner:
135
+ from pretensor.intelligence.combining import MaxScoreCombiner
136
+
137
+ return MaxScoreCombiner()
138
+
139
+
140
+ def _default_search_index_cls() -> type[BaseSearchIndex]:
141
+ from pretensor.search.index import KeywordSearchIndex
142
+
143
+ return KeywordSearchIndex
144
+
145
+
146
+ def _default_semantic_layer() -> SemanticLayer:
147
+ from pretensor.semantic.base import NullSemanticLayer
148
+
149
+ return NullSemanticLayer()
150
+
151
+
152
+ @dataclass
153
+ class PretensorConfig:
154
+ """Central pluggable configuration for the Pretensor graph system.
155
+
156
+ Bundles all tunable and swappable components so that downstream packages can ship a
157
+ ``PretensorConfig`` subclass with its own implementations
158
+ without patching individual files.
159
+
160
+ All fields default to the OSS implementations, so constructing
161
+ ``PretensorConfig()`` with no arguments is always valid for basic use.
162
+
163
+ Attributes:
164
+ graph: Graph intelligence tuning parameters (clustering, join paths).
165
+ embeddings: Opt-in toggles for the ``[embeddings]`` extra.
166
+ Default: ``EmbeddingsConfig()`` (all toggles off).
167
+ scorer_registry: Ordered registry of relationship scorers used by discovery.
168
+ Default: ``ScorerRegistry([HeuristicScorer()])``.
169
+ combiner: Strategy for merging scored relationship candidates.
170
+ Default: ``MaxScoreCombiner()``.
171
+ search_index_cls: Class used to build or load the keyword/vector search index.
172
+ Default: ``KeywordSearchIndex``.
173
+ semantic_layer: Semantic enrichment layer for entities, metrics, and dimensions.
174
+ Default: ``NullSemanticLayer()`` (no-op for OSS).
175
+ """
176
+
177
+ graph: GraphConfig = field(default_factory=GraphConfig)
178
+ embeddings: EmbeddingsConfig = field(default_factory=EmbeddingsConfig)
179
+ scorer_registry: ScorerRegistry = field(default_factory=_default_scorer_registry)
180
+ combiner: ConfidenceCombiner = field(default_factory=_default_combiner)
181
+ search_index_cls: type[BaseSearchIndex] = field(
182
+ default_factory=_default_search_index_cls
183
+ )
184
+ semantic_layer: SemanticLayer = field(default_factory=_default_semantic_layer)
185
+
186
+
187
+ def load_semantic_layer(
188
+ yaml_path: Path,
189
+ *,
190
+ store: KuzuStore,
191
+ database_key: str,
192
+ dialect: str = "postgres",
193
+ ) -> YamlSemanticLayer:
194
+ """Read a YAML file and return a ready-to-use :class:`YamlSemanticLayer`.
195
+
196
+ The YAML must match the
197
+ :class:`pretensor.introspection.models.semantic.SemanticLayer` schema.
198
+ The returned layer uses ``store`` for graph lookups (metric compilation,
199
+ query validation, impact) scoped to ``database_key``.
200
+
201
+ Args:
202
+ yaml_path: Filesystem path to the semantic layer YAML.
203
+ store: Open Kuzu store for the same ``database_key``.
204
+ database_key: Logical database key as recorded on ``SchemaTable``.
205
+ dialect: sqlglot dialect for SQL parsing (default PostgreSQL).
206
+
207
+ Returns:
208
+ A :class:`YamlSemanticLayer` instance.
209
+
210
+ Raises:
211
+ FileNotFoundError: If ``yaml_path`` does not exist.
212
+ pydantic.ValidationError: If the YAML fails schema validation.
213
+ """
214
+ from pretensor.introspection.models.semantic import (
215
+ SemanticLayer as SemanticLayerModel,
216
+ )
217
+ from pretensor.semantic.yaml_layer import YamlSemanticLayer
218
+
219
+ text = yaml_path.read_text(encoding="utf-8")
220
+ model = SemanticLayerModel.from_yaml(text)
221
+ return YamlSemanticLayer(
222
+ model,
223
+ store=store,
224
+ database_key=database_key,
225
+ dialect=dialect,
226
+ )
@@ -0,0 +1,29 @@
1
+ """Connector-layer schema types, introspection, and snapshot diffing."""
2
+
3
+ from pretensor.connectors.inspect import inspect
4
+ from pretensor.connectors.models import (
5
+ Column,
6
+ ConnectionConfig,
7
+ ForeignKey,
8
+ SchemaSnapshot,
9
+ Table,
10
+ )
11
+ from pretensor.connectors.snapshot import (
12
+ ChangeTarget,
13
+ ChangeType,
14
+ SchemaChange,
15
+ diff_snapshots,
16
+ )
17
+
18
+ __all__ = [
19
+ "ChangeTarget",
20
+ "ChangeType",
21
+ "Column",
22
+ "ConnectionConfig",
23
+ "ForeignKey",
24
+ "SchemaChange",
25
+ "SchemaSnapshot",
26
+ "Table",
27
+ "diff_snapshots",
28
+ "inspect",
29
+ ]
@@ -0,0 +1,165 @@
1
+ """Abstract connector interface for database introspection.
2
+
3
+ Every database connector implements BaseConnector. All database-specific
4
+ imports and logic live inside the concrete connector files — nothing
5
+ above the connector layer knows which database produced the data.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any
12
+
13
+ from pydantic import Field
14
+
15
+ from pretensor.connectors.models import ViewDependency
16
+ from pretensor.introspection.models.base import PretensorModel
17
+ from pretensor.introspection.models.config import ConnectionConfig, SchemaFilter
18
+
19
+
20
+ class TableInfo(PretensorModel):
21
+ """Table metadata returned by a connector (no column details)."""
22
+
23
+ name: str
24
+ schema_name: str
25
+ row_count: int | None = None
26
+ row_count_source: str | None = Field(
27
+ default=None,
28
+ description=(
29
+ "Provenance of `row_count`: 'stat' (catalog statistic), "
30
+ "'view_count' (live SELECT COUNT(*) on a view), "
31
+ "'view_timeout' (view count exceeded the connector timeout — "
32
+ "row_count = -1)."
33
+ ),
34
+ )
35
+ comment: str | None = None
36
+ table_type: str | None = None
37
+
38
+
39
+ class ColumnInfo(PretensorModel):
40
+ """Column metadata returned by a connector (no stats)."""
41
+
42
+ name: str
43
+ data_type: str
44
+ nullable: bool = True
45
+ is_primary_key: bool = False
46
+ default_value: str | None = None
47
+ comment: str | None = None
48
+ is_indexed: bool = False
49
+ check_constraints: list[str] = Field(default_factory=list)
50
+ ordinal_position: int | None = None
51
+ column_cardinality: int | None = None
52
+ index_type: str | None = None
53
+ index_is_unique: bool | None = None
54
+ parent_column: str | None = None
55
+ is_array: bool = False
56
+
57
+
58
+ class ForeignKeyInfo(PretensorModel):
59
+ """Foreign key relationship returned by a connector."""
60
+
61
+ constraint_name: str | None = None
62
+ source_schema: str
63
+ source_table: str
64
+ source_column: str
65
+ target_schema: str
66
+ target_table: str
67
+ target_column: str
68
+
69
+
70
+ class ColumnStats(PretensorModel):
71
+ """Statistical metadata for a single column."""
72
+
73
+ distinct_count: int | None = None
74
+ min_value: str | None = None
75
+ max_value: str | None = None
76
+ null_percentage: float | None = None
77
+ sample_distinct_values: list[str] | None = None
78
+
79
+
80
+ class TableGrant(PretensorModel):
81
+ """SELECT grant on a table for an effective grantee (role or login)."""
82
+
83
+ grantee: str
84
+ schema_name: str
85
+ table_name: str
86
+
87
+
88
+ class BaseConnector(ABC):
89
+ """Abstract interface that every database connector must implement.
90
+
91
+ Usage as a context manager is preferred::
92
+
93
+ with PostgresConnector(config) as conn:
94
+ tables = conn.get_tables()
95
+ """
96
+
97
+ def __init__(self, config: ConnectionConfig) -> None:
98
+ self.config = config
99
+
100
+ def __enter__(self) -> BaseConnector:
101
+ self.connect()
102
+ return self
103
+
104
+ def __exit__(self, *exc: Any) -> None:
105
+ self.disconnect()
106
+
107
+ @abstractmethod
108
+ def connect(self) -> None: ...
109
+
110
+ @abstractmethod
111
+ def disconnect(self) -> None: ...
112
+
113
+ @abstractmethod
114
+ def get_tables(
115
+ self, schema_filter: SchemaFilter | None = None
116
+ ) -> list[TableInfo]: ...
117
+
118
+ @abstractmethod
119
+ def get_columns(self, table_name: str, schema_name: str) -> list[ColumnInfo]: ...
120
+
121
+ @abstractmethod
122
+ def get_foreign_keys(self) -> list[ForeignKeyInfo]: ...
123
+
124
+ @abstractmethod
125
+ def get_table_row_count(self, table_name: str, schema_name: str) -> int: ...
126
+
127
+ @abstractmethod
128
+ def get_column_stats(
129
+ self, table_name: str, column_name: str, schema_name: str
130
+ ) -> ColumnStats: ...
131
+
132
+ @abstractmethod
133
+ def execute_query(self, sql: str) -> list[dict[str, Any]]: ...
134
+
135
+ def load_deep_catalog(
136
+ self, schema_filter: SchemaFilter
137
+ ) -> tuple[
138
+ dict[tuple[str, str], dict[str, Any]],
139
+ dict[tuple[str, str, str], dict[str, Any]],
140
+ ]:
141
+ """Optional bulk catalog enrichment (usage, stats, grants). Default: none.
142
+
143
+ Callers treat this as best-effort: failures must not block indexing (log and
144
+ continue without enrichment).
145
+ """
146
+ _ = schema_filter
147
+ return {}, {}
148
+
149
+ def load_view_dependencies(self, schema_filter: SchemaFilter) -> list[ViewDependency]:
150
+ """Table-level lineage from views, triggers, or warehouse objects. Default: none."""
151
+ _ = schema_filter
152
+ return []
153
+
154
+ def get_table_grants(
155
+ self, schema_filter: SchemaFilter | None = None
156
+ ) -> list[TableGrant]:
157
+ """SELECT table grants, including connector-specific expansion (e.g. role inheritance).
158
+
159
+ Default: no grant introspection (empty list).
160
+
161
+ Callers treat this as best-effort: failures must not block indexing (log and
162
+ continue without grants), same contract as :meth:`load_deep_catalog`.
163
+ """
164
+ _ = schema_filter
165
+ return []