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,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
from ruamel.yaml import YAML
|
|
9
|
+
|
|
10
|
+
from .base import PretensorModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AttributeRole(StrEnum):
|
|
14
|
+
IDENTIFIER = "identifier"
|
|
15
|
+
DIMENSION = "dimension"
|
|
16
|
+
MEASURE = "measure"
|
|
17
|
+
TIME_DIMENSION = "time_dimension"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MetricType(StrEnum):
|
|
21
|
+
COUNT = "count"
|
|
22
|
+
COUNT_DISTINCT = "count_distinct"
|
|
23
|
+
SUM = "sum"
|
|
24
|
+
AVERAGE = "average"
|
|
25
|
+
DERIVED = "derived"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FormatConfig(PretensorModel):
|
|
29
|
+
"""Per-attribute or per-metric format override."""
|
|
30
|
+
|
|
31
|
+
format_type: Literal["number", "currency", "percentage", "date"] | None = None
|
|
32
|
+
decimals: int | None = None
|
|
33
|
+
symbol: str | None = None
|
|
34
|
+
position: Literal["prefix", "suffix"] | None = None
|
|
35
|
+
suffix: str | None = None
|
|
36
|
+
thousands_separator: str | None = None
|
|
37
|
+
format_string: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Attribute(PretensorModel):
|
|
41
|
+
name: str
|
|
42
|
+
description: str
|
|
43
|
+
role: AttributeRole
|
|
44
|
+
source_column: str
|
|
45
|
+
format_config: FormatConfig | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Metric(PretensorModel):
|
|
49
|
+
name: str
|
|
50
|
+
description: str
|
|
51
|
+
type: MetricType
|
|
52
|
+
field: str | None = None
|
|
53
|
+
expression: str | None = None
|
|
54
|
+
time_dimension: str | None = None
|
|
55
|
+
granularity: str | None = None
|
|
56
|
+
format_config: FormatConfig | None = None
|
|
57
|
+
filters: list[str] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Entity(PretensorModel):
|
|
61
|
+
name: str
|
|
62
|
+
description: str
|
|
63
|
+
source_table: str
|
|
64
|
+
attributes: list[Attribute] = Field(default_factory=list)
|
|
65
|
+
metrics: list[Metric] = Field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Domain(PretensorModel):
|
|
69
|
+
name: str
|
|
70
|
+
description: str
|
|
71
|
+
entities: list[Entity] = Field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NumberFormatDefaults(PretensorModel):
|
|
75
|
+
decimals: int = 0
|
|
76
|
+
thousands_separator: str = ","
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CurrencyFormatDefaults(PretensorModel):
|
|
80
|
+
symbol: str = "$"
|
|
81
|
+
position: Literal["prefix", "suffix"] = "prefix"
|
|
82
|
+
decimals: int = 2
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PercentageFormatDefaults(PretensorModel):
|
|
86
|
+
decimals: int = 1
|
|
87
|
+
suffix: str = "%"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class DateFormatDefaults(PretensorModel):
|
|
91
|
+
format_string: str = "YYYY-MM-DD"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FormatDefaults(PretensorModel):
|
|
95
|
+
number: NumberFormatDefaults = Field(default_factory=NumberFormatDefaults)
|
|
96
|
+
currency: CurrencyFormatDefaults = Field(default_factory=CurrencyFormatDefaults)
|
|
97
|
+
percentage: PercentageFormatDefaults = Field(
|
|
98
|
+
default_factory=PercentageFormatDefaults
|
|
99
|
+
)
|
|
100
|
+
date: DateFormatDefaults = Field(default_factory=DateFormatDefaults)
|
|
101
|
+
chart_preferences: dict[str, str] = Field(default_factory=dict)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SemanticLayer(PretensorModel):
|
|
105
|
+
connection_name: str
|
|
106
|
+
domains: list[Domain] = Field(default_factory=list)
|
|
107
|
+
format_defaults: FormatDefaults = Field(default_factory=FormatDefaults)
|
|
108
|
+
|
|
109
|
+
def to_yaml(self) -> str:
|
|
110
|
+
data = self.model_dump(mode="json")
|
|
111
|
+
yaml = YAML()
|
|
112
|
+
yaml.default_flow_style = False
|
|
113
|
+
buf = StringIO()
|
|
114
|
+
yaml.dump(data, buf)
|
|
115
|
+
return buf.getvalue()
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_yaml(cls, yaml_str: str) -> SemanticLayer:
|
|
119
|
+
yaml = YAML()
|
|
120
|
+
data = yaml.load(yaml_str)
|
|
121
|
+
return cls.model_validate(data)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Validation report models — shared across all validation layers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from .base import PretensorModel
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Severity",
|
|
14
|
+
"RepairState",
|
|
15
|
+
"ValidationIssue",
|
|
16
|
+
"ComponentScore",
|
|
17
|
+
"LayerResult",
|
|
18
|
+
"AnalyticalQualityResult",
|
|
19
|
+
"ValidationReport",
|
|
20
|
+
"LayerStatus",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
LayerStatus = Literal["pass", "fail", "repaired", "not_evaluated"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Severity(StrEnum):
|
|
27
|
+
ERROR = "error"
|
|
28
|
+
WARNING = "warning"
|
|
29
|
+
INFO = "info"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RepairState(StrEnum):
|
|
33
|
+
PASS = "pass"
|
|
34
|
+
FAIL = "fail"
|
|
35
|
+
REPAIRED = "repaired"
|
|
36
|
+
NOT_EVALUATED = "not_evaluated"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ValidationIssue(PretensorModel):
|
|
40
|
+
model_config = PretensorModel.model_config.copy()
|
|
41
|
+
model_config["frozen"] = False
|
|
42
|
+
|
|
43
|
+
severity: Severity
|
|
44
|
+
layer: str
|
|
45
|
+
path: str
|
|
46
|
+
message: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LayerResult(PretensorModel):
|
|
50
|
+
model_config = PretensorModel.model_config.copy()
|
|
51
|
+
model_config["frozen"] = False
|
|
52
|
+
|
|
53
|
+
layer_name: str
|
|
54
|
+
status: LayerStatus
|
|
55
|
+
repair_state: RepairState = RepairState.PASS
|
|
56
|
+
issues: list[ValidationIssue] = Field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def error_count(self) -> int:
|
|
60
|
+
return sum(1 for i in self.issues if i.severity == Severity.ERROR)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def warning_count(self) -> int:
|
|
64
|
+
return sum(1 for i in self.issues if i.severity == Severity.WARNING)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ComponentScore(PretensorModel):
|
|
68
|
+
"""Per-chart quality score produced by the L4 analyst review."""
|
|
69
|
+
|
|
70
|
+
model_config = PretensorModel.model_config.copy()
|
|
71
|
+
model_config["frozen"] = False
|
|
72
|
+
|
|
73
|
+
score: int
|
|
74
|
+
answers_main_question: bool
|
|
75
|
+
priority_rank: int
|
|
76
|
+
current_position: int
|
|
77
|
+
note: str
|
|
78
|
+
subtracting: str | None = None
|
|
79
|
+
suggested_action: str | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AnalyticalQualityResult(LayerResult):
|
|
83
|
+
"""Extended LayerResult for L4, carrying per-chart ComponentScore breakdown."""
|
|
84
|
+
|
|
85
|
+
model_config = LayerResult.model_config.copy()
|
|
86
|
+
|
|
87
|
+
overall_score: int = 0
|
|
88
|
+
overall_summary: str = ""
|
|
89
|
+
question_answered: bool = False
|
|
90
|
+
components: dict[str, ComponentScore] = Field(default_factory=dict)
|
|
91
|
+
repair_tier: int = 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ValidationReport(PretensorModel):
|
|
95
|
+
model_config = PretensorModel.model_config.copy()
|
|
96
|
+
model_config["frozen"] = False
|
|
97
|
+
|
|
98
|
+
dashboard_name: str
|
|
99
|
+
layers: list[LayerResult] = Field(default_factory=list)
|
|
100
|
+
repair_actions_taken: list[str] = Field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def passed(self) -> bool:
|
|
104
|
+
return all(r.status == "pass" for r in self.layers)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def all_issues(self) -> list[ValidationIssue]:
|
|
108
|
+
return [issue for layer in self.layers for issue in layer.issues]
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def error_count(self) -> int:
|
|
112
|
+
return sum(r.error_count for r in self.layers)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def warning_count(self) -> int:
|
|
116
|
+
return sum(r.warning_count for r in self.layers)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Snapshot diff — re-exported from ``pretensor.connectors.snapshot``."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pretensor.connectors.models import SchemaSnapshot
|
|
6
|
+
from pretensor.connectors.snapshot import (
|
|
7
|
+
ChangeTarget,
|
|
8
|
+
ChangeType,
|
|
9
|
+
SchemaChange,
|
|
10
|
+
diff_snapshots,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def save_snapshot(snapshot: SchemaSnapshot, base_path: Path) -> Path:
|
|
15
|
+
"""Serialize a snapshot to YAML and write it to disk.
|
|
16
|
+
|
|
17
|
+
Writes to ``{base_path}/{connection_name}.yaml``.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The path to the written file.
|
|
21
|
+
"""
|
|
22
|
+
base_path.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
file_path = base_path / f"{snapshot.connection_name}.yaml"
|
|
24
|
+
file_path.write_text(snapshot.to_yaml())
|
|
25
|
+
return file_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_snapshot(path: Path) -> SchemaSnapshot:
|
|
29
|
+
"""Load a schema snapshot from a YAML file.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
FileNotFoundError: If the file does not exist.
|
|
33
|
+
"""
|
|
34
|
+
if not path.exists():
|
|
35
|
+
raise FileNotFoundError(f"Snapshot file not found: {path}")
|
|
36
|
+
return SchemaSnapshot.from_yaml(path.read_text())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ChangeTarget",
|
|
41
|
+
"ChangeType",
|
|
42
|
+
"SchemaChange",
|
|
43
|
+
"diff_snapshots",
|
|
44
|
+
"load_snapshot",
|
|
45
|
+
"save_snapshot",
|
|
46
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""MCP server and service helpers for Pretensor Graph."""
|
|
2
|
+
|
|
3
|
+
from pretensor.mcp.server import (
|
|
4
|
+
create_server,
|
|
5
|
+
print_mcp_config,
|
|
6
|
+
run_server,
|
|
7
|
+
)
|
|
8
|
+
from pretensor.mcp.tool_registry import McpTool, McpToolRegistry
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"create_server",
|
|
12
|
+
"print_mcp_config",
|
|
13
|
+
"run_server",
|
|
14
|
+
"McpTool",
|
|
15
|
+
"McpToolRegistry",
|
|
16
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""MCP client config snippet (Cursor / Claude Desktop)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def mcp_config_json(graph_dir: Path) -> str:
|
|
10
|
+
"""Claude Desktop / Cursor style ``mcpServers`` block for stdio transport."""
|
|
11
|
+
cmd = "pretensor"
|
|
12
|
+
graph_dir_resolved = str(graph_dir.resolve())
|
|
13
|
+
block = {
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"pretensor": {
|
|
16
|
+
"command": cmd,
|
|
17
|
+
"args": ["serve", "--state-dir", graph_dir_resolved],
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return json.dumps(block, indent=2)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["mcp_config_json"]
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Typed payloads and small helpers for MCP tool responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, TypedDict
|
|
7
|
+
|
|
8
|
+
from pretensor.config import GraphConfig
|
|
9
|
+
from pretensor.mcp.service_context import get_effective_graph_config
|
|
10
|
+
|
|
11
|
+
DB_TYPE_POSTGRES = "postgresql"
|
|
12
|
+
|
|
13
|
+
DIALECT_TO_DB_TYPE: dict[str, str] = {
|
|
14
|
+
"postgres": "postgresql",
|
|
15
|
+
"mysql": "mysql",
|
|
16
|
+
"snowflake": "snowflake",
|
|
17
|
+
"bigquery": "bigquery",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatabaseListItem(TypedDict, total=False):
|
|
22
|
+
"""One row for ``list_databases`` and the databases resource."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
db_type: str
|
|
26
|
+
table_count: int
|
|
27
|
+
column_count: int
|
|
28
|
+
row_count: int
|
|
29
|
+
last_indexed: str
|
|
30
|
+
is_stale: bool
|
|
31
|
+
staleness_days: int
|
|
32
|
+
stale_warning: str
|
|
33
|
+
graph_path: str
|
|
34
|
+
database: str
|
|
35
|
+
schemas: list[str]
|
|
36
|
+
# Trinary capability state: "not_attempted" | "empty" | "present".
|
|
37
|
+
# "not_attempted" means the indexer never ran this pass, so absence of signal
|
|
38
|
+
# is not authoritative. "empty" means the pass ran but found nothing.
|
|
39
|
+
has_dbt_manifest: str
|
|
40
|
+
has_llm_enrichment: str
|
|
41
|
+
has_external_consumers: str
|
|
42
|
+
# Unified capability list derived from the trinary flags plus always-on
|
|
43
|
+
# features. Additive convenience over the individual ``has_*`` fields;
|
|
44
|
+
# scan this to decide whether a graph supports a feature at a glance.
|
|
45
|
+
capabilities: list[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class QueryHit(TypedDict, total=False):
|
|
49
|
+
"""One search hit for the ``query`` tool."""
|
|
50
|
+
|
|
51
|
+
node_type: str
|
|
52
|
+
name: str
|
|
53
|
+
database_name: str
|
|
54
|
+
connection_name: str
|
|
55
|
+
description: str
|
|
56
|
+
snippet: str
|
|
57
|
+
score: float
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SemanticHit(TypedDict, total=False):
|
|
61
|
+
"""One search hit for the ``semantic_search`` tool (cosine over ``SchemaTable.embedding``)."""
|
|
62
|
+
|
|
63
|
+
node_type: str
|
|
64
|
+
name: str
|
|
65
|
+
database_name: str
|
|
66
|
+
connection_name: str
|
|
67
|
+
description: str
|
|
68
|
+
snippet: str
|
|
69
|
+
score: float
|
|
70
|
+
cluster_id: str | None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ColumnInfo(TypedDict, total=False):
|
|
74
|
+
"""Column metadata for ``context`` (from ``SchemaColumn`` nodes when present).
|
|
75
|
+
|
|
76
|
+
``column_name`` is the canonical field.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
column_name: str
|
|
80
|
+
data_type: str
|
|
81
|
+
nullable: bool
|
|
82
|
+
is_primary_key: bool
|
|
83
|
+
is_foreign_key: bool
|
|
84
|
+
description: str
|
|
85
|
+
is_indexed: bool
|
|
86
|
+
check_constraints: list[str]
|
|
87
|
+
most_common_values: list[str]
|
|
88
|
+
histogram_bounds: list[str]
|
|
89
|
+
stats_correlation: float
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class RelationshipInfo(TypedDict, total=False):
|
|
93
|
+
"""FK or inferred join from a table to another table.
|
|
94
|
+
|
|
95
|
+
For composite FKs, ``source_columns`` / ``target_columns`` carry the
|
|
96
|
+
ordered column lists and ``constraint_name`` identifies the constraint.
|
|
97
|
+
Single-column FKs still set ``source_column`` / ``target_column`` for
|
|
98
|
+
backward compatibility.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
target_table: str
|
|
102
|
+
rel_type: str
|
|
103
|
+
source_column: str
|
|
104
|
+
target_column: str
|
|
105
|
+
source_columns: list[str]
|
|
106
|
+
target_columns: list[str]
|
|
107
|
+
constraint_name: str | None
|
|
108
|
+
source: str | None
|
|
109
|
+
confidence: float | None
|
|
110
|
+
reasoning: str | None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class LineageRef(TypedDict, total=False):
|
|
114
|
+
"""One table-level LINEAGE hop for ``context``."""
|
|
115
|
+
|
|
116
|
+
table: str
|
|
117
|
+
lineage_type: str
|
|
118
|
+
confidence: float
|
|
119
|
+
source: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ClusterInfo(TypedDict, total=False):
|
|
123
|
+
"""Domain cluster assignment for ``context``."""
|
|
124
|
+
|
|
125
|
+
cluster_id: str
|
|
126
|
+
label: str
|
|
127
|
+
description: str
|
|
128
|
+
cohesion_score: float
|
|
129
|
+
schema_pattern: str
|
|
130
|
+
stale: bool
|
|
131
|
+
stale_warning: str
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SimilarTable(TypedDict, total=False):
|
|
135
|
+
"""One cross-cluster nearest neighbor in ``context.similar_tables``."""
|
|
136
|
+
|
|
137
|
+
table_id: str
|
|
138
|
+
qualified_name: str
|
|
139
|
+
score: float
|
|
140
|
+
cluster_id: str | None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ContextPayload(TypedDict, total=False):
|
|
144
|
+
"""Structured ``context`` tool response."""
|
|
145
|
+
|
|
146
|
+
connection_name: str
|
|
147
|
+
database: str
|
|
148
|
+
schema_name: str
|
|
149
|
+
table_name: str
|
|
150
|
+
table_type: str | None
|
|
151
|
+
qualified_name: str
|
|
152
|
+
description: str
|
|
153
|
+
tags: list[str]
|
|
154
|
+
has_external_consumers: bool
|
|
155
|
+
test_count: int
|
|
156
|
+
staleness_status: str
|
|
157
|
+
staleness_as_of: str
|
|
158
|
+
row_count: int | None
|
|
159
|
+
entity_type: str | None
|
|
160
|
+
entity_name: str | None
|
|
161
|
+
entity_description: str | None
|
|
162
|
+
role: str | None
|
|
163
|
+
role_confidence: float | None
|
|
164
|
+
classification_signals: list[str]
|
|
165
|
+
classification_summary: str
|
|
166
|
+
cluster: ClusterInfo | None
|
|
167
|
+
columns: list[ColumnInfo]
|
|
168
|
+
relationships: list[RelationshipInfo]
|
|
169
|
+
lineage_in: list[LineageRef]
|
|
170
|
+
lineage_out: list[LineageRef]
|
|
171
|
+
lineage_markdown: str
|
|
172
|
+
aliases: list[str]
|
|
173
|
+
shadow_of: str | None
|
|
174
|
+
deprecation_signal: str | None
|
|
175
|
+
staleness_warning: str | None
|
|
176
|
+
detail: str
|
|
177
|
+
usage_stats: dict[str, int]
|
|
178
|
+
partition: dict[str, object]
|
|
179
|
+
grants: list[dict[str, str]]
|
|
180
|
+
access_patterns: dict[str, object]
|
|
181
|
+
similar_tables: list[SimilarTable]
|
|
182
|
+
similar_reason: str
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TraverseStepPayload(TypedDict, total=False):
|
|
186
|
+
"""One hop in ``traverse`` output."""
|
|
187
|
+
|
|
188
|
+
from_table: str
|
|
189
|
+
to_table: str
|
|
190
|
+
from_column: str
|
|
191
|
+
to_column: str
|
|
192
|
+
edge_type: str
|
|
193
|
+
from_db: str
|
|
194
|
+
to_db: str
|
|
195
|
+
via: str
|
|
196
|
+
join_columns: str | None
|
|
197
|
+
constraint_name: str | None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TraversePathPayload(TypedDict, total=False):
|
|
201
|
+
"""One ranked path in ``traverse`` output."""
|
|
202
|
+
|
|
203
|
+
confidence: float
|
|
204
|
+
ambiguous: bool
|
|
205
|
+
semantic_label: str
|
|
206
|
+
steps: list[TraverseStepPayload]
|
|
207
|
+
sql_hint: str
|
|
208
|
+
stale: bool
|
|
209
|
+
stale_warning: str
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ImpactItemPayload(TypedDict, total=False):
|
|
213
|
+
"""Single dependent in ``impact`` output."""
|
|
214
|
+
|
|
215
|
+
type: str
|
|
216
|
+
name: str
|
|
217
|
+
via: str
|
|
218
|
+
confidence: float
|
|
219
|
+
hop: int
|
|
220
|
+
# Additive: external code consumers of this table (empty when none). Old
|
|
221
|
+
# callers that ignore this key are unaffected.
|
|
222
|
+
consumers: list[dict[str, Any]]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def utc_now() -> datetime:
|
|
226
|
+
return datetime.now(timezone.utc)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def iso_format(dt: datetime) -> str:
|
|
230
|
+
if dt.tzinfo is None:
|
|
231
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
232
|
+
return dt.astimezone(timezone.utc).isoformat()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def stale_threshold_days(config: GraphConfig | None = None) -> int:
|
|
236
|
+
return get_effective_graph_config(config).stale_index_warning_days
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def staleness_days(indexed_at: datetime, now: datetime | None = None) -> int:
|
|
240
|
+
reference = now or utc_now()
|
|
241
|
+
if indexed_at.tzinfo is None:
|
|
242
|
+
indexed_at = indexed_at.replace(tzinfo=timezone.utc)
|
|
243
|
+
delta = reference - indexed_at.astimezone(timezone.utc)
|
|
244
|
+
return max(0, int(delta.total_seconds() // 86400))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def snippet(text: str, max_len: int = 200) -> str:
|
|
248
|
+
t = text.strip()
|
|
249
|
+
if len(t) <= max_len:
|
|
250
|
+
return t
|
|
251
|
+
return t[: max_len - 3] + "..."
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
__all__ = [
|
|
255
|
+
"ClusterInfo",
|
|
256
|
+
"ColumnInfo",
|
|
257
|
+
"ContextPayload",
|
|
258
|
+
"DatabaseListItem",
|
|
259
|
+
"ImpactItemPayload",
|
|
260
|
+
"LineageRef",
|
|
261
|
+
"QueryHit",
|
|
262
|
+
"RelationshipInfo",
|
|
263
|
+
"SemanticHit",
|
|
264
|
+
"SimilarTable",
|
|
265
|
+
"TraversePathPayload",
|
|
266
|
+
"TraverseStepPayload",
|
|
267
|
+
"DB_TYPE_POSTGRES",
|
|
268
|
+
"DIALECT_TO_DB_TYPE",
|
|
269
|
+
"iso_format",
|
|
270
|
+
"snippet",
|
|
271
|
+
"staleness_days",
|
|
272
|
+
"stale_threshold_days",
|
|
273
|
+
"utc_now",
|
|
274
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""MCP resource payloads (markdown)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pretensor.mcp.resources.markdown import (
|
|
6
|
+
clusters_resource_markdown,
|
|
7
|
+
databases_resource_markdown,
|
|
8
|
+
db_overview_resource_markdown,
|
|
9
|
+
metrics_resource_markdown,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"clusters_resource_markdown",
|
|
14
|
+
"databases_resource_markdown",
|
|
15
|
+
"db_overview_resource_markdown",
|
|
16
|
+
"metrics_resource_markdown",
|
|
17
|
+
]
|