marimo-pmprov 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.
- marimo_pmprov-0.1.0.dist-info/METADATA +15 -0
- marimo_pmprov-0.1.0.dist-info/RECORD +13 -0
- marimo_pmprov-0.1.0.dist-info/WHEEL +4 -0
- provenance_widget/__init__.py +1 -0
- provenance_widget/display.py +24 -0
- provenance_widget/interfaces.py +119 -0
- provenance_widget/mock_source.py +207 -0
- provenance_widget/pmprov_adapter.py +175 -0
- provenance_widget/serialize.py +57 -0
- provenance_widget/static/.gitkeep +0 -0
- provenance_widget/static/widget.css +1 -0
- provenance_widget/static/widget.js +2 -0
- provenance_widget/widget.py +82 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marimo-pmprov
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A marimo anywidget for analytic provenance and annotation, decoupled from pmprov.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: anywidget>=0.9
|
|
7
|
+
Requires-Dist: marimo>=0.9
|
|
8
|
+
Requires-Dist: traitlets>=5.14
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: altair; extra == 'dev'
|
|
11
|
+
Requires-Dist: matplotlib; extra == 'dev'
|
|
12
|
+
Requires-Dist: pandas; extra == 'dev'
|
|
13
|
+
Requires-Dist: plotly; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: vl-convert-python; extra == 'dev'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
provenance_widget/__init__.py,sha256=C327TiRMza0oQhmTNew5hjdOTfUs7JyJlTd556-ylIM,84
|
|
2
|
+
provenance_widget/display.py,sha256=Q7AMKXrErCRgoLCOiotjet5OaCOykDYVEfefFE2EX18,847
|
|
3
|
+
provenance_widget/interfaces.py,sha256=1IGZM7sdpZUyo_Zxk-BbOx-NCiG0arLR2SLhZWQmHeQ,2518
|
|
4
|
+
provenance_widget/mock_source.py,sha256=K-THrXAtBZM4aJXMnyQqYUZ99Q5_yMT7fH6LMUGO_mY,11429
|
|
5
|
+
provenance_widget/pmprov_adapter.py,sha256=2P40gCQKfhMVLOzGEf7FN6TXsKMr84hUxwq29FZKQyA,7404
|
|
6
|
+
provenance_widget/serialize.py,sha256=BY36NuU9uJEESqSRmEul9axtqBKmFK8x_m_I_7BMMOI,2237
|
|
7
|
+
provenance_widget/widget.py,sha256=IUsstc6zHxK71KFK7F5veF8c2lm-JMh-BLyJxmAvLT0,3196
|
|
8
|
+
provenance_widget/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
provenance_widget/static/widget.css,sha256=uPeA5sasZqvO_T7CQK4Q2aCeFxt51BnB36kz6HCnpdM,6023
|
|
10
|
+
provenance_widget/static/widget.js,sha256=ni9TMSwo7ldg7vbKVdGNSAL7LG8VGCUwLJ_0hABttp4,20777
|
|
11
|
+
marimo_pmprov-0.1.0.dist-info/METADATA,sha256=XLzJhZWCd9U5NBjhYXr7ClaxHO19plWgy3lbK2nce0c,528
|
|
12
|
+
marimo_pmprov-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
13
|
+
marimo_pmprov-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""marimo-pmprov: provenance and annotation sidebar widget for marimo notebooks."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
|
|
5
|
+
import marimo as mo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _wrap(inner_html: str, artifact_id: str, artifact_name: str, granularity: str) -> mo.Html:
|
|
9
|
+
return mo.Html(
|
|
10
|
+
f'<div class="pw-annotatable" '
|
|
11
|
+
f'data-pw-artifact-id="{html.escape(artifact_id, quote=True)}" '
|
|
12
|
+
f'data-pw-artifact-name="{html.escape(artifact_name, quote=True)}" '
|
|
13
|
+
f'data-pw-granularity="{html.escape(granularity, quote=True)}">'
|
|
14
|
+
f"{inner_html}"
|
|
15
|
+
f"</div>"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def show_table(table: "mo.ui.table", artifact_id: str, artifact_name: str) -> mo.Html:
|
|
20
|
+
return _wrap(table.text, artifact_id, artifact_name, granularity="dataset")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def show_chart(chart_html: mo.Html, artifact_id: str, artifact_name: str) -> mo.Html:
|
|
24
|
+
return _wrap(chart_html.text, artifact_id, artifact_name, granularity="chart_selection")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StepCategory(str, Enum):
|
|
9
|
+
DATA_LOADING = "DATA_LOADING"
|
|
10
|
+
CLEANING = "CLEANING"
|
|
11
|
+
FEATURE_ENGINEERING = "FEATURE_ENGINEERING"
|
|
12
|
+
AGGREGATION = "AGGREGATION"
|
|
13
|
+
COMPARISON = "COMPARISON"
|
|
14
|
+
ANALYSIS = "ANALYSIS"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Agent:
|
|
19
|
+
agent_id: str
|
|
20
|
+
agent_type: str
|
|
21
|
+
display_name: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ColumnAdded:
|
|
26
|
+
name: str
|
|
27
|
+
dtype: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ColumnRemoved:
|
|
32
|
+
name: str
|
|
33
|
+
dtype: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Delta:
|
|
38
|
+
columns_added: list[ColumnAdded]
|
|
39
|
+
columns_removed: list[ColumnRemoved]
|
|
40
|
+
row_count_before: int
|
|
41
|
+
row_count_after: int
|
|
42
|
+
|
|
43
|
+
def summary(self) -> str:
|
|
44
|
+
parts = []
|
|
45
|
+
for c in self.columns_added:
|
|
46
|
+
parts.append(f"+{c.name}")
|
|
47
|
+
for c in self.columns_removed:
|
|
48
|
+
parts.append(f"-{c.name}")
|
|
49
|
+
parts.append(f"rows {self.row_count_before}->{self.row_count_after}")
|
|
50
|
+
return ", ".join(parts)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Operation:
|
|
55
|
+
operation_id: str
|
|
56
|
+
name: str
|
|
57
|
+
operation_type: str
|
|
58
|
+
command_name: str
|
|
59
|
+
category: StepCategory
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ArtifactRef:
|
|
64
|
+
artifact_id: str
|
|
65
|
+
artifact_name: str
|
|
66
|
+
granularity: str
|
|
67
|
+
detail: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class Tag:
|
|
72
|
+
name: str
|
|
73
|
+
color: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class Annotation:
|
|
78
|
+
annotation_id: str
|
|
79
|
+
title: str
|
|
80
|
+
note: str
|
|
81
|
+
tags: list[Tag]
|
|
82
|
+
artifacts: list[ArtifactRef]
|
|
83
|
+
author: Agent
|
|
84
|
+
timestamp: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class ProvenanceNode:
|
|
89
|
+
state_id: str
|
|
90
|
+
parent_state_id: str | None
|
|
91
|
+
branch_id: str
|
|
92
|
+
operation: Operation
|
|
93
|
+
params: dict
|
|
94
|
+
delta: Delta
|
|
95
|
+
annotations: list[Annotation] = field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class ProvenanceTree:
|
|
100
|
+
nodes: dict[str, ProvenanceNode]
|
|
101
|
+
root_id: str
|
|
102
|
+
branches: dict[str, str]
|
|
103
|
+
|
|
104
|
+
def children_of(self, state_id: str) -> list[ProvenanceNode]:
|
|
105
|
+
return [n for n in self.nodes.values() if n.parent_state_id == state_id]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ProvenanceSource(Protocol):
|
|
109
|
+
def get_tree(self) -> ProvenanceTree: ...
|
|
110
|
+
|
|
111
|
+
def state_for_artifact(self, artifact_id: str) -> str | None:
|
|
112
|
+
"""Return the state_id of the analysis state that contains/produced
|
|
113
|
+
this artifact, or None if the source can't resolve it.
|
|
114
|
+
|
|
115
|
+
Used to associate a new annotation with the right tree node purely
|
|
116
|
+
from the artifact it targets, without the caller having to thread a
|
|
117
|
+
state_id through by hand.
|
|
118
|
+
"""
|
|
119
|
+
...
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from provenance_widget.interfaces import (
|
|
4
|
+
Agent, Annotation, ArtifactRef, ColumnAdded, ColumnRemoved, Delta,
|
|
5
|
+
Operation, ProvenanceNode, ProvenanceTree, StepCategory, Tag,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
_STUDENT = Agent(agent_id="agent-student", agent_type="human", display_name="S")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _op(op_id: str, name: str, op_type: str, command: str, category: StepCategory) -> Operation:
|
|
12
|
+
return Operation(operation_id=op_id, name=name, operation_type=op_type,
|
|
13
|
+
command_name=command, category=category)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MockProvenanceSource:
|
|
17
|
+
"""Deterministic in-memory ProvenanceSource for demoing the widget without pmprov."""
|
|
18
|
+
|
|
19
|
+
def get_tree(self) -> ProvenanceTree:
|
|
20
|
+
root = ProvenanceNode(
|
|
21
|
+
state_id="s0", parent_state_id=None, branch_id="main",
|
|
22
|
+
operation=_op("op0", "Load event log", "IMPORT", "load_event_log",
|
|
23
|
+
StepCategory.DATA_LOADING),
|
|
24
|
+
params={"filepath": "data/rtfm_full.csv"},
|
|
25
|
+
delta=Delta([ColumnAdded("case:concept:name", "object"),
|
|
26
|
+
ColumnAdded("concept:name", "object"),
|
|
27
|
+
ColumnAdded("time:timestamp", "datetime64[ns]")],
|
|
28
|
+
[], row_count_before=0, row_count_after=15214),
|
|
29
|
+
annotations=[],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
filtered = ProvenanceNode(
|
|
33
|
+
state_id="s1", parent_state_id="s0", branch_id="main",
|
|
34
|
+
operation=_op("op1", "Filter long traces", "FILTER", "filter_duration",
|
|
35
|
+
StepCategory.CLEANING),
|
|
36
|
+
params={"threshold_hours": 120},
|
|
37
|
+
delta=Delta([], [], row_count_before=15214, row_count_after=14032),
|
|
38
|
+
annotations=[Annotation(
|
|
39
|
+
annotation_id="ann-c8f2", title="Truncate outliers",
|
|
40
|
+
note="Removed traces longer than 120h; distorted the mean duration.",
|
|
41
|
+
tags=[Tag(name="cleaning", color="#0EA5E9"), Tag(name="v1.1", color="#94A3B8")],
|
|
42
|
+
artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
|
|
43
|
+
granularity="column", detail="duration")],
|
|
44
|
+
author=_STUDENT, timestamp="2026-08-04T10:42:00",
|
|
45
|
+
)],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
enriched = ProvenanceNode(
|
|
49
|
+
state_id="s2", parent_state_id="s1", branch_id="main",
|
|
50
|
+
operation=_op("op2", "Calculate case cost", "ENRICHMENT", "add_attribute",
|
|
51
|
+
StepCategory.FEATURE_ENGINEERING),
|
|
52
|
+
params={"output_col": "case_cost", "source": "resource_wage_matrix"},
|
|
53
|
+
delta=Delta([ColumnAdded("case_cost", "float64")], [],
|
|
54
|
+
row_count_before=14032, row_count_after=14032),
|
|
55
|
+
annotations=[Annotation(
|
|
56
|
+
annotation_id="ann-a911", title="Calculate case cost",
|
|
57
|
+
note="Joined hourly wage matrix with resource trace logs.",
|
|
58
|
+
tags=[Tag(name="cost", color="#10B981"), Tag(name="enrichment", color="#10B981")],
|
|
59
|
+
artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
|
|
60
|
+
granularity="dataset")],
|
|
61
|
+
author=_STUDENT, timestamp="2026-08-04T10:58:00",
|
|
62
|
+
)],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
explore_threshold = ProvenanceNode(
|
|
66
|
+
state_id="s1b", parent_state_id="s0", branch_id="branch-explore",
|
|
67
|
+
operation=_op("op1b", "Filter long traces (60h)", "FILTER", "filter_duration",
|
|
68
|
+
StepCategory.CLEANING),
|
|
69
|
+
params={"threshold_hours": 60},
|
|
70
|
+
delta=Delta([], [], row_count_before=15214, row_count_after=11800),
|
|
71
|
+
annotations=[Annotation(
|
|
72
|
+
annotation_id="ann-e771", title="Tighter threshold experiment",
|
|
73
|
+
note="Tried a stricter 60h cutoff; dropped too many valid cases, abandoned.",
|
|
74
|
+
tags=[Tag(name="cleaning", color="#0EA5E9"), Tag(name="exploration", color="#A855F7")],
|
|
75
|
+
artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
|
|
76
|
+
granularity="row_subset", detail="rows 1-11800")],
|
|
77
|
+
author=_STUDENT, timestamp="2026-08-04T10:47:00",
|
|
78
|
+
)],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
rule_induction = ProvenanceNode(
|
|
82
|
+
state_id="s3a", parent_state_id="s2", branch_id="branch-a",
|
|
83
|
+
operation=_op("op3a", "Induce decision rules", "RULE_INDUCTION", "induce_rules",
|
|
84
|
+
StepCategory.ANALYSIS),
|
|
85
|
+
params={"target_col": "is_long_case", "max_depth": 4},
|
|
86
|
+
delta=Delta([ColumnAdded("predicted_outcome", "bool")], [],
|
|
87
|
+
row_count_before=14032, row_count_after=14032),
|
|
88
|
+
annotations=[Annotation(
|
|
89
|
+
annotation_id="ann-d213", title="First rule set",
|
|
90
|
+
note="Depth-4 tree overfits on resource attribute; needs pruning.",
|
|
91
|
+
tags=[Tag(name="rules", color="#F59E0B")],
|
|
92
|
+
artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
|
|
93
|
+
granularity="row_subset", detail="rows 1-4032")],
|
|
94
|
+
author=_STUDENT, timestamp="2026-08-04T11:10:00",
|
|
95
|
+
)],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
conformance = ProvenanceNode(
|
|
99
|
+
state_id="s3b", parent_state_id="s2", branch_id="branch-b",
|
|
100
|
+
operation=_op("op3b", "Conformance check", "CONFORMANCE_CHECK", "check_conformance",
|
|
101
|
+
StepCategory.COMPARISON),
|
|
102
|
+
params={"model": "petri_net_v1"},
|
|
103
|
+
delta=Delta([ColumnAdded("fitness", "float64")], [],
|
|
104
|
+
row_count_before=14032, row_count_after=14032),
|
|
105
|
+
annotations=[Annotation(
|
|
106
|
+
annotation_id="ann-f004", title="Baseline fitness",
|
|
107
|
+
note="Fitness 0.87 against the reference Petri net.",
|
|
108
|
+
tags=[Tag(name="conformance", color="#F43F5E"), Tag(name="v1.1", color="#94A3B8")],
|
|
109
|
+
artifacts=[ArtifactRef(artifact_id="art-process-map", artifact_name="Chart_ProcessMap",
|
|
110
|
+
granularity="chart_selection")],
|
|
111
|
+
author=_STUDENT, timestamp="2026-08-04T11:22:00",
|
|
112
|
+
)],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
pruned_rules = ProvenanceNode(
|
|
116
|
+
state_id="s4a", parent_state_id="s3a", branch_id="branch-a",
|
|
117
|
+
operation=_op("op4a", "Prune rule set", "RULE_PRUNING", "prune_rules",
|
|
118
|
+
StepCategory.ANALYSIS),
|
|
119
|
+
params={"max_depth": 2, "min_support": 0.05},
|
|
120
|
+
delta=Delta([], [], row_count_before=14032, row_count_after=14032),
|
|
121
|
+
annotations=[Annotation(
|
|
122
|
+
annotation_id="ann-b552", title="Pruned rule set",
|
|
123
|
+
note="Capped depth at 2; resource attribute no longer dominates the split.",
|
|
124
|
+
tags=[Tag(name="rules", color="#F59E0B"), Tag(name="v1.2", color="#94A3B8")],
|
|
125
|
+
artifacts=[ArtifactRef(artifact_id="art-rule-report", artifact_name="rule_report.json",
|
|
126
|
+
granularity="dataset")],
|
|
127
|
+
author=_STUDENT, timestamp="2026-08-04T11:35:00",
|
|
128
|
+
)],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
cost_aggregation = ProvenanceNode(
|
|
132
|
+
state_id="s4b", parent_state_id="s3b", branch_id="branch-b",
|
|
133
|
+
operation=_op("op4b", "Aggregate cost by outcome", "GROUP_AGGREGATE", "groupby_agg",
|
|
134
|
+
StepCategory.AGGREGATION),
|
|
135
|
+
params={"group_by": "fitness_bucket", "agg": "mean(case_cost)"},
|
|
136
|
+
delta=Delta([ColumnAdded("mean_case_cost", "float64")], [],
|
|
137
|
+
row_count_before=14032, row_count_after=6),
|
|
138
|
+
annotations=[Annotation(
|
|
139
|
+
annotation_id="ann-9a10", title="Cost by conformance bucket",
|
|
140
|
+
note="Non-conformant cases cost 2.3x more on average — worth flagging to reviewer.",
|
|
141
|
+
tags=[Tag(name="cost", color="#10B981"), Tag(name="conformance", color="#F43F5E")],
|
|
142
|
+
artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
|
|
143
|
+
granularity="column", detail="case_cost")],
|
|
144
|
+
author=_STUDENT, timestamp="2026-08-04T11:48:00",
|
|
145
|
+
)],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
reviewer_check = ProvenanceNode(
|
|
149
|
+
state_id="s5a", parent_state_id="s4a", branch_id="branch-a",
|
|
150
|
+
operation=_op("op5a", "Reviewer sign-off", "REVIEW", "review_annotation",
|
|
151
|
+
StepCategory.COMPARISON),
|
|
152
|
+
params={"reviewer": "agent-reviewer-1"},
|
|
153
|
+
delta=Delta([], [], row_count_before=14032, row_count_after=14032),
|
|
154
|
+
annotations=[Annotation(
|
|
155
|
+
annotation_id="ann-7c31", title="Reviewer approved pruning",
|
|
156
|
+
note="Confirmed the pruned rule set generalizes on a held-out fold.",
|
|
157
|
+
tags=[Tag(name="rules", color="#F59E0B"), Tag(name="review", color="#3B82F6")],
|
|
158
|
+
artifacts=[ArtifactRef(artifact_id="art-rule-report", artifact_name="rule_report.json",
|
|
159
|
+
granularity="dataset")],
|
|
160
|
+
author=Agent(agent_id="agent-reviewer-1", agent_type="human", display_name="R"),
|
|
161
|
+
timestamp="2026-08-04T13:05:00",
|
|
162
|
+
)],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
cost_model = ProvenanceNode(
|
|
166
|
+
state_id="s5b", parent_state_id="s4b", branch_id="branch-b",
|
|
167
|
+
operation=_op("op5b", "Fit cost prediction model", "MODEL_FIT", "fit_regressor",
|
|
168
|
+
StepCategory.FEATURE_ENGINEERING),
|
|
169
|
+
params={"model": "gradient_boost", "target": "mean_case_cost"},
|
|
170
|
+
delta=Delta([ColumnAdded("predicted_cost", "float64")], [],
|
|
171
|
+
row_count_before=6, row_count_after=6),
|
|
172
|
+
annotations=[Annotation(
|
|
173
|
+
annotation_id="ann-1de4", title="Cost prediction model",
|
|
174
|
+
note="R^2 0.81 on the 6 conformance buckets; small sample, treat as directional.",
|
|
175
|
+
tags=[Tag(name="cost", color="#10B981"), Tag(name="v1.2", color="#94A3B8")],
|
|
176
|
+
artifacts=[ArtifactRef(artifact_id="art-cost-model", artifact_name="cost_model.pkl",
|
|
177
|
+
granularity="dataset")],
|
|
178
|
+
author=_STUDENT, timestamp="2026-08-04T12:15:00",
|
|
179
|
+
)],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return ProvenanceTree(
|
|
183
|
+
nodes={n.state_id: n for n in [
|
|
184
|
+
root, filtered, explore_threshold, enriched,
|
|
185
|
+
rule_induction, conformance, pruned_rules, cost_aggregation,
|
|
186
|
+
reviewer_check, cost_model,
|
|
187
|
+
]},
|
|
188
|
+
root_id="s0",
|
|
189
|
+
branches={
|
|
190
|
+
"main": "main",
|
|
191
|
+
"branch-explore": "explore-threshold",
|
|
192
|
+
"branch-a": "rule-induction",
|
|
193
|
+
"branch-b": "conformance",
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def state_for_artifact(self, artifact_id: str) -> str | None:
|
|
198
|
+
"""Best-effort: MockProvenanceSource has no artifact registry either
|
|
199
|
+
(its ArtifactRefs are just hand-authored strings) — fall back to
|
|
200
|
+
whichever node's own baked-in annotations already reference this
|
|
201
|
+
artifact_id, since that's the closest thing to "produced it" here.
|
|
202
|
+
"""
|
|
203
|
+
for node in self.get_tree().nodes.values():
|
|
204
|
+
for annotation in node.annotations:
|
|
205
|
+
if any(a.artifact_id == artifact_id for a in annotation.artifacts):
|
|
206
|
+
return node.state_id
|
|
207
|
+
return None
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""ProvenanceSource adapter backed by a live pmprov RuntimeTracker.
|
|
2
|
+
|
|
3
|
+
pmprov (https://github.com/alvarobernal2412/pmprov) records provenance
|
|
4
|
+
automatically at the AST level — no explicit annotation concept exists there
|
|
5
|
+
(see the pmprov README's own note on this). This adapter translates its
|
|
6
|
+
state/step graph into the ProvenanceTree shape this widget renders; all
|
|
7
|
+
annotation still happens on the marimo-pmprov side via
|
|
8
|
+
`ProvenancePanel.commit_annotation()` or the sidebar's composer.
|
|
9
|
+
|
|
10
|
+
Only imports pmprov lazily inside methods — marimo-pmprov itself has no
|
|
11
|
+
hard dependency on pmprov (see the `pmprov` extra in pyproject.toml).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from provenance_widget.interfaces import (
|
|
19
|
+
ColumnAdded,
|
|
20
|
+
ColumnRemoved,
|
|
21
|
+
Delta,
|
|
22
|
+
Operation,
|
|
23
|
+
ProvenanceNode,
|
|
24
|
+
ProvenanceTree,
|
|
25
|
+
StepCategory,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_DEFAULT_CATEGORY = StepCategory.ANALYSIS
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _map_category(name: str | None) -> StepCategory:
|
|
32
|
+
"""Map a pmprov step_category name onto this widget's fixed enum.
|
|
33
|
+
|
|
34
|
+
Register pmprov step categories with the exact StepCategory member names
|
|
35
|
+
(DATA_LOADING, CLEANING, FEATURE_ENGINEERING, AGGREGATION, COMPARISON,
|
|
36
|
+
ANALYSIS) via `tracker.step_category(...)` for a 1:1 mapping — anything
|
|
37
|
+
else (including pmprov's "unknown" default) falls back to ANALYSIS.
|
|
38
|
+
"""
|
|
39
|
+
if not name:
|
|
40
|
+
return _DEFAULT_CATEGORY
|
|
41
|
+
try:
|
|
42
|
+
return StepCategory[name.upper()]
|
|
43
|
+
except KeyError:
|
|
44
|
+
return _DEFAULT_CATEGORY
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _root_node(state_id: str, branch_id: str) -> ProvenanceNode:
|
|
48
|
+
return ProvenanceNode(
|
|
49
|
+
state_id=state_id,
|
|
50
|
+
parent_state_id=None,
|
|
51
|
+
branch_id=branch_id,
|
|
52
|
+
operation=Operation(
|
|
53
|
+
operation_id="root",
|
|
54
|
+
name="Session start",
|
|
55
|
+
operation_type="IMPORT",
|
|
56
|
+
command_name="init",
|
|
57
|
+
category=StepCategory.DATA_LOADING,
|
|
58
|
+
),
|
|
59
|
+
params={},
|
|
60
|
+
delta=Delta([], [], row_count_before=0, row_count_after=0),
|
|
61
|
+
annotations=[],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PmprovAdapter:
|
|
66
|
+
"""Reads a pmprov RuntimeTracker's history as a ProvenanceTree.
|
|
67
|
+
|
|
68
|
+
Unlike MockProvenanceSource's fixed snapshot, this tree grows as the
|
|
69
|
+
notebook runs — call `ProvenancePanel.refresh()` after each traced step
|
|
70
|
+
(once its writes have settled; see `settle()` in the pmprov README's
|
|
71
|
+
Marimo snippet) to pick up the new state.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, tracker: Any) -> None:
|
|
75
|
+
self._rt = tracker
|
|
76
|
+
|
|
77
|
+
def get_tree(self) -> ProvenanceTree:
|
|
78
|
+
history_id = self._rt._history.history_id
|
|
79
|
+
graph = self._rt.storage.load_graph(history_id)
|
|
80
|
+
states_by_id = {s["state_id"]: s for s in graph["states"]}
|
|
81
|
+
steps = sorted(graph["steps"], key=lambda s: s["timestamp"])
|
|
82
|
+
|
|
83
|
+
# pmprov's DuckDB/SQLite backend stores this as "" (empty string),
|
|
84
|
+
# not NULL, for the root state — falsy check, not an `is None` one.
|
|
85
|
+
root_state = next(
|
|
86
|
+
(s for s in states_by_id.values() if not s["produced_by_step_id"]), None
|
|
87
|
+
)
|
|
88
|
+
if root_state is None:
|
|
89
|
+
# RuntimeTracker always writes its root state synchronously at
|
|
90
|
+
# init, so this only happens if get_tree() runs before that —
|
|
91
|
+
# fall back to an empty-but-valid single-node tree.
|
|
92
|
+
return ProvenanceTree(
|
|
93
|
+
nodes={"root": _root_node("root", "main")}, root_id="root", branches={"main": "main"}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
root_id = root_state["state_id"]
|
|
97
|
+
nodes: dict[str, ProvenanceNode] = {root_id: _root_node(root_id, root_state["branch_id"])}
|
|
98
|
+
branches: dict[str, str] = {root_state["branch_id"]: "main"}
|
|
99
|
+
|
|
100
|
+
# deltas only carry a net rows_delta, not before/after counts, so
|
|
101
|
+
# row counts are approximated by walking the pipeline in order and
|
|
102
|
+
# accumulating from 0 at the root — accurate for the common linear
|
|
103
|
+
# case this adapter is meant for, not for arbitrary branch topologies.
|
|
104
|
+
running_rows: dict[str, int] = {root_id: 0}
|
|
105
|
+
|
|
106
|
+
for step in steps:
|
|
107
|
+
output_id = step["output_state_id"]
|
|
108
|
+
state = states_by_id.get(output_id)
|
|
109
|
+
if state is None:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
detail = self._rt.storage.load_state_detail(output_id)
|
|
113
|
+
op_info = detail.get("operation") or {}
|
|
114
|
+
delta_info = detail.get("delta") or {}
|
|
115
|
+
branch_name = detail.get("branch_name") or state["branch_id"]
|
|
116
|
+
branches.setdefault(state["branch_id"], branch_name)
|
|
117
|
+
|
|
118
|
+
parent_id = step["input_state_id"]
|
|
119
|
+
rows_before = running_rows.get(parent_id, 0)
|
|
120
|
+
rows_after = rows_before + (delta_info.get("rows_delta") or 0)
|
|
121
|
+
running_rows[output_id] = rows_after
|
|
122
|
+
|
|
123
|
+
nodes[output_id] = ProvenanceNode(
|
|
124
|
+
state_id=output_id,
|
|
125
|
+
parent_state_id=parent_id,
|
|
126
|
+
branch_id=state["branch_id"],
|
|
127
|
+
operation=Operation(
|
|
128
|
+
operation_id=step["step_id"],
|
|
129
|
+
name=op_info.get("name") or step["func_name"],
|
|
130
|
+
operation_type=op_info.get("type") or "unknown",
|
|
131
|
+
command_name=step["func_name"],
|
|
132
|
+
category=_map_category(op_info.get("category")),
|
|
133
|
+
),
|
|
134
|
+
params={p["param_id"]: p["value"] for p in detail.get("params", [])},
|
|
135
|
+
delta=Delta(
|
|
136
|
+
columns_added=[ColumnAdded(name=c, dtype="") for c in delta_info.get("columns_added", [])],
|
|
137
|
+
columns_removed=[
|
|
138
|
+
ColumnRemoved(name=c, dtype="") for c in delta_info.get("columns_removed", [])
|
|
139
|
+
],
|
|
140
|
+
row_count_before=rows_before,
|
|
141
|
+
row_count_after=rows_after,
|
|
142
|
+
),
|
|
143
|
+
annotations=[],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return ProvenanceTree(nodes=nodes, root_id=root_id, branches=branches)
|
|
147
|
+
|
|
148
|
+
def state_for_artifact(self, artifact_id: str) -> str | None:
|
|
149
|
+
"""pmprov has no separate artifact registry — a "state" *is* the
|
|
150
|
+
DataFrame/artifact a step produced, so an artifact_id built from
|
|
151
|
+
`latest_state_id()` (the convention this adapter's callers use, see
|
|
152
|
+
the demo notebook) is already a real state_id. Confirm it still
|
|
153
|
+
exists in the current graph before handing it back.
|
|
154
|
+
"""
|
|
155
|
+
history_id = self._rt._history.history_id
|
|
156
|
+
graph = self._rt.storage.load_graph(history_id)
|
|
157
|
+
if any(s["state_id"] == artifact_id for s in graph["states"]):
|
|
158
|
+
return artifact_id
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def latest_state_id(self, func_name: str | None = None) -> str | None:
|
|
162
|
+
"""Return the output_state_id of the most recent step.
|
|
163
|
+
|
|
164
|
+
Pass func_name to disambiguate ("the last time X ran") if steps have
|
|
165
|
+
happened since; omit it to just mean "whatever ran last". Convenience
|
|
166
|
+
for notebooks that want to annotate "the step I just ran" without
|
|
167
|
+
threading state_ids through by hand — settle() first (see pmprov's
|
|
168
|
+
README) so the write has landed before this reads it.
|
|
169
|
+
"""
|
|
170
|
+
history_id = self._rt._history.history_id
|
|
171
|
+
steps = self._rt.storage.load_graph(history_id)["steps"]
|
|
172
|
+
candidates = steps if func_name is None else [s for s in steps if s["func_name"] == func_name]
|
|
173
|
+
if not candidates:
|
|
174
|
+
return None
|
|
175
|
+
return max(candidates, key=lambda s: s["timestamp"])["output_state_id"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
|
|
5
|
+
from provenance_widget.interfaces import ProvenanceTree
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def tree_to_json(tree: ProvenanceTree) -> dict:
|
|
9
|
+
nodes = {}
|
|
10
|
+
for state_id, node in tree.nodes.items():
|
|
11
|
+
node_dict = asdict(node)
|
|
12
|
+
node_dict["operation"]["category"] = node.operation.category.value
|
|
13
|
+
nodes[state_id] = {
|
|
14
|
+
"stateId": node_dict["state_id"],
|
|
15
|
+
"parentStateId": node_dict["parent_state_id"],
|
|
16
|
+
"branchId": node_dict["branch_id"],
|
|
17
|
+
"operation": {
|
|
18
|
+
"operationId": node_dict["operation"]["operation_id"],
|
|
19
|
+
"name": node_dict["operation"]["name"],
|
|
20
|
+
"operationType": node_dict["operation"]["operation_type"],
|
|
21
|
+
"commandName": node_dict["operation"]["command_name"],
|
|
22
|
+
"category": node_dict["operation"]["category"],
|
|
23
|
+
},
|
|
24
|
+
"params": node_dict["params"],
|
|
25
|
+
"delta": {
|
|
26
|
+
"columnsAdded": node_dict["delta"]["columns_added"],
|
|
27
|
+
"columnsRemoved": node_dict["delta"]["columns_removed"],
|
|
28
|
+
"rowCountBefore": node_dict["delta"]["row_count_before"],
|
|
29
|
+
"rowCountAfter": node_dict["delta"]["row_count_after"],
|
|
30
|
+
"summary": node.delta.summary(),
|
|
31
|
+
},
|
|
32
|
+
"annotations": [
|
|
33
|
+
{
|
|
34
|
+
"annotationId": a["annotation_id"],
|
|
35
|
+
"title": a["title"],
|
|
36
|
+
"note": a["note"],
|
|
37
|
+
"tags": a["tags"],
|
|
38
|
+
"artifacts": [
|
|
39
|
+
{
|
|
40
|
+
"artifactId": ar["artifact_id"],
|
|
41
|
+
"artifactName": ar["artifact_name"],
|
|
42
|
+
"granularity": ar["granularity"],
|
|
43
|
+
"detail": ar["detail"],
|
|
44
|
+
}
|
|
45
|
+
for ar in a["artifacts"]
|
|
46
|
+
],
|
|
47
|
+
"author": a["author"],
|
|
48
|
+
"timestamp": a["timestamp"],
|
|
49
|
+
}
|
|
50
|
+
for a in node_dict["annotations"]
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
"rootId": tree.root_id,
|
|
55
|
+
"branches": tree.branches,
|
|
56
|
+
"nodes": nodes,
|
|
57
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.pw-root{font-family:ui-sans-serif,system-ui,sans-serif;color:#0f172a;background:#f8fafc;padding:8px;border-radius:8px;min-width:0;overflow:hidden}@media (prefers-color-scheme: dark){.pw-root{color:#f8fafc;background:#0f172a}}.pw-root.pw-collapsed .pw-content{display:none}body.pw-picking-active *{cursor:crosshair!important}.pw-segmented-control{display:flex;gap:4px;margin-bottom:8px;min-width:0}.pw-segment{border:1px solid #94A3B8;background:transparent;border-radius:6px;padding:4px 10px;cursor:pointer}.pw-segment-active{background:#64748b;color:#fff}.pw-node-card,.pw-commit-card{border:1px solid #E2E8F0;border-left:4px solid var(--category-color, #64748B);border-radius:10px;padding:8px 10px;margin-bottom:8px;cursor:pointer;background:#fff}@media (prefers-color-scheme: dark){.pw-node-card,.pw-commit-card{background:#1e293b;border-color:#334155}}.pw-node-card.pw-picker-hover,.pw-annotatable.pw-picker-hover{outline:2px solid #F59E0B}.pw-node-card-selected{outline:2px solid #0EA5E9}.pw-node-card-collapsed{padding:6px 10px;margin-bottom:4px}.pw-node-card-collapsed .pw-node-title{font-weight:500;opacity:.55;margin:0}.pw-op-badge{font-size:11px;text-transform:uppercase;color:var(--category-color, #64748B);font-weight:600}.pw-node-title{font-weight:700;margin:2px 0}.pw-node-note{font-size:12px;color:#64748b;margin-bottom:4px}.pw-node-tags,.pw-node-artifacts{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px;min-width:0}.pw-node-no-annotations{font-size:12px;color:#94a3b8;font-style:italic;margin-top:4px}.pw-annotation-block{border-top:1px dashed #E2E8F0;margin-top:6px;padding-top:6px}@media (prefers-color-scheme: dark){.pw-annotation-block{border-color:#334155}}.pw-annotation-title{font-size:12px;font-weight:600;color:#334155;margin-bottom:2px}@media (prefers-color-scheme: dark){.pw-annotation-title{color:#cbd5e1}}.pw-tag-chip{background:color-mix(in srgb,var(--chip-color, #64748B) 20%,transparent);color:var(--chip-color, #64748B);border-radius:999px;padding:2px 8px;font-size:11px}.pw-restore-btn{margin-left:6px;border:none;background:#f59e0b;color:#fff;border-radius:999px;padding:1px 6px;font-size:10px;cursor:pointer}.pw-artifact-badge{border:1px solid #94A3B8;border-radius:6px;padding:2px 6px;font-size:11px;font-family:ui-monospace,monospace}.pw-columns-scroll{flex:1 1 auto;min-height:0;overflow:auto}.pw-columns{display:flex;gap:12px;min-width:0;position:relative}.pw-column{min-width:260px;position:relative;z-index:1}.pw-tree-lines{position:absolute;top:0;left:0;z-index:0;pointer-events:none;overflow:visible}.pw-tree-line{fill:none;stroke:#cbd5e1;stroke-width:1.5}.pw-tree-line-active{fill:none;stroke:#0ea5e9;stroke-width:2}@media (prefers-color-scheme: dark){.pw-tree-line{stroke:#334155}}.pw-breadcrumb{flex:0 0 auto;font-size:12px;color:#64748b;margin-bottom:8px}.pw-breadcrumb-sep{margin:0 6px}.pw-filter-bar{flex:0 0 auto;margin-bottom:8px}.pw-filter-row{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:6px;min-width:0}.pw-filter-label{font-size:11px;text-transform:uppercase;color:#64748b;font-weight:600;min-width:90px}.pw-filter-chip{border:1px solid #94A3B8;border-radius:999px;background:transparent;cursor:pointer;padding:3px 10px;font-size:12px}.pw-filter-chip-active{background:#0ea5e9;border-color:#0ea5e9;color:#fff}.pw-filter-chip-empty{border-style:dashed;color:#64748b;font-style:italic;cursor:default}.pw-tree-inspect-pane{flex:0 0 auto;margin-top:12px;border:1px solid #CBD5E1;border-radius:8px;background:#f1f5f9;padding:10px 14px 14px;max-height:38%;overflow:auto}@media (prefers-color-scheme: dark){.pw-tree-inspect-pane{border-color:#334155;background:#1e293b}}.pw-tree-inspect-pane-label{font-size:11px;text-transform:uppercase;letter-spacing:.04em;font-weight:700;color:#475569;margin-bottom:8px}@media (prefers-color-scheme: dark){.pw-tree-inspect-pane-label{color:#94a3b8}}.pw-tree-inspect-pane-empty{color:#64748b;font-size:13px;font-style:italic}.pw-tree-portal .pw-column{min-width:300px}.pw-tree-header{flex:0 0 auto;margin-bottom:8px}.pw-tree-title{font-weight:700;font-size:15px}.pw-tree-caption{font-size:12px;color:#64748b;margin-top:2px}.pw-inspect-empty{padding:24px 8px;text-align:center;color:#64748b;font-size:13px}.pw-diff-pane{font-family:ui-monospace,monospace;font-size:12px;margin:8px 0}.pw-diff-added{color:#10b981}.pw-diff-removed{color:#f43f5e}.pw-composer{display:flex;flex-direction:column;gap:6px;border-top:1px solid #E2E8F0;padding-top:8px;margin-top:8px}.pw-composer textarea{min-height:60px}.pw-target-selector{border:1px solid #94A3B8;border-radius:6px;padding:6px 8px;font-size:12px}.pw-target-selector-empty{color:#64748b;font-style:italic}.pw-target-selector-label{font-size:10px;text-transform:uppercase;color:#64748b;font-weight:600;margin-bottom:2px}.pw-target-selector-value{font-family:ui-monospace,monospace;color:#0f172a}@media (prefers-color-scheme: dark){.pw-target-selector-value{color:#f8fafc}}.pw-commit-btn{background:#0f172a;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer}@media (prefers-color-scheme: dark){.pw-commit-btn{background:#f8fafc;color:#0f172a}}.pw-config-heading{font-size:11px;text-transform:uppercase;color:#64748b;font-weight:600;margin-bottom:6px}.pw-config-radio{display:flex;align-items:center;gap:6px;padding:4px 0;cursor:pointer;text-transform:capitalize}.pw-restore-banner{background:#fef3c7;border:1px solid #F59E0B;border-radius:6px;padding:8px;margin-bottom:8px;font-size:13px}.pw-pick-btn{border:1px dashed #94A3B8;background:transparent;border-radius:6px;padding:6px;cursor:pointer;margin-top:4px}.pw-pick-btn-armed{border:1px solid #F59E0B;background:color-mix(in srgb,#F59E0B 15%,transparent);color:#f59e0b;font-weight:600}.pw-gutter-plus{width:20px;height:20px;border-radius:50%;border:none;background:#0ea5e9;color:#fff;font-size:14px;line-height:1;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;box-shadow:0 1px 4px #0f172a59;z-index:1000}.pw-gutter-plus:hover{background:#0284c7}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function U(n,t,e){let o=document.createElement("div");return o.className="pw-segmented-control",n.forEach(({value:r,label:i})=>{let a=document.createElement("button");a.textContent=i,a.className=r===t?"pw-segment pw-segment-active":"pw-segment",a.addEventListener("click",()=>e(r)),o.appendChild(a)}),o}function B(n){let t=document.createElement("span");return t.className="pw-tag-chip",t.textContent=`#${n.name}`,t.style.setProperty("--chip-color",n.color),t}var z={dataset:"Whole dataset",column:"Column",row_subset:"Row subset",cell:"Cell",chart_selection:"Chart selection"};function _(n){let t=document.createElement("span");t.className="pw-artifact-badge";let e=z[n.granularity]??n.granularity;return t.textContent=n.detail?`${n.artifactName} \xB7 ${e}: ${n.detail}`:`${n.artifactName} \xB7 ${e}`,t.dataset.artifactId=n.artifactId,t}function me(n){let t=[];for(let e of Object.values(n.nodes))for(let o of e.annotations)t.push({stateId:e.stateId,annotation:o});for(let e of Y)t.push(e);return t.sort((e,o)=>e.annotation.timestamp<o.annotation.timestamp?-1:1)}var Y=[];function ue(n,t,e){let o=document.createElement("div");o.className="pw-commit-card",o.dataset.stateId=n.stateId;let r=document.createElement("div");r.className="pw-node-title",r.textContent=n.annotation.title,o.appendChild(r);let i=document.createElement("div");i.className="pw-node-tags",n.annotation.tags.forEach(s=>{let c=B(s);if(t==="reviewer"){let l=document.createElement("button");l.className="pw-restore-btn",l.textContent=`Restore to @${s.name}`,l.addEventListener("click",m=>{m.stopPropagation(),e.set("restore_request",{tag:s.name}),e.save_changes()}),c.appendChild(l)}i.appendChild(c)}),o.appendChild(i);let a=document.createElement("div");return a.className="pw-node-artifacts",n.annotation.artifacts.forEach(s=>a.appendChild(_(s))),o.appendChild(a),o}function fe(n){let t=document.createElement("div");t.className="pw-target-selector";let e=n.get("selection")?.artifact??{},o=e.artifactId?String(e.artifactId):null;if(!o)return t.classList.add("pw-target-selector-empty"),t.textContent=`No target selected \u2014 use "Pick from notebook" or hover an element's "+".`,t;let r=e.artifactName?String(e.artifactName):o,i=e.granularity?String(e.granularity):"dataset",a=e.detail?String(e.detail):null,s=z[i]??i,c=document.createElement("div");c.className="pw-target-selector-label",c.textContent="Target",t.appendChild(c);let l=document.createElement("div");return l.className="pw-target-selector-value",l.textContent=a?`${r} \xB7 ${s}: ${a}`:`${r} \xB7 ${s}`,l.title=`artifact id: ${o}`,t.appendChild(l),t}function ge(n,t){let e=document.createElement("div");e.className="pw-composer",e.appendChild(fe(t));let o=document.createElement("textarea");o.placeholder="Describe reasoning for this analytical step...",e.appendChild(o);let r=document.createElement("input");r.placeholder="#tags",e.appendChild(r);let i=document.createElement("button");return i.className="pw-commit-btn",i.textContent="Commit annotation",i.addEventListener("click",()=>{let a=t.get("selection")?.artifact??{},s={annotationId:`ann-${Date.now()}`,title:o.value.slice(0,40)||"Untitled annotation",note:o.value,tags:r.value.split(" ").filter(Boolean).map(g=>({name:g.replace(/^#/,""),color:"#64748B"})),artifacts:a.artifactId?[{artifactId:String(a.artifactId),artifactName:String(a.artifactName??""),granularity:String(a.granularity??"dataset"),detail:a.detail??null}]:[],author:{agent_id:"agent-student",agent_type:"human",display_name:"S"},timestamp:new Date().toISOString()},c=t.get("tree"),l=s.artifacts[0]?.artifactId,m=l&&c.nodes[l]?l:"composer",p=t.get("commits");t.set("commits",[...p,{stateId:m,annotation:s}]),t.save_changes(),o.value="",r.value=""}),e.appendChild(i),e}function K(n,t){n.innerHTML="";let e=t.get("tree"),o=t.get("mode");Y=t.get("commits");let r=document.createElement("div");r.className="pw-tree-header";let i=document.createElement("div");i.className="pw-tree-title",i.textContent="Annotation History",r.appendChild(i);let a=document.createElement("div");a.className="pw-tree-caption",a.textContent="Every annotation made in this analysis, oldest first.",r.appendChild(a),n.appendChild(r);let s=document.createElement("div");s.className="pw-commit-rail",me(e).forEach(c=>s.appendChild(ue(c,o,t))),n.appendChild(s),o==="student"&&n.appendChild(ge(n,t))}var X={CLEANING:"#0EA5E9",FEATURE_ENGINEERING:"#10B981",ANALYSIS:"#F59E0B",COMPARISON:"#F43F5E",DATA_LOADING:"#8B5CF6",AGGREGATION:"#8B5CF6"};function Q(n,t={}){let e=document.createElement("div");if(e.className="pw-node-card",t.collapsed&&e.classList.add("pw-node-card-collapsed"),t.selected&&e.classList.add("pw-node-card-selected"),e.dataset.stateId=n.stateId,e.style.setProperty("--category-color",X[n.operation.category]??"#64748B"),t.collapsed){let i=document.createElement("div");return i.className="pw-node-title",i.textContent=n.operation.name,e.appendChild(i),t.onSelect&&(e.style.cursor="pointer",e.title="Click to expand",e.addEventListener("click",t.onSelect)),e}let o=document.createElement("div");o.className="pw-op-badge",o.textContent=n.operation.category.replace("_"," "),e.appendChild(o);let r=document.createElement("div");if(r.className="pw-node-title",r.textContent=n.operation.name,e.appendChild(r),n.annotations.length===0){let i=document.createElement("div");i.className="pw-node-no-annotations",i.textContent="No annotations on this state yet.",e.appendChild(i)}return n.annotations.forEach(i=>{let a=document.createElement("div");a.className="pw-annotation-block";let s=document.createElement("div");s.className="pw-annotation-title",s.textContent=i.title,a.appendChild(s);let c=document.createElement("div");c.className="pw-node-note",c.textContent=i.note,a.appendChild(c);let l=document.createElement("div");l.className="pw-node-tags",i.tags.forEach(p=>l.appendChild(B(p))),a.appendChild(l);let m=document.createElement("div");m.className="pw-node-artifacts",i.artifacts.forEach(p=>m.appendChild(_(p))),a.appendChild(m),e.appendChild(a)}),t.onSelect&&(e.style.cursor="pointer",e.title="Click to expand",e.addEventListener("click",t.onSelect)),e}function he(n,t){let e=[],o=t;for(;o;){let r=n.nodes[o];e.unshift(r),o=r.parentStateId}return e}function V(n,t){n.innerHTML="";let e=t.get("tree"),r=(t.get("selection")??{}).node?.pickedStateId;if(r===void 0){let p=document.createElement("div");p.className="pw-inspect-empty",p.textContent="Click a node in the Tree tab to inspect it here.",n.appendChild(p);return}let i=r,a=e.nodes[i],s=document.createElement("div");s.className="pw-breadcrumb",s.textContent=he(e,i).map(p=>p.operation.commandName).join(" -> "),n.appendChild(s);let c=document.createElement("div");c.className="pw-diff-pane",a.delta.columnsAdded.forEach(p=>{let g=document.createElement("div");g.className="pw-diff-line pw-diff-added",g.textContent=`+ ${p.name}: ${p.dtype}`,c.appendChild(g)}),a.delta.columnsRemoved.forEach(p=>{let g=document.createElement("div");g.className="pw-diff-line pw-diff-removed",g.textContent=`- ${p.name}: ${p.dtype}`,c.appendChild(g)});let l=document.createElement("div");l.className="pw-diff-line",l.textContent=`rows: ${a.delta.rowCountBefore} -> ${a.delta.rowCountAfter}`,c.appendChild(l),n.appendChild(c);let m=document.createElement("div");m.className="pw-annotation-pane",a.annotations.forEach(p=>{let g=document.createElement("div");g.className="pw-node-title",g.textContent=p.title,m.appendChild(g);let y=document.createElement("div");y.className="pw-node-note",y.textContent=p.note,m.appendChild(y);let b=document.createElement("div");b.className="pw-node-tags",p.tags.forEach(v=>b.appendChild(B(v))),m.appendChild(b)}),n.appendChild(m)}function we(n){let t=new Map;for(let e of Object.values(n.nodes))for(let o of e.annotations)for(let r of o.artifacts)t.set(r.artifactId,r.artifactName);return[...t.entries()].map(([e,o])=>({id:e,name:o}))}function ve(n){let t=new Set;for(let e of Object.values(n.nodes))for(let o of e.annotations)for(let r of o.tags)t.add(r.name);return[...t].sort()}function xe(n,t){return n.annotations.some(e=>e.artifacts.some(o=>o.artifactId===t))}function Ee(n,t){return n.annotations.some(e=>e.tags.some(o=>o.name===t))}function Z(n,t){n.innerHTML="";let e=t.get("tree"),o=t.get("commits")??[];for(let{stateId:h,annotation:d}of o){let f=e.nodes[h];f&&(f.annotations.some(x=>x.annotationId===d.annotationId)||(f.annotations=[...f.annotations,d]))}let r=document.createElement("div");r.className="pw-tree-header";let i=document.createElement("div");i.className="pw-tree-title",i.textContent="Provenance Tree",r.appendChild(i);let a=document.createElement("div");a.className="pw-tree-caption",a.textContent="Click a card to drill into its branch and inspect it. Use the filters below to spotlight matching steps \u2014 the rest collapse to a single line.",r.appendChild(a),n.appendChild(r);let s=new Set,c=null,l=document.createElement("div");l.className="pw-filter-bar",n.appendChild(l);let m=document.createElement("div");m.className="pw-filter-row";let p=document.createElement("span");p.className="pw-filter-label",p.textContent="Tags",m.appendChild(p),l.appendChild(m);let g=document.createElement("div");g.className="pw-filter-row";let y=document.createElement("span");y.className="pw-filter-label",y.textContent="Linked artifact",g.appendChild(y),l.appendChild(g);let b=document.createElement("div");b.className="pw-breadcrumb",n.appendChild(b);let v=document.createElement("div");v.className="pw-columns-scroll",n.appendChild(v);let w=document.createElement("div");w.className="pw-columns",v.appendChild(w);let I=document.createElement("div");I.className="pw-tree-inspect-pane";let M=document.createElement("div");M.className="pw-tree-inspect-pane-label",M.textContent="Inspect",I.appendChild(M);let L=document.createElement("div");I.appendChild(L),n.appendChild(I);let C=t;(!C._pwTreePath||C._pwTreePath[0]!==e.rootId)&&(C._pwTreePath=[e.rootId]);let u=C._pwTreePath;function E(h){let d=s.size===0||[...s].some(x=>Ee(h,x)),f=c===null||xe(h,c);return d&&f}function J(){return s.size>0||c!==null}function de(){if((t.get("selection")??{}).node?.pickedStateId===void 0){L.classList.add("pw-tree-inspect-pane-empty"),L.textContent="Click a card above to inspect it here.";return}L.classList.remove("pw-tree-inspect-pane-empty"),V(L,t)}function D(){m.querySelectorAll(".pw-filter-chip").forEach(d=>d.remove());let h=ve(e);if(h.length===0){let d=document.createElement("span");d.className="pw-filter-chip pw-filter-chip-empty",d.textContent="none yet",m.appendChild(d);return}h.forEach(d=>{let f=document.createElement("button");f.className=s.has(d)?"pw-filter-chip pw-filter-chip-active":"pw-filter-chip",f.textContent=`#${d}`,f.addEventListener("click",()=>{s.has(d)?s.delete(d):s.add(d),D(),F()}),m.appendChild(f)})}function G(){g.querySelectorAll(".pw-filter-chip").forEach(d=>d.remove());let h=we(e);if(h.length===0){let d=document.createElement("span");d.className="pw-filter-chip pw-filter-chip-empty",d.textContent="none yet",g.appendChild(d);return}h.forEach(d=>{let f=document.createElement("button");f.className=c===d.id?"pw-filter-chip pw-filter-chip-active":"pw-filter-chip",f.textContent=d.name,f.addEventListener("click",()=>{c=c===d.id?null:d.id,G(),F()}),g.appendChild(f)})}function q(){b.innerHTML="",u.forEach((h,d)=>{let f=e.nodes[h],x=document.createElement("span");if(x.className="pw-breadcrumb-item",x.textContent=f.operation.name,b.appendChild(x),d<u.length-1){let k=document.createElement("span");k.className="pw-breadcrumb-sep",k.textContent=">",b.appendChild(k)}})}function j(h,d){let f=d.get(h);if(f!==void 0)return f;let x=e.nodes[h],k=x.parentStateId?j(x.parentStateId,d)+1:0;return d.set(h,k),k}function pe(h){w.querySelector("svg.pw-tree-lines")?.remove();let d=document.createElementNS("http://www.w3.org/2000/svg","svg");d.setAttribute("class","pw-tree-lines"),d.setAttribute("width",String(w.scrollWidth)),d.setAttribute("height",String(w.scrollHeight));let f=w.getBoundingClientRect();for(let[x,k]of h){let P=e.nodes[x],T=P.parentStateId?h.get(P.parentStateId):void 0;if(!T)continue;let A=T.getBoundingClientRect(),N=k.getBoundingClientRect(),$=A.right-f.left+w.scrollLeft,R=A.top+A.height/2-f.top+w.scrollTop,S=N.left-f.left+w.scrollLeft,H=N.top+N.height/2-f.top+w.scrollTop,W=($+S)/2,O=document.createElementNS("http://www.w3.org/2000/svg","path");O.setAttribute("d",`M ${$} ${R} C ${W} ${R}, ${W} ${H}, ${S} ${H}`),O.setAttribute("class",u.includes(x)&&u.includes(P.parentStateId??"")?"pw-tree-line-active":"pw-tree-line"),d.appendChild(O)}w.insertBefore(d,w.firstChild)}function F(){w.innerHTML="";let d=(t.get("selection")??{}).node?.pickedStateId,f=new Map,x=new Map,k=0;for(let T of Object.values(e.nodes)){let A=j(T.stateId,f);k=Math.max(k,A),(x.get(A)??x.set(A,[]).get(A)).push(T)}let P=new Map;for(let T=0;T<=k;T++){let A=document.createElement("div");A.className="pw-column",(x.get(T)??[]).forEach(N=>{let R=!u.includes(N.stateId)||J()&&!E(N),S=Q(N,{collapsed:R,selected:N.stateId===d,onSelect:()=>{u.splice(T,u.length,N.stateId);let H=t.get("selection")??{};t.set("selection",{...H,node:{pickedStateId:N.stateId}}),t.save_changes(),q(),F()}});P.set(N.stateId,S),A.appendChild(S)}),w.appendChild(A)}pe(P),de()}D(),G(),q(),F()}var be=["student","reviewer"];function ee(n,t){n.innerHTML="";let e=document.createElement("div");e.className="pw-config-heading",e.textContent="Persona",n.appendChild(e);let o=t.get("mode");be.forEach(r=>{let i=document.createElement("label");i.className="pw-config-radio";let a=document.createElement("input");a.type="radio",a.name="pw-persona",a.value=r,a.checked=o===r,a.addEventListener("change",()=>{t.set("mode",r),t.save_changes()}),i.appendChild(a);let s=document.createElement("span");s.textContent=r,i.appendChild(s),n.appendChild(i)})}var ne=[];function oe(n){ne.push(n)}function ae(n,t){for(let e of ne){let o=e.resolve(n,t);if(o)return o}return null}function te(n,t){return t!==null&&n.includes(t)}oe({name:"html-table",resolve(n,t){let e=t.closest("td");if(e&&te(n,e)){let r=e.parentElement,i=r?Array.from(r.children).indexOf(e):-1,a=e.closest("table"),s=null;a&&i>=0&&(s=a.querySelector("thead tr")?.children[i]?.textContent?.trim()??null);let c=r?.parentElement,l=r&&c?Array.from(c.children).indexOf(r):-1;return{granularity:"cell",detail:s?`row ${l}, column ${s}`:`row ${l}`}}let o=t.closest("th");return o&&te(n,o)?{granularity:"column",detail:o.textContent?.trim()??null}:null}});oe({name:"svg-mark",resolve(n){for(let t of n){if(!(t instanceof Element)||t.closest("svg")===null&&t.tagName.toLowerCase()!=="svg")continue;let e=t.getAttribute("data-pw-detail");if(e)return{granularity:t.getAttribute("data-pw-granularity")??"mark",detail:e};let o=t.getAttribute("aria-label");if(o&&t.getAttribute("role")==="graphics-symbol")return{granularity:"mark",detail:o}}return null}});function re(n){let t=n;for(;t;){if(t.dataset&&t.dataset.pwArtifactId)return t;if(t.parentElement)t=t.parentElement;else{let e=t.getRootNode();t=e instanceof ShadowRoot?e.host:null}}return null}function ye(n,t,e){let o=ae(e,t);return o||{granularity:n.dataset.pwGranularity??"dataset",detail:n.dataset.pwDetail??null}}function ie(n,t,e){let o=null;function r(c){let l=re(c.composedPath()[0]??c.target);o&&o!==l&&o.classList.remove("pw-picker-hover"),l&&l.classList.add("pw-picker-hover"),o=l}function i(c){let l=c.composedPath(),m=l[0]??c.target,p=re(m),g=m;if(s(),!p){e?.();return}c.preventDefault(),c.stopPropagation();let{granularity:y,detail:b}=ye(p,g,l);t({artifactId:p.dataset.pwArtifactId,artifactName:p.dataset.pwArtifactName??"",granularity:y,detail:b})}function a(c){c.key==="Escape"&&(s(),e?.())}function s(){document.removeEventListener("mousemove",r,!0),document.removeEventListener("click",i,!0),document.removeEventListener("keydown",a,!0),o&&o.classList.remove("pw-picker-hover")}document.addEventListener("mousemove",r,!0),document.addEventListener("click",i,!0),document.addEventListener("keydown",a,!0)}function se(n){let t=n;for(;t;){if(t.dataset&&t.dataset.pwArtifactId)return t;if(t.parentElement)t=t.parentElement;else{let e=t.getRootNode();t=e instanceof ShadowRoot?e.host:null}}return null}var ce=new WeakSet;function le(n,t){if(ce.has(n))return;ce.add(n);let e=null,o=null;function r(){return e||(e=document.createElement("button"),e.className="pw-gutter-plus",e.textContent="+",e.style.position="fixed",e.style.display="none",e.addEventListener("click",s=>{if(s.preventDefault(),s.stopPropagation(),!o)return;let c=o.dataset.pwGranularity??"dataset",l=o.dataset.pwDetail??null;t({artifactId:o.dataset.pwArtifactId,artifactName:o.dataset.pwArtifactName??"",granularity:c,detail:l}),i()}),document.body.appendChild(e),e)}function i(){e&&(e.style.display="none"),o=null}function a(s){let c=r();o=s;let l=s.getBoundingClientRect();c.style.display="block",c.style.top=`${l.top}px`,c.style.left=`${l.right-20}px`}document.addEventListener("mouseover",s=>{if(n.get("mode")!=="student"||document.body.classList.contains("pw-picking-active")){i();return}let c=se(s.composedPath()[0]??s.target);c&&a(c)}),document.addEventListener("mouseout",s=>{if(!o)return;let c=s.relatedTarget;c&&(o.contains(c)||c===e||e?.contains(c))||se(s.composedPath()[0]??s.target)!==o||i()})}function Ce({model:n,el:t}){t._pwTreePortal?.remove(),t.innerHTML="";let e=document.createElement("div");e.className="pw-root",t.appendChild(e);let o=document.createElement("div");o.className="pw-content",e.appendChild(o);let r=document.createElement("div");o.appendChild(r);let i=document.createElement("div");i.className="pw-restore-banner",i.style.display="none",o.appendChild(i);let a=document.createElement("div");a.className="pw-body",o.appendChild(a);let s=80;new ResizeObserver(u=>{let E=u[0]?.contentRect.width??e.clientWidth;e.classList.toggle("pw-collapsed",E<s),v.style.display!=="none"&&L()}).observe(e);let l=document.createElement("div");l.className="pw-picker-row";let m=document.createElement("button");m.className="pw-pick-btn",m.textContent="Pick from notebook";let p=!1;function g(){p=!1,m.classList.remove("pw-pick-btn-armed"),m.textContent="Pick from notebook",document.body.classList.remove("pw-picking-active")}m.addEventListener("click",()=>{p||(p=!0,m.classList.add("pw-pick-btn-armed"),m.textContent="Picking\u2026 (Esc to cancel)",document.body.classList.add("pw-picking-active"),ie(n,u=>{g();let E=n.get("selection")??{};n.set("selection",{...E,artifact:u}),n.save_changes(),C()},()=>{g()}))}),l.appendChild(m);function y(){r.innerHTML="";let u=n.get("active_tab");r.appendChild(U([{value:"curated",label:"Annotations"},{value:"tree",label:"Tree"},{value:"config",label:"Config"}],u,E=>{n.set("active_tab",E),n.save_changes()}))}function b(){let u=n.get("restore_request");n.get("mode")==="reviewer"&&u&&u.tag?(i.style.display="block",i.textContent=`Restoring parameters to @${u.tag} \u2014 dependent marimo cells will re-run reactively.`):i.style.display="none"}let v=document.createElement("div");v.className="pw-tree-portal",Object.assign(v.style,{display:"none",position:"fixed",top:"0",right:"0",bottom:"0",zIndex:"900",background:matchMedia("(prefers-color-scheme: dark)").matches?"#0F172A":"#F8FAFC",color:matchMedia("(prefers-color-scheme: dark)").matches?"#F8FAFC":"#0F172A",borderLeft:`1px solid ${matchMedia("(prefers-color-scheme: dark)").matches?"#334155":"#E2E8F0"}`,padding:"16px 24px",overflow:"hidden",fontFamily:"ui-sans-serif, system-ui, sans-serif"});let w=document.createElement("div"),I=t.getRootNode();I instanceof ShadowRoot&&(I.adoptedStyleSheets.forEach(u=>{let E=document.createElement("style");E.textContent=[...u.cssRules].map(J=>J.cssText).join(`
|
|
2
|
+
`),w.appendChild(E)}),I.querySelectorAll("style").forEach(u=>{w.appendChild(u.cloneNode(!0))})),v.appendChild(w);let M=document.createElement("div");Object.assign(M.style,{display:"flex",flexDirection:"column",height:"100%",minHeight:"0"}),v.appendChild(M),document.body.appendChild(v),t._pwTreePortal=v;function L(){let u=e.getBoundingClientRect();v.style.left=`${u.right}px`}function C(){let u=n.get("active_tab");if(a.innerHTML="",u==="tree"&&t.offsetParent!==null){L(),v.style.display="block",Z(M,n);return}v.style.display="none",M.innerHTML="";let E=document.createElement("div");if(a.appendChild(E),u==="config"){ee(E,n);return}K(E,n),n.get("mode")==="student"&&a.appendChild(l)}window.addEventListener("resize",()=>{v.style.display!=="none"&&L()}),y(),b(),C(),n.on("change:active_tab",()=>{y(),C()}),n.on("change:mode",()=>{C(),b()}),n.on("change:tree",C),n.on("change:commits",C),n.on("change:restore_request",b),le(n,u=>{n.get("active_tab")!=="curated"&&n.set("active_tab","curated");let E=n.get("selection")??{};n.set("selection",{...E,artifact:u}),n.save_changes(),y(),C(),a.querySelector(".pw-composer textarea")?.focus()})}var et={render:Ce};export{et as default};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import anywidget
|
|
6
|
+
import traitlets
|
|
7
|
+
|
|
8
|
+
from provenance_widget.interfaces import ProvenanceSource
|
|
9
|
+
from provenance_widget.serialize import tree_to_json
|
|
10
|
+
|
|
11
|
+
_STATIC_DIR = pathlib.Path(__file__).parent / "static"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProvenanceWidget(anywidget.AnyWidget):
|
|
15
|
+
_esm = _STATIC_DIR / "widget.js"
|
|
16
|
+
_css = _STATIC_DIR / "widget.css"
|
|
17
|
+
|
|
18
|
+
tree = traitlets.Dict({}).tag(sync=True)
|
|
19
|
+
mode = traitlets.Unicode("student").tag(sync=True)
|
|
20
|
+
active_tab = traitlets.Unicode("curated").tag(sync=True)
|
|
21
|
+
selection = traitlets.Dict({}).tag(sync=True)
|
|
22
|
+
commits = traitlets.List([]).tag(sync=True)
|
|
23
|
+
restore_request = traitlets.Dict({}).tag(sync=True)
|
|
24
|
+
restore_ack = traitlets.Dict({}).tag(sync=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProvenancePanel:
|
|
28
|
+
"""Python-side façade wrapping one ProvenanceWidget instance."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, source: ProvenanceSource, mode: str = "student"):
|
|
31
|
+
self._source = source
|
|
32
|
+
self.widget = ProvenanceWidget(tree=tree_to_json(source.get_tree()), mode=mode)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def source(self) -> ProvenanceSource:
|
|
36
|
+
return self._source
|
|
37
|
+
|
|
38
|
+
def sidebar(self) -> ProvenanceWidget:
|
|
39
|
+
return self.widget
|
|
40
|
+
|
|
41
|
+
def tree_surface(self) -> ProvenanceWidget:
|
|
42
|
+
return self.widget
|
|
43
|
+
|
|
44
|
+
def refresh(self) -> None:
|
|
45
|
+
"""Re-pull the tree from the source.
|
|
46
|
+
|
|
47
|
+
For sources whose history grows over time (e.g. a live pmprov
|
|
48
|
+
RuntimeTracker being driven by an in-progress notebook run, as
|
|
49
|
+
opposed to MockProvenanceSource's fixed snapshot), call this after
|
|
50
|
+
new steps have been recorded so the sidebar reflects them.
|
|
51
|
+
"""
|
|
52
|
+
self.widget.tree = tree_to_json(self._source.get_tree())
|
|
53
|
+
# Belt-and-braces: under marimo (unlike Jupyter), a Dict trait set
|
|
54
|
+
# from a cell that isn't itself re-running doesn't reliably push a
|
|
55
|
+
# fresh sync message to an already-displayed widget — force one.
|
|
56
|
+
self.widget.send_state(["tree"])
|
|
57
|
+
|
|
58
|
+
def commit_annotation(self, annotation_dict: dict, state_id: str | None = None) -> None:
|
|
59
|
+
"""Record an annotation, associating it with an analysis state.
|
|
60
|
+
|
|
61
|
+
Pass state_id explicitly to force it. Otherwise this resolves it
|
|
62
|
+
from the annotation's own targeted artifact(s), via the source's
|
|
63
|
+
`state_for_artifact()` — the state that contains/produced the
|
|
64
|
+
artifact the annotation is actually about. Falls back to "python"
|
|
65
|
+
if no artifact resolves (e.g. a general note not tied to output).
|
|
66
|
+
"""
|
|
67
|
+
if state_id is None:
|
|
68
|
+
for ref in annotation_dict.get("artifacts") or []:
|
|
69
|
+
resolved = self._source.state_for_artifact(ref.get("artifactId", ""))
|
|
70
|
+
if resolved:
|
|
71
|
+
state_id = resolved
|
|
72
|
+
break
|
|
73
|
+
if state_id is None:
|
|
74
|
+
state_id = "python"
|
|
75
|
+
self.widget.commits = [*self.widget.commits, {"stateId": state_id, "annotation": annotation_dict}]
|
|
76
|
+
|
|
77
|
+
def request_restore(self, tag: str) -> None:
|
|
78
|
+
self.widget.restore_request = {"tag": tag}
|
|
79
|
+
|
|
80
|
+
def acknowledge_restore(self) -> None:
|
|
81
|
+
self.widget.restore_ack = {"acknowledged": True}
|
|
82
|
+
self.widget.restore_request = {}
|