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,263 @@
1
+ """Enrich ``SchemaTable`` / ``SchemaColumn`` nodes from dbt ``manifest.json`` metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+
8
+ from pretensor.core.ids import column_node_id
9
+ from pretensor.core.store import KuzuStore
10
+ from pretensor.enrichment.dbt.manifest import DbtManifest, DbtModel, DbtSource
11
+ from pretensor.enrichment.dbt.resolution import (
12
+ physical_table_name_for_model,
13
+ physical_table_name_for_source,
14
+ )
15
+
16
+ __all__ = ["DbtMetadataWriteStats", "write_dbt_metadata"]
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class DbtMetadataWriteStats:
23
+ """Counts from ``write_dbt_metadata`` for CLI summaries.
24
+
25
+ ``tags_set`` counts tags *actually added* to the graph (delta against any
26
+ pre-existing tags on the node), not tags offered from the manifest.
27
+ """
28
+
29
+ tables_enriched: int
30
+ tags_set: int
31
+
32
+
33
+ def _empty_stats() -> DbtMetadataWriteStats:
34
+ return DbtMetadataWriteStats(tables_enriched=0, tags_set=0)
35
+
36
+
37
+ def _non_empty_str(value: str | None) -> str | None:
38
+ if value is None:
39
+ return None
40
+ s = value.strip()
41
+ return s if s else None
42
+
43
+
44
+ def _existing_table_tags(
45
+ store: KuzuStore,
46
+ connection_name: str,
47
+ schema_name: str,
48
+ table_name: str,
49
+ ) -> set[str]:
50
+ rows = store.query_all_rows(
51
+ """
52
+ MATCH (t:SchemaTable {connection_name: $cn, schema_name: $s, table_name: $n})
53
+ RETURN COALESCE(t.tags, CAST([] AS STRING[])) AS tags
54
+ """,
55
+ {"cn": connection_name, "s": schema_name, "n": table_name},
56
+ )
57
+ if not rows:
58
+ return set()
59
+ raw = rows[0][0]
60
+ if not isinstance(raw, (list, tuple)):
61
+ return set()
62
+ return {str(x) for x in raw if str(x).strip()}
63
+
64
+
65
+ def _apply_table_metadata(
66
+ store: KuzuStore,
67
+ connection_name: str,
68
+ schema_name: str,
69
+ table_name: str,
70
+ *,
71
+ description: str | None,
72
+ tags: tuple[str, ...],
73
+ dbt_ref: str,
74
+ ) -> DbtMetadataWriteStats:
75
+ desc = _non_empty_str(description)
76
+ tag_list = [t.strip() for t in tags if t and str(t).strip()]
77
+ if desc is None and not tag_list:
78
+ return _empty_stats()
79
+
80
+ pre_existing = (
81
+ _existing_table_tags(store, connection_name, schema_name, table_name)
82
+ if tag_list
83
+ else set()
84
+ )
85
+
86
+ params: dict[str, object] = {
87
+ "cn": connection_name,
88
+ "s": schema_name,
89
+ "n": table_name,
90
+ }
91
+ set_parts: list[str] = []
92
+ if desc is not None:
93
+ set_parts.append(
94
+ "t.description = CASE WHEN t.description IS NULL OR t.description = '' "
95
+ "THEN $tbl_desc ELSE t.description END"
96
+ )
97
+ params["tbl_desc"] = desc
98
+ if tag_list:
99
+ set_parts.append(
100
+ "t.tags = list_distinct(list_concat(COALESCE(t.tags, CAST([] AS STRING[])), $new_tags))"
101
+ )
102
+ params["new_tags"] = tag_list
103
+ if not set_parts:
104
+ return _empty_stats()
105
+ result = store.query_all_rows(
106
+ f"""
107
+ MATCH (t:SchemaTable {{connection_name: $cn, schema_name: $s, table_name: $n}})
108
+ SET {", ".join(set_parts)}
109
+ RETURN t.node_id
110
+ """,
111
+ params,
112
+ )
113
+ if not result:
114
+ logger.debug(
115
+ "dbt metadata: SchemaTable missing for %s (schema=%r table=%r)",
116
+ dbt_ref,
117
+ schema_name,
118
+ table_name,
119
+ )
120
+ return _empty_stats()
121
+
122
+ new_tags_added = len({t for t in tag_list if t not in pre_existing})
123
+ return DbtMetadataWriteStats(
124
+ tables_enriched=1,
125
+ tags_set=new_tags_added,
126
+ )
127
+
128
+
129
+ def _apply_column_description(
130
+ store: KuzuStore,
131
+ connection_name: str,
132
+ schema_name: str,
133
+ table_name: str,
134
+ column_name: str,
135
+ description: str,
136
+ *,
137
+ dbt_ref: str,
138
+ ) -> None:
139
+ desc = _non_empty_str(description)
140
+ if desc is None:
141
+ return
142
+ cid = column_node_id(connection_name, schema_name, table_name, column_name)
143
+ result = store.query_all_rows(
144
+ """
145
+ MATCH (c:SchemaColumn {node_id: $cid})
146
+ SET c.description = CASE WHEN c.description IS NULL OR c.description = ''
147
+ THEN $col_desc ELSE c.description END
148
+ RETURN c.node_id
149
+ """,
150
+ {"cid": cid, "col_desc": desc},
151
+ )
152
+ if not result:
153
+ logger.debug(
154
+ "dbt metadata: SchemaColumn missing for %s column %r (node_id=%s)",
155
+ dbt_ref,
156
+ column_name,
157
+ cid,
158
+ )
159
+
160
+
161
+ def _enrich_model(
162
+ store: KuzuStore, connection_name: str, model: DbtModel
163
+ ) -> DbtMetadataWriteStats:
164
+ schema = model.schema_name
165
+ phys = physical_table_name_for_model(model)
166
+ if not schema or not phys:
167
+ logger.debug(
168
+ "dbt metadata: incomplete model metadata for %s (schema=%r table=%r)",
169
+ model.unique_id,
170
+ schema,
171
+ phys,
172
+ )
173
+ return _empty_stats()
174
+ stats = _apply_table_metadata(
175
+ store,
176
+ connection_name,
177
+ schema,
178
+ phys,
179
+ description=model.description,
180
+ tags=model.tags,
181
+ dbt_ref=model.unique_id,
182
+ )
183
+ for col_name, col_desc in model.column_descriptions.items():
184
+ _apply_column_description(
185
+ store,
186
+ connection_name,
187
+ schema,
188
+ phys,
189
+ col_name,
190
+ col_desc,
191
+ dbt_ref=model.unique_id,
192
+ )
193
+ return stats
194
+
195
+
196
+ def _enrich_source(
197
+ store: KuzuStore, connection_name: str, source: DbtSource
198
+ ) -> DbtMetadataWriteStats:
199
+ schema = source.schema_name
200
+ phys = physical_table_name_for_source(source)
201
+ if not schema or not phys:
202
+ logger.debug(
203
+ "dbt metadata: incomplete source metadata for %s (schema=%r table=%r)",
204
+ source.unique_id,
205
+ schema,
206
+ phys,
207
+ )
208
+ return _empty_stats()
209
+ stats = _apply_table_metadata(
210
+ store,
211
+ connection_name,
212
+ schema,
213
+ phys,
214
+ description=source.description,
215
+ tags=source.tags,
216
+ dbt_ref=source.unique_id,
217
+ )
218
+ for col_name, col_desc in source.column_descriptions.items():
219
+ _apply_column_description(
220
+ store,
221
+ connection_name,
222
+ schema,
223
+ phys,
224
+ col_name,
225
+ col_desc,
226
+ dbt_ref=source.unique_id,
227
+ )
228
+ return stats
229
+
230
+
231
+ def write_dbt_metadata(
232
+ manifest: DbtManifest, store: KuzuStore, connection_name: str
233
+ ) -> DbtMetadataWriteStats:
234
+ """Merge dbt model/source descriptions, column descriptions, and tags into the graph.
235
+
236
+ Table-level: sets ``SchemaTable.description`` only when the current value is null or
237
+ empty; merges dbt ``tags`` into ``SchemaTable.tags`` (union, order not preserved).
238
+
239
+ Column-level: sets ``SchemaColumn.description`` when null or empty, matching by
240
+ ``connection_name``, schema, physical table name, and column name.
241
+
242
+ Skips nodes that are not present in the store (debug log only). Does not resolve
243
+ ``docs()`` blocks — only inline string descriptions in the manifest.
244
+
245
+ Args:
246
+ manifest: Parsed dbt manifest.
247
+ store: Open Kuzu store (``ensure_schema`` recommended).
248
+ connection_name: Pretensor connection name used when tables were indexed.
249
+
250
+ Returns:
251
+ Counts of tables updated and number of distinct new tags actually added.
252
+ """
253
+ total_tables = 0
254
+ total_tags = 0
255
+ for model in manifest.nodes.values():
256
+ st = _enrich_model(store, connection_name, model)
257
+ total_tables += st.tables_enriched
258
+ total_tags += st.tags_set
259
+ for source in manifest.sources.values():
260
+ st = _enrich_source(store, connection_name, source)
261
+ total_tables += st.tables_enriched
262
+ total_tags += st.tags_set
263
+ return DbtMetadataWriteStats(tables_enriched=total_tables, tags_set=total_tags)
@@ -0,0 +1,77 @@
1
+ """Orchestrate dbt manifest enrichment passes (lineage, metadata, signals)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from pretensor.core.store import KuzuStore
9
+ from pretensor.enrichment.dbt.lineage import write_dbt_lineage
10
+ from pretensor.enrichment.dbt.manifest import DbtManifest
11
+ from pretensor.enrichment.dbt.metadata import write_dbt_metadata
12
+ from pretensor.enrichment.dbt.signals import write_dbt_signals
13
+
14
+ __all__ = ["DbtEnrichmentSummary", "run_dbt_enrichment", "run_dbt_enrichment_from_manifest"]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class DbtEnrichmentSummary:
19
+ """Aggregate counts after running all dbt enrichment writers."""
20
+
21
+ lineage_edges: int
22
+ tables_enriched: int
23
+ tags_set: int
24
+ exposures_marked: int
25
+ freshness_rows_applied: int
26
+ tests_counted: int
27
+
28
+
29
+ def run_dbt_enrichment_from_manifest(
30
+ manifest: DbtManifest,
31
+ sources_path: Path | None,
32
+ store: KuzuStore,
33
+ connection_name: str,
34
+ ) -> DbtEnrichmentSummary:
35
+ """Run lineage → metadata → signals on an already-parsed dbt manifest.
36
+
37
+ Exists so CLI commands can preload/validate the manifest *before* mutating
38
+ the graph and still share the same orchestration as ``run_dbt_enrichment``.
39
+ """
40
+ lineage_edges = write_dbt_lineage(manifest, store, connection_name)
41
+ meta_stats = write_dbt_metadata(manifest, store, connection_name)
42
+ signal_stats = write_dbt_signals(
43
+ manifest, store, connection_name, sources_path=sources_path
44
+ )
45
+ return DbtEnrichmentSummary(
46
+ lineage_edges=lineage_edges,
47
+ tables_enriched=meta_stats.tables_enriched,
48
+ tags_set=meta_stats.tags_set,
49
+ exposures_marked=signal_stats.exposures_marked,
50
+ freshness_rows_applied=signal_stats.freshness_rows_applied,
51
+ tests_counted=signal_stats.tests_counted,
52
+ )
53
+
54
+
55
+ def run_dbt_enrichment(
56
+ manifest_path: Path,
57
+ sources_path: Path | None,
58
+ store: KuzuStore,
59
+ connection_name: str,
60
+ ) -> DbtEnrichmentSummary:
61
+ """Load a dbt manifest and run lineage, metadata, then exposure/freshness signals.
62
+
63
+ Order matches the dbt enrichment pipeline sequence.
64
+
65
+ Args:
66
+ manifest_path: Path to dbt ``target/manifest.json`` (after ``dbt compile`` / ``dbt run``).
67
+ sources_path: Optional ``sources.json`` from ``dbt source freshness`` (or ``None``).
68
+ store: Open Kuzu graph store for the indexed connection.
69
+ connection_name: Logical Pretensor connection name for this database.
70
+
71
+ Returns:
72
+ Counts for CLI summary lines.
73
+ """
74
+ manifest = DbtManifest.load(manifest_path)
75
+ return run_dbt_enrichment_from_manifest(
76
+ manifest, sources_path, store, connection_name
77
+ )
@@ -0,0 +1,101 @@
1
+ """Name and node-id resolution helpers shared across dbt enrichment writers.
2
+
3
+ These map dbt ``unique_id`` values (``model.<pkg>.<name>`` / ``source.<pkg>.<src>.<name>``)
4
+ to physical ``SchemaTable`` node ids for an indexed Pretensor connection.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ from pretensor.core.ids import table_node_id
12
+ from pretensor.enrichment.dbt.manifest import DbtManifest, DbtModel, DbtSource
13
+
14
+ __all__ = [
15
+ "physical_table_name_for_model",
16
+ "physical_table_name_for_source",
17
+ "resolve_dbt_parent_node_id",
18
+ "resolve_dbt_child_node_id",
19
+ ]
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def physical_table_name_for_model(model: DbtModel) -> str | None:
25
+ """Return the physical table name dbt writes the model to (``alias`` or ``name``)."""
26
+ return model.alias or model.name
27
+
28
+
29
+ def physical_table_name_for_source(source: DbtSource) -> str | None:
30
+ """Return the physical table name dbt reads the source from (``identifier`` or ``name``)."""
31
+ return source.identifier or source.name
32
+
33
+
34
+ def resolve_dbt_parent_node_id(
35
+ manifest: DbtManifest,
36
+ connection_name: str,
37
+ parent_id: str,
38
+ ) -> str | None:
39
+ """Resolve a dbt ``unique_id`` (model or source) to its ``SchemaTable`` node id."""
40
+ if parent_id.startswith("source."):
41
+ src = manifest.sources.get(parent_id)
42
+ if src is None:
43
+ logger.debug("dbt resolution: unknown source id %s", parent_id)
44
+ return None
45
+ schema = src.schema_name
46
+ phys = physical_table_name_for_source(src)
47
+ if not schema or not phys:
48
+ logger.debug(
49
+ "dbt resolution: incomplete source metadata for %s (schema=%r table=%r)",
50
+ parent_id,
51
+ schema,
52
+ phys,
53
+ )
54
+ return None
55
+ return table_node_id(connection_name, schema, phys)
56
+ if parent_id.startswith("model."):
57
+ mdl = manifest.nodes.get(parent_id)
58
+ if mdl is None:
59
+ logger.debug("dbt resolution: unknown model id %s", parent_id)
60
+ return None
61
+ schema = mdl.schema_name
62
+ phys = physical_table_name_for_model(mdl)
63
+ if not schema or not phys:
64
+ logger.debug(
65
+ "dbt resolution: incomplete model metadata for %s (schema=%r table=%r)",
66
+ parent_id,
67
+ schema,
68
+ phys,
69
+ )
70
+ return None
71
+ return table_node_id(connection_name, schema, phys)
72
+ logger.debug("dbt resolution: skipping non-model non-source id %s", parent_id)
73
+ return None
74
+
75
+
76
+ def resolve_dbt_child_node_id(
77
+ manifest: DbtManifest,
78
+ connection_name: str,
79
+ child_id: str,
80
+ ) -> str | None:
81
+ """Resolve a dbt ``model.*`` ``unique_id`` to its ``SchemaTable`` node id.
82
+
83
+ Only models can be lineage children (sources have no upstream in dbt).
84
+ """
85
+ if not child_id.startswith("model."):
86
+ return None
87
+ mdl = manifest.nodes.get(child_id)
88
+ if mdl is None:
89
+ logger.debug("dbt resolution: unknown child model id %s", child_id)
90
+ return None
91
+ schema = mdl.schema_name
92
+ phys = physical_table_name_for_model(mdl)
93
+ if not schema or not phys:
94
+ logger.debug(
95
+ "dbt resolution: incomplete child model metadata for %s (schema=%r table=%r)",
96
+ child_id,
97
+ schema,
98
+ phys,
99
+ )
100
+ return None
101
+ return table_node_id(connection_name, schema, phys)