pretensor 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pretensor/__init__.py +50 -0
- pretensor/benchmark/__init__.py +54 -0
- pretensor/benchmark/cli.py +294 -0
- pretensor/benchmark/fixtures.py +84 -0
- pretensor/benchmark/l1/__init__.py +23 -0
- pretensor/benchmark/l1/metrics.py +141 -0
- pretensor/benchmark/l1/pipeline.py +188 -0
- pretensor/benchmark/l1/runner.py +245 -0
- pretensor/benchmark/l2/__init__.py +27 -0
- pretensor/benchmark/l2/gold.py +236 -0
- pretensor/benchmark/l2/metrics.py +146 -0
- pretensor/benchmark/l2/pipeline.py +124 -0
- pretensor/benchmark/l2/runner.py +530 -0
- pretensor/benchmark/l3/__init__.py +73 -0
- pretensor/benchmark/l3/agent.py +316 -0
- pretensor/benchmark/l3/db.py +188 -0
- pretensor/benchmark/l3/gold.py +85 -0
- pretensor/benchmark/l3/llm_client.py +395 -0
- pretensor/benchmark/l3/mcp_client.py +357 -0
- pretensor/benchmark/l3/pretensor_runner.py +456 -0
- pretensor/benchmark/l3/prompt.py +132 -0
- pretensor/benchmark/l3/runner.py +358 -0
- pretensor/benchmark/l3/sql_equivalence.py +176 -0
- pretensor/benchmark/release_gate.py +448 -0
- pretensor/benchmark/results.py +298 -0
- pretensor/benchmark/runner.py +109 -0
- pretensor/cli/__init__.py +1 -0
- pretensor/cli/commands/_source_runner.py +147 -0
- pretensor/cli/commands/analyze.py +201 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +126 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +131 -0
- pretensor/cli/commands/index.py +559 -0
- pretensor/cli/commands/list.py +76 -0
- pretensor/cli/commands/quickstart.py +207 -0
- pretensor/cli/commands/reindex.py +646 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +144 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +176 -0
- pretensor/cli/config_file.py +442 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +109 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +226 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +468 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +97 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/mysql.py +402 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +938 -0
- pretensor/connectors/registry.py +93 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +908 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +307 -0
- pretensor/core/dsn_crypto.py +51 -0
- pretensor/core/graph_schema_manager.py +246 -0
- pretensor/core/graph_store.py +1226 -0
- pretensor/core/ids.py +101 -0
- pretensor/core/portable_export.py +276 -0
- pretensor/core/query_runner.py +67 -0
- pretensor/core/registry.py +209 -0
- pretensor/core/schema.py +473 -0
- pretensor/core/secure_io.py +93 -0
- pretensor/core/store.py +469 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/analyze/__init__.py +0 -0
- pretensor/enrichment/analyze/classify.py +49 -0
- pretensor/enrichment/analyze/extract_python.py +196 -0
- pretensor/enrichment/analyze/parse.py +141 -0
- pretensor/enrichment/analyze/pipeline.py +195 -0
- pretensor/enrichment/analyze/summary.py +38 -0
- pretensor/enrichment/analyze/walker.py +98 -0
- pretensor/enrichment/analyze/writers.py +214 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +300 -0
- pretensor/enrichment/dbt/metadata.py +263 -0
- pretensor/enrichment/dbt/pipeline.py +77 -0
- pretensor/enrichment/dbt/resolution.py +101 -0
- pretensor/enrichment/dbt/signals.py +305 -0
- pretensor/entities/__init__.py +27 -0
- pretensor/entities/builder.py +63 -0
- pretensor/entities/classifier.py +383 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/errors.py +35 -0
- pretensor/graph_models/__init__.py +17 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/consumer.py +71 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +79 -0
- pretensor/graph_models/relationship.py +33 -0
- pretensor/integrations/__init__.py +42 -0
- pretensor/integrations/_base.py +138 -0
- pretensor/integrations/google_adk.py +49 -0
- pretensor/integrations/langchain.py +55 -0
- pretensor/integrations/llamaindex.py +53 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +425 -0
- pretensor/intelligence/clustering.py +168 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +317 -0
- pretensor/intelligence/graph_export.py +200 -0
- pretensor/intelligence/heuristic.py +544 -0
- pretensor/intelligence/join_paths/__init__.py +130 -0
- pretensor/intelligence/join_paths/on_demand.py +516 -0
- pretensor/intelligence/join_paths/storage.py +70 -0
- pretensor/intelligence/llm_infer.py +78 -0
- pretensor/intelligence/llm_runtime.py +62 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +364 -0
- pretensor/intelligence/role_exemplars.py +263 -0
- pretensor/intelligence/schema_classification.py +360 -0
- pretensor/intelligence/scoring.py +76 -0
- pretensor/intelligence/semantic.py +240 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +191 -0
- pretensor/intelligence/steps_embedding.py +168 -0
- pretensor/introspection/__init__.py +6 -0
- pretensor/introspection/inspector.py +5 -0
- pretensor/introspection/models/__init__.py +0 -0
- pretensor/introspection/models/base.py +5 -0
- pretensor/introspection/models/config.py +237 -0
- pretensor/introspection/models/dsn.py +550 -0
- pretensor/introspection/models/plan.py +116 -0
- pretensor/introspection/models/schema.py +10 -0
- pretensor/introspection/models/semantic.py +121 -0
- pretensor/introspection/models/validation.py +116 -0
- pretensor/introspection/snapshot.py +46 -0
- pretensor/mcp/__init__.py +16 -0
- pretensor/mcp/config_json.py +24 -0
- pretensor/mcp/payload_types.py +274 -0
- pretensor/mcp/resources/__init__.py +17 -0
- pretensor/mcp/resources/markdown.py +314 -0
- pretensor/mcp/server.py +285 -0
- pretensor/mcp/service.py +49 -0
- pretensor/mcp/service_context.py +142 -0
- pretensor/mcp/service_registry.py +294 -0
- pretensor/mcp/store_cache.py +43 -0
- pretensor/mcp/tool_registry.py +136 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/_rank.py +244 -0
- pretensor/mcp/tools/_timed.py +26 -0
- pretensor/mcp/tools/compile_metric.py +144 -0
- pretensor/mcp/tools/consumers.py +161 -0
- pretensor/mcp/tools/context.py +1121 -0
- pretensor/mcp/tools/cypher.py +509 -0
- pretensor/mcp/tools/detect_changes.py +254 -0
- pretensor/mcp/tools/impact.py +271 -0
- pretensor/mcp/tools/list.py +131 -0
- pretensor/mcp/tools/schema.py +170 -0
- pretensor/mcp/tools/search.py +316 -0
- pretensor/mcp/tools/semantic_search.py +282 -0
- pretensor/mcp/tools/traverse.py +1027 -0
- pretensor/mcp/tools/validate_sql.py +150 -0
- pretensor/observability.py +203 -0
- pretensor/py.typed +0 -0
- pretensor/quickstart/README.md +29 -0
- pretensor/quickstart/__init__.py +6 -0
- pretensor/quickstart/docker-compose.yml +18 -0
- pretensor/quickstart/pagila_data.sql +63 -0
- pretensor/quickstart/pagila_ddl.sql +92 -0
- pretensor/search/__init__.py +6 -0
- pretensor/search/base.py +80 -0
- pretensor/search/index.py +435 -0
- pretensor/semantic/__init__.py +24 -0
- pretensor/semantic/base.py +123 -0
- pretensor/semantic/compiler.py +487 -0
- pretensor/semantic/yaml_layer.py +180 -0
- pretensor/skills/__init__.py +5 -0
- pretensor/skills/generator.py +235 -0
- pretensor/staleness/__init__.py +15 -0
- pretensor/staleness/graph_patcher.py +355 -0
- pretensor/staleness/impact_analyzer.py +162 -0
- pretensor/staleness/snapshot_store.py +38 -0
- pretensor/validation/__init__.py +9 -0
- pretensor/validation/query_validator.py +436 -0
- pretensor/visibility/__init__.py +23 -0
- pretensor/visibility/config.py +126 -0
- pretensor/visibility/filter.py +143 -0
- pretensor/visibility/kuzu_helpers.py +32 -0
- pretensor/visibility/runtime.py +36 -0
- pretensor/visibility/sync_grants.py +188 -0
- pretensor-0.1.0.dist-info/METADATA +251 -0
- pretensor-0.1.0.dist-info/RECORD +198 -0
- pretensor-0.1.0.dist-info/WHEEL +4 -0
- pretensor-0.1.0.dist-info/entry_points.txt +2 -0
- pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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)
|