interloper-toolkit 0.43.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.
- interloper_toolkit/__init__.py +14 -0
- interloper_toolkit/analytics.py +153 -0
- interloper_toolkit/catalog.py +303 -0
- interloper_toolkit/collection.py +60 -0
- interloper_toolkit/context.py +55 -0
- interloper_toolkit/lineage.py +241 -0
- interloper_toolkit/models.py +386 -0
- interloper_toolkit/scheduling.py +172 -0
- interloper_toolkit-0.43.0.dist-info/METADATA +40 -0
- interloper_toolkit-0.43.0.dist-info/RECORD +11 -0
- interloper_toolkit-0.43.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Lineage tools — dependency analysis, impact assessment, and DAG traversal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from interloper_toolkit.context import ToolkitContext
|
|
9
|
+
from interloper_toolkit.models import (
|
|
10
|
+
AssetRef,
|
|
11
|
+
CrossSourceDependencies,
|
|
12
|
+
CrossSourceEdge,
|
|
13
|
+
DependencyEdge,
|
|
14
|
+
DownstreamResult,
|
|
15
|
+
ImpactAnalysis,
|
|
16
|
+
LineageItem,
|
|
17
|
+
LineageResult,
|
|
18
|
+
ToolError,
|
|
19
|
+
UpstreamResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_upstream(ctx: ToolkitContext, asset_id: str) -> UpstreamResult | ToolError:
|
|
24
|
+
"""Get the direct upstream dependencies of an asset.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
asset_id: UUID of the asset to inspect.
|
|
28
|
+
|
|
29
|
+
Returns the list of upstream assets that this asset depends on,
|
|
30
|
+
including the parameter name used for each dependency.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
deps = ctx.store.list_relations(ctx.org_id, type="dependency")
|
|
34
|
+
target = UUID(asset_id)
|
|
35
|
+
|
|
36
|
+
upstream = []
|
|
37
|
+
for dep in deps:
|
|
38
|
+
if dep.src_id == target:
|
|
39
|
+
asset = ctx.store.get_component(dep.dst_id, kind="asset")
|
|
40
|
+
upstream.append(DependencyEdge(
|
|
41
|
+
asset_id=str(dep.dst_id),
|
|
42
|
+
param_name=dep.slot,
|
|
43
|
+
asset_key=asset.key,
|
|
44
|
+
source_id=str(asset.parent_id),
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
return UpstreamResult(asset_id=asset_id, upstream=upstream)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
return ToolError(error=str(e))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_downstream(ctx: ToolkitContext, asset_id: str) -> DownstreamResult | ToolError:
|
|
53
|
+
"""Get the direct downstream dependents of an asset.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
asset_id: UUID of the asset to inspect.
|
|
57
|
+
|
|
58
|
+
Returns the list of assets that directly depend on this asset.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
deps = ctx.store.list_relations(ctx.org_id, type="dependency")
|
|
62
|
+
target = UUID(asset_id)
|
|
63
|
+
|
|
64
|
+
downstream = []
|
|
65
|
+
for dep in deps:
|
|
66
|
+
if dep.dst_id == target:
|
|
67
|
+
asset = ctx.store.get_component(dep.src_id, kind="asset")
|
|
68
|
+
downstream.append(DependencyEdge(
|
|
69
|
+
asset_id=str(dep.src_id),
|
|
70
|
+
param_name=dep.slot,
|
|
71
|
+
asset_key=asset.key,
|
|
72
|
+
source_id=str(asset.parent_id),
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
return DownstreamResult(asset_id=asset_id, downstream=downstream)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return ToolError(error=str(e))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_full_lineage(ctx: ToolkitContext, asset_id: str, direction: str = "upstream") -> LineageResult | ToolError:
|
|
81
|
+
"""Recursively traverse the full lineage of an asset.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
asset_id: UUID of the asset to start from.
|
|
85
|
+
direction: Either 'upstream' (ancestors) or 'downstream' (dependents).
|
|
86
|
+
|
|
87
|
+
Returns an ordered list of assets in the lineage chain with depth levels.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
adj, asset_info = _build_adjacency(ctx, direction)
|
|
91
|
+
target = UUID(asset_id)
|
|
92
|
+
|
|
93
|
+
visited: set[UUID] = set()
|
|
94
|
+
result: list[LineageItem] = []
|
|
95
|
+
queue: list[tuple[UUID, int]] = [(target, 0)]
|
|
96
|
+
|
|
97
|
+
while queue:
|
|
98
|
+
current, depth = queue.pop(0)
|
|
99
|
+
if current in visited:
|
|
100
|
+
continue
|
|
101
|
+
visited.add(current)
|
|
102
|
+
if current != target:
|
|
103
|
+
info = asset_info.get(current, {})
|
|
104
|
+
result.append(LineageItem(asset_id=str(current), depth=depth, **info))
|
|
105
|
+
for neighbor in adj.get(current, []):
|
|
106
|
+
if neighbor not in visited:
|
|
107
|
+
queue.append((neighbor, depth + 1))
|
|
108
|
+
|
|
109
|
+
return LineageResult(
|
|
110
|
+
asset_id=asset_id,
|
|
111
|
+
direction=direction,
|
|
112
|
+
lineage_count=len(result),
|
|
113
|
+
lineage=result,
|
|
114
|
+
)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return ToolError(error=str(e))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def impact_analysis(ctx: ToolkitContext, asset_id: str) -> ImpactAnalysis | ToolError:
|
|
120
|
+
"""Analyze the downstream impact if an asset fails or is disabled.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
asset_id: UUID of the asset to analyze.
|
|
124
|
+
|
|
125
|
+
Returns all downstream assets grouped by source, with total affected count.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
adj, asset_info = _build_adjacency(ctx, "downstream")
|
|
129
|
+
target = UUID(asset_id)
|
|
130
|
+
|
|
131
|
+
visited: set[UUID] = set()
|
|
132
|
+
affected: list[LineageItem] = []
|
|
133
|
+
queue: list[tuple[UUID, int]] = [(target, 0)]
|
|
134
|
+
|
|
135
|
+
while queue:
|
|
136
|
+
current, depth = queue.pop(0)
|
|
137
|
+
if current in visited:
|
|
138
|
+
continue
|
|
139
|
+
visited.add(current)
|
|
140
|
+
if current != target:
|
|
141
|
+
info = asset_info.get(current, {})
|
|
142
|
+
affected.append(LineageItem(asset_id=str(current), depth=depth, **info))
|
|
143
|
+
for neighbor in adj.get(current, []):
|
|
144
|
+
if neighbor not in visited:
|
|
145
|
+
queue.append((neighbor, depth + 1))
|
|
146
|
+
|
|
147
|
+
# Group by source
|
|
148
|
+
by_source: dict[str, list[LineageItem]] = defaultdict(list)
|
|
149
|
+
for item in affected:
|
|
150
|
+
by_source[item.source_key or "unknown"].append(item)
|
|
151
|
+
|
|
152
|
+
return ImpactAnalysis(
|
|
153
|
+
asset_id=asset_id,
|
|
154
|
+
total_affected=len(affected),
|
|
155
|
+
by_source=dict(by_source),
|
|
156
|
+
)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
return ToolError(error=str(e))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cross_source_dependencies(ctx: ToolkitContext) -> CrossSourceDependencies | ToolError:
|
|
162
|
+
"""List all dependency edges that cross source boundaries.
|
|
163
|
+
|
|
164
|
+
Returns edges where the upstream and downstream assets belong to
|
|
165
|
+
different sources.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
deps = ctx.store.list_relations(ctx.org_id, type="dependency")
|
|
169
|
+
assets = ctx.store.list_components(ctx.org_id, kinds=["asset"])
|
|
170
|
+
|
|
171
|
+
asset_source: dict[UUID, UUID | None] = {}
|
|
172
|
+
asset_info: dict[UUID, AssetRef] = {}
|
|
173
|
+
for a in assets:
|
|
174
|
+
if not a.id:
|
|
175
|
+
continue
|
|
176
|
+
asset_source[a.id] = a.parent_id
|
|
177
|
+
asset_info[a.id] = AssetRef(asset_key=a.key, source_id=str(a.parent_id) if a.parent_id else "")
|
|
178
|
+
|
|
179
|
+
cross_deps = []
|
|
180
|
+
for dep in deps:
|
|
181
|
+
src_down = asset_source.get(dep.src_id)
|
|
182
|
+
src_up = asset_source.get(dep.dst_id)
|
|
183
|
+
if src_down and src_up and src_down != src_up:
|
|
184
|
+
cross_deps.append(CrossSourceEdge(
|
|
185
|
+
downstream_asset_id=str(dep.src_id),
|
|
186
|
+
downstream=asset_info.get(dep.src_id, AssetRef()),
|
|
187
|
+
upstream_asset_id=str(dep.dst_id),
|
|
188
|
+
upstream=asset_info.get(dep.dst_id, AssetRef()),
|
|
189
|
+
param_name=dep.slot,
|
|
190
|
+
))
|
|
191
|
+
|
|
192
|
+
return CrossSourceDependencies(cross_source_count=len(cross_deps), dependencies=cross_deps)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return ToolError(error=str(e))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# -- Helpers -------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _build_adjacency(
|
|
201
|
+
ctx: ToolkitContext,
|
|
202
|
+
direction: str,
|
|
203
|
+
) -> tuple[dict[UUID, list[UUID]], dict[UUID, dict[str, str]]]:
|
|
204
|
+
"""Build an adjacency map and asset info lookup from all dependencies.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
ctx: The toolkit context.
|
|
208
|
+
direction: 'upstream' builds child→parents; 'downstream' builds parent→children.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
``(adjacency_map, asset_info_map)`` — info values feed
|
|
212
|
+
:class:`LineageItem` kwargs (asset_key, source_id, source_key).
|
|
213
|
+
"""
|
|
214
|
+
deps = ctx.store.list_relations(ctx.org_id, type="dependency")
|
|
215
|
+
assets = ctx.store.list_components(ctx.org_id, kinds=["asset"])
|
|
216
|
+
|
|
217
|
+
asset_info: dict[UUID, dict[str, str]] = {}
|
|
218
|
+
source_keys: dict[UUID, str] = {}
|
|
219
|
+
for a in assets:
|
|
220
|
+
if not a.id:
|
|
221
|
+
continue
|
|
222
|
+
asset_info[a.id] = {"asset_key": a.key, "source_id": str(a.parent_id) if a.parent_id else ""}
|
|
223
|
+
if a.parent and a.parent_id:
|
|
224
|
+
source_keys[a.parent_id] = a.parent.key
|
|
225
|
+
|
|
226
|
+
# Add source_key to asset_info
|
|
227
|
+
for uid, info in asset_info.items():
|
|
228
|
+
sid_str = info["source_id"]
|
|
229
|
+
if sid_str:
|
|
230
|
+
info["source_key"] = source_keys.get(UUID(sid_str), "unknown")
|
|
231
|
+
else:
|
|
232
|
+
info["source_key"] = ""
|
|
233
|
+
|
|
234
|
+
adj: dict[UUID, list[UUID]] = defaultdict(list)
|
|
235
|
+
for dep in deps:
|
|
236
|
+
if direction == "upstream":
|
|
237
|
+
adj[dep.src_id].append(dep.dst_id)
|
|
238
|
+
else:
|
|
239
|
+
adj[dep.dst_id].append(dep.src_id)
|
|
240
|
+
|
|
241
|
+
return adj, asset_info
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Typed result models for the toolkit surface.
|
|
2
|
+
|
|
3
|
+
Every toolkit function returns ``<SuccessModel> | ToolError`` — a union
|
|
4
|
+
discriminated by the literal ``status`` field, preserving the
|
|
5
|
+
``{"status": "success" | "error"}`` envelope both AI surfaces already
|
|
6
|
+
speak. The models are the tool contract: MCP derives per-tool output
|
|
7
|
+
schemas from them, and the fields of row-projecting models act as an
|
|
8
|
+
allowlist of what leaves the platform.
|
|
9
|
+
|
|
10
|
+
Row-shaped payloads deliberately embed the interloper-db models (``Run``,
|
|
11
|
+
``Backfill``, ``Event``, ``Component`` — already pydantic via SQLModel)
|
|
12
|
+
rather than duplicating their shape: the full-row contract predates this
|
|
13
|
+
module, and embedding makes the coupling visible in the signature. The one
|
|
14
|
+
exception is :class:`ComponentSummary`, where projecting is the point —
|
|
15
|
+
sensitive kinds must never expose config or credential material, so the
|
|
16
|
+
model's fields fail closed instead of relying on conditional key insertion.
|
|
17
|
+
|
|
18
|
+
Catalog definition payloads stay ``dict[str, Any]``: their content is
|
|
19
|
+
definition-specific JSON schema material with no fixed shape to type.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from typing import Any, Literal
|
|
26
|
+
|
|
27
|
+
from interloper_db.models import AssetExecution, Backfill, Component, Event, Run
|
|
28
|
+
from pydantic import BaseModel
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ToolError(BaseModel):
|
|
32
|
+
"""A failed tool call, as a structured result rather than an exception."""
|
|
33
|
+
|
|
34
|
+
status: Literal["error"] = "error"
|
|
35
|
+
error: str
|
|
36
|
+
valid_kinds: list[str] | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# -- Catalog --------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DefinitionCounts(BaseModel):
|
|
43
|
+
"""Per-kind definition counts (the kind-less ``list_definitions`` call)."""
|
|
44
|
+
|
|
45
|
+
status: Literal["success"] = "success"
|
|
46
|
+
definition_counts: dict[str, int]
|
|
47
|
+
message: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DefinitionEntry(BaseModel):
|
|
51
|
+
"""One catalog definition, summarized for listing.
|
|
52
|
+
|
|
53
|
+
The trailing optional fields are kind-specific: ``asset_count`` /
|
|
54
|
+
``in_collection`` / ``collection_count`` are populated for sources,
|
|
55
|
+
``provider`` / ``required_fields`` / ``oauth`` / ``oauth_available``
|
|
56
|
+
for connections.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
key: str
|
|
60
|
+
name: str
|
|
61
|
+
description: str | None = None
|
|
62
|
+
icon: str | None = None
|
|
63
|
+
tags: list[str] = []
|
|
64
|
+
asset_count: int | None = None
|
|
65
|
+
in_collection: bool | None = None
|
|
66
|
+
collection_count: int | None = None
|
|
67
|
+
provider: str | None = None
|
|
68
|
+
required_fields: list[str] | None = None
|
|
69
|
+
oauth: bool | None = None
|
|
70
|
+
oauth_available: bool | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DefinitionList(BaseModel):
|
|
74
|
+
"""Catalog definitions of one kind."""
|
|
75
|
+
|
|
76
|
+
status: Literal["success"] = "success"
|
|
77
|
+
kind: str
|
|
78
|
+
count: int
|
|
79
|
+
definitions: list[DefinitionEntry]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DefinitionDetail(BaseModel):
|
|
83
|
+
"""One definition's full catalog payload (definition-specific shape)."""
|
|
84
|
+
|
|
85
|
+
status: Literal["success"] = "success"
|
|
86
|
+
definition: dict[str, Any]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AssetSchemaResult(BaseModel):
|
|
90
|
+
"""The JSON schema and metadata of one asset within a source."""
|
|
91
|
+
|
|
92
|
+
status: Literal["success"] = "success"
|
|
93
|
+
source_key: str
|
|
94
|
+
asset_key: str
|
|
95
|
+
qualified_key: str
|
|
96
|
+
asset_schema: dict[str, Any] | None = None
|
|
97
|
+
partitioning: Any = None
|
|
98
|
+
tags: list[str] = []
|
|
99
|
+
requires: dict[str, Any] = {}
|
|
100
|
+
optional_requires: dict[str, Any] = {}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class FieldMatch(BaseModel):
|
|
104
|
+
"""One schema field matching a search query."""
|
|
105
|
+
|
|
106
|
+
source_key: str
|
|
107
|
+
asset_key: str
|
|
108
|
+
qualified_key: str
|
|
109
|
+
field_name: str
|
|
110
|
+
field_type: str
|
|
111
|
+
description: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class FieldSearchResult(BaseModel):
|
|
115
|
+
"""Fields across all asset schemas matching a query."""
|
|
116
|
+
|
|
117
|
+
status: Literal["success"] = "success"
|
|
118
|
+
query: str
|
|
119
|
+
match_count: int
|
|
120
|
+
matches: list[FieldMatch]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class SharedField(BaseModel):
|
|
124
|
+
"""A field present in both compared schemas, with type agreement."""
|
|
125
|
+
|
|
126
|
+
field: str
|
|
127
|
+
type_a: str
|
|
128
|
+
type_b: str
|
|
129
|
+
type_match: bool
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class SchemaComparison(BaseModel):
|
|
133
|
+
"""Side-by-side comparison of two asset schemas."""
|
|
134
|
+
|
|
135
|
+
status: Literal["success"] = "success"
|
|
136
|
+
asset_a: str
|
|
137
|
+
asset_b: str
|
|
138
|
+
shared_count: int
|
|
139
|
+
only_a_count: int
|
|
140
|
+
only_b_count: int
|
|
141
|
+
shared_fields: list[SharedField]
|
|
142
|
+
only_in_a: list[str]
|
|
143
|
+
only_in_b: list[str]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# -- Collection -----------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ComponentCounts(BaseModel):
|
|
150
|
+
"""Per-kind component counts (the kind-less ``list_components`` call)."""
|
|
151
|
+
|
|
152
|
+
status: Literal["success"] = "success"
|
|
153
|
+
component_counts: dict[str, int]
|
|
154
|
+
message: str
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ComponentSummary(BaseModel):
|
|
158
|
+
"""A component instance, projected for listing.
|
|
159
|
+
|
|
160
|
+
Deliberately not the db row: these fields are an allowlist. ``config``
|
|
161
|
+
is only populated for non-sensitive kinds; credential-bearing kinds
|
|
162
|
+
surface identity and metadata alone.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
id: str
|
|
166
|
+
key: str
|
|
167
|
+
name: str | None = None
|
|
168
|
+
type_name: str
|
|
169
|
+
created_at: datetime | None = None
|
|
170
|
+
config: dict[str, Any] | None = None
|
|
171
|
+
asset_count: int | None = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ComponentList(BaseModel):
|
|
175
|
+
"""The org's component instances of one kind."""
|
|
176
|
+
|
|
177
|
+
status: Literal["success"] = "success"
|
|
178
|
+
kind: str
|
|
179
|
+
count: int
|
|
180
|
+
components: list[ComponentSummary]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# -- Lineage --------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class DependencyEdge(BaseModel):
|
|
187
|
+
"""A direct dependency edge from the perspective of one asset."""
|
|
188
|
+
|
|
189
|
+
asset_id: str
|
|
190
|
+
param_name: str
|
|
191
|
+
asset_key: str
|
|
192
|
+
source_id: str
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class UpstreamResult(BaseModel):
|
|
196
|
+
"""Direct upstream dependencies of an asset."""
|
|
197
|
+
|
|
198
|
+
status: Literal["success"] = "success"
|
|
199
|
+
asset_id: str
|
|
200
|
+
upstream: list[DependencyEdge]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class DownstreamResult(BaseModel):
|
|
204
|
+
"""Direct downstream dependents of an asset."""
|
|
205
|
+
|
|
206
|
+
status: Literal["success"] = "success"
|
|
207
|
+
asset_id: str
|
|
208
|
+
downstream: list[DependencyEdge]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class LineageItem(BaseModel):
|
|
212
|
+
"""One asset in a lineage traversal, with its BFS depth."""
|
|
213
|
+
|
|
214
|
+
asset_id: str
|
|
215
|
+
depth: int
|
|
216
|
+
asset_key: str | None = None
|
|
217
|
+
source_id: str | None = None
|
|
218
|
+
source_key: str | None = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class LineageResult(BaseModel):
|
|
222
|
+
"""The full recursive lineage of an asset in one direction."""
|
|
223
|
+
|
|
224
|
+
status: Literal["success"] = "success"
|
|
225
|
+
asset_id: str
|
|
226
|
+
direction: str
|
|
227
|
+
lineage_count: int
|
|
228
|
+
lineage: list[LineageItem]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ImpactAnalysis(BaseModel):
|
|
232
|
+
"""All downstream assets affected by a failure, grouped by source."""
|
|
233
|
+
|
|
234
|
+
status: Literal["success"] = "success"
|
|
235
|
+
asset_id: str
|
|
236
|
+
total_affected: int
|
|
237
|
+
by_source: dict[str, list[LineageItem]]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class AssetRef(BaseModel):
|
|
241
|
+
"""A minimal asset reference on a cross-source edge."""
|
|
242
|
+
|
|
243
|
+
asset_key: str | None = None
|
|
244
|
+
source_id: str | None = None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class CrossSourceEdge(BaseModel):
|
|
248
|
+
"""A dependency edge whose endpoints belong to different sources."""
|
|
249
|
+
|
|
250
|
+
downstream_asset_id: str
|
|
251
|
+
downstream: AssetRef
|
|
252
|
+
upstream_asset_id: str
|
|
253
|
+
upstream: AssetRef
|
|
254
|
+
param_name: str
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class CrossSourceDependencies(BaseModel):
|
|
258
|
+
"""All dependency edges crossing source boundaries."""
|
|
259
|
+
|
|
260
|
+
status: Literal["success"] = "success"
|
|
261
|
+
cross_source_count: int
|
|
262
|
+
dependencies: list[CrossSourceEdge]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# -- Scheduling -----------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class JobList(BaseModel):
|
|
269
|
+
"""The org's scheduled jobs (full component rows)."""
|
|
270
|
+
|
|
271
|
+
status: Literal["success"] = "success"
|
|
272
|
+
count: int
|
|
273
|
+
jobs: list[Component]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class JobHealthStats(BaseModel):
|
|
277
|
+
"""Success/failure statistics over a job's recent runs."""
|
|
278
|
+
|
|
279
|
+
total_recent_runs: int
|
|
280
|
+
success_count: int
|
|
281
|
+
failed_count: int
|
|
282
|
+
success_rate: float | None = None
|
|
283
|
+
avg_duration_seconds: float | None = None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class JobHealth(BaseModel):
|
|
287
|
+
"""A job's metadata plus health computed from its last runs."""
|
|
288
|
+
|
|
289
|
+
status: Literal["success"] = "success"
|
|
290
|
+
job: Component
|
|
291
|
+
health: JobHealthStats
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class RunList(BaseModel):
|
|
295
|
+
"""Recent runs matching the filters."""
|
|
296
|
+
|
|
297
|
+
status: Literal["success"] = "success"
|
|
298
|
+
count: int
|
|
299
|
+
runs: list[Run]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class RunDetail(BaseModel):
|
|
303
|
+
"""One run with its event timeline and per-asset execution summary."""
|
|
304
|
+
|
|
305
|
+
status: Literal["success"] = "success"
|
|
306
|
+
run: Run
|
|
307
|
+
events: list[Event]
|
|
308
|
+
asset_executions: list[AssetExecution]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class RunErrorEvent(BaseModel):
|
|
312
|
+
"""One error event of a failed run."""
|
|
313
|
+
|
|
314
|
+
asset_key: str | None = None
|
|
315
|
+
error: str
|
|
316
|
+
timestamp: datetime
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class RunFailure(BaseModel):
|
|
320
|
+
"""A failed run with its error events."""
|
|
321
|
+
|
|
322
|
+
run: Run
|
|
323
|
+
errors: list[RunErrorEvent]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class FailureList(BaseModel):
|
|
327
|
+
"""Recent failed runs with their errors."""
|
|
328
|
+
|
|
329
|
+
status: Literal["success"] = "success"
|
|
330
|
+
count: int
|
|
331
|
+
failures: list[RunFailure]
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class BackfillList(BaseModel):
|
|
335
|
+
"""Backfills, optionally only the active ones."""
|
|
336
|
+
|
|
337
|
+
status: Literal["success"] = "success"
|
|
338
|
+
count: int
|
|
339
|
+
backfills: list[Backfill]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# -- Analytics ------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class RunHistorySummary(BaseModel):
|
|
346
|
+
"""Aggregate run statistics over a look-back period."""
|
|
347
|
+
|
|
348
|
+
status: Literal["success"] = "success"
|
|
349
|
+
period_days: int
|
|
350
|
+
component_id: str | None = None
|
|
351
|
+
total_runs: int
|
|
352
|
+
by_status: dict[str, int]
|
|
353
|
+
success_rate: float | None = None
|
|
354
|
+
avg_duration_seconds: float | None = None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class PartitionCoverage(BaseModel):
|
|
358
|
+
"""Which partition dates in a range have successful runs."""
|
|
359
|
+
|
|
360
|
+
status: Literal["success"] = "success"
|
|
361
|
+
component_id: str
|
|
362
|
+
start_date: str
|
|
363
|
+
end_date: str
|
|
364
|
+
total_days: int
|
|
365
|
+
covered_days: int
|
|
366
|
+
missing_days: int
|
|
367
|
+
coverage_percent: float
|
|
368
|
+
missing_dates: list[str]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class JobFreshness(BaseModel):
|
|
372
|
+
"""One job's data freshness."""
|
|
373
|
+
|
|
374
|
+
job: Component
|
|
375
|
+
last_success_at: datetime | None = None
|
|
376
|
+
hours_since_success: float | None = None
|
|
377
|
+
stale: bool
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class FreshnessReport(BaseModel):
|
|
381
|
+
"""Freshness across all enabled jobs."""
|
|
382
|
+
|
|
383
|
+
status: Literal["success"] = "success"
|
|
384
|
+
total_jobs: int
|
|
385
|
+
stale_count: int
|
|
386
|
+
jobs: list[JobFreshness]
|