dataeval-flow 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.
- dataeval_flow/__init__.py +93 -0
- dataeval_flow/__main__.py +149 -0
- dataeval_flow/_app/__init__.py +5 -0
- dataeval_flow/_app/_model/__init__.py +5 -0
- dataeval_flow/_app/_model/_coerce.py +126 -0
- dataeval_flow/_app/_model/_discover.py +171 -0
- dataeval_flow/_app/_model/_execution.py +108 -0
- dataeval_flow/_app/_model/_introspect.py +280 -0
- dataeval_flow/_app/_model/_item.py +213 -0
- dataeval_flow/_app/_model/_registry.py +255 -0
- dataeval_flow/_app/_model/_state.py +322 -0
- dataeval_flow/_app/_model/_undo.py +61 -0
- dataeval_flow/_app/_panes/__init__.py +35 -0
- dataeval_flow/_app/_panes/_config_pane.py +173 -0
- dataeval_flow/_app/_panes/_result_pane.py +125 -0
- dataeval_flow/_app/_panes/_task_pane.py +91 -0
- dataeval_flow/_app/_panes/_widgets.py +111 -0
- dataeval_flow/_app/_screens/__init__.py +25 -0
- dataeval_flow/_app/_screens/_base.py +242 -0
- dataeval_flow/_app/_screens/_detail.py +333 -0
- dataeval_flow/_app/_screens/_model.py +102 -0
- dataeval_flow/_app/_screens/_params.py +80 -0
- dataeval_flow/_app/_screens/_pathpicker.py +68 -0
- dataeval_flow/_app/_screens/_section.py +621 -0
- dataeval_flow/_app/_screens/_settings.py +183 -0
- dataeval_flow/_app/_viewmodel/__init__.py +15 -0
- dataeval_flow/_app/_viewmodel/_builder_vm.py +272 -0
- dataeval_flow/_app/_viewmodel/_model_vm.py +70 -0
- dataeval_flow/_app/_viewmodel/_rendering.py +189 -0
- dataeval_flow/_app/_viewmodel/_result_vm.py +210 -0
- dataeval_flow/_app/_viewmodel/_section_vm.py +224 -0
- dataeval_flow/_app/app.py +742 -0
- dataeval_flow/_app/cli.py +592 -0
- dataeval_flow/_logging.py +102 -0
- dataeval_flow/cache.py +1355 -0
- dataeval_flow/config/__init__.py +80 -0
- dataeval_flow/config/_loader.py +79 -0
- dataeval_flow/config/_merge.py +92 -0
- dataeval_flow/config/_models.py +115 -0
- dataeval_flow/config/_paths.py +85 -0
- dataeval_flow/config/schemas/__init__.py +112 -0
- dataeval_flow/config/schemas/_dataset.py +111 -0
- dataeval_flow/config/schemas/_extractor.py +119 -0
- dataeval_flow/config/schemas/_metadata.py +28 -0
- dataeval_flow/config/schemas/_preprocessor.py +18 -0
- dataeval_flow/config/schemas/_selection.py +100 -0
- dataeval_flow/config/schemas/_task.py +89 -0
- dataeval_flow/config/schemas/_workflow.py +135 -0
- dataeval_flow/dataset.py +635 -0
- dataeval_flow/embeddings.py +135 -0
- dataeval_flow/metadata.py +48 -0
- dataeval_flow/preprocessing.py +141 -0
- dataeval_flow/py.typed +0 -0
- dataeval_flow/runner.py +118 -0
- dataeval_flow/selection.py +50 -0
- dataeval_flow/workflow/__init__.py +328 -0
- dataeval_flow/workflow/_text_report.py +511 -0
- dataeval_flow/workflow/base.py +69 -0
- dataeval_flow/workflow/orchestrator.py +454 -0
- dataeval_flow/workflows/__init__.py +1 -0
- dataeval_flow/workflows/analysis/__init__.py +38 -0
- dataeval_flow/workflows/analysis/outputs.py +202 -0
- dataeval_flow/workflows/analysis/params.py +114 -0
- dataeval_flow/workflows/analysis/workflow.py +1313 -0
- dataeval_flow/workflows/cleaning/__init__.py +23 -0
- dataeval_flow/workflows/cleaning/outputs.py +200 -0
- dataeval_flow/workflows/cleaning/params.py +160 -0
- dataeval_flow/workflows/cleaning/report.py +304 -0
- dataeval_flow/workflows/cleaning/workflow.py +794 -0
- dataeval_flow/workflows/drift/__init__.py +1 -0
- dataeval_flow/workflows/drift/outputs.py +144 -0
- dataeval_flow/workflows/drift/params.py +332 -0
- dataeval_flow/workflows/drift/report.py +201 -0
- dataeval_flow/workflows/drift/workflow.py +647 -0
- dataeval_flow/workflows/ood/__init__.py +1 -0
- dataeval_flow/workflows/ood/outputs.py +134 -0
- dataeval_flow/workflows/ood/params.py +161 -0
- dataeval_flow/workflows/ood/report.py +311 -0
- dataeval_flow/workflows/ood/workflow.py +728 -0
- dataeval_flow/workflows/prioritization/__init__.py +1 -0
- dataeval_flow/workflows/prioritization/outputs.py +122 -0
- dataeval_flow/workflows/prioritization/params.py +124 -0
- dataeval_flow/workflows/prioritization/report.py +117 -0
- dataeval_flow/workflows/prioritization/workflow.py +587 -0
- dataeval_flow/workflows/splitting/__init__.py +25 -0
- dataeval_flow/workflows/splitting/outputs.py +101 -0
- dataeval_flow/workflows/splitting/params.py +61 -0
- dataeval_flow/workflows/splitting/report.py +485 -0
- dataeval_flow/workflows/splitting/workflow.py +371 -0
- dataeval_flow-0.1.0.dist-info/METADATA +305 -0
- dataeval_flow-0.1.0.dist-info/RECORD +94 -0
- dataeval_flow-0.1.0.dist-info/WHEEL +4 -0
- dataeval_flow-0.1.0.dist-info/entry_points.txt +2 -0
- dataeval_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Workflow framework - protocol, context, result, discovery."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"DatasetContext",
|
|
5
|
+
"WorkflowContext",
|
|
6
|
+
"WorkflowProtocol",
|
|
7
|
+
"WorkflowResult",
|
|
8
|
+
"get_workflow",
|
|
9
|
+
"list_workflows",
|
|
10
|
+
"run_task",
|
|
11
|
+
"run_tasks",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, Protocol, TypeVar, cast, overload, runtime_checkable
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from dataeval_flow.workflow._text_report import (
|
|
22
|
+
_WIDTH,
|
|
23
|
+
_render_config_section,
|
|
24
|
+
_render_detail_section,
|
|
25
|
+
_summary_line,
|
|
26
|
+
)
|
|
27
|
+
from dataeval_flow.workflow.orchestrator import run_task, run_tasks
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from dataeval.protocols import AnnotatedDataset
|
|
31
|
+
|
|
32
|
+
from dataeval_flow.cache import DatasetCache
|
|
33
|
+
from dataeval_flow.config.schemas import ExtractorConfig, ResultMetadata, SelectionStep
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class DatasetContext:
|
|
38
|
+
"""Per-dataset runtime context — groups a loaded dataset with its resolved configs."""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
dataset: "AnnotatedDataset[Any]"
|
|
42
|
+
extractor: "ExtractorConfig | None" = None
|
|
43
|
+
transforms: Callable | None = None
|
|
44
|
+
selection_steps: "Sequence[SelectionStep] | None" = None
|
|
45
|
+
batch_size: int | None = None
|
|
46
|
+
label_source: str | None = None
|
|
47
|
+
cache: "DatasetCache | None" = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class WorkflowContext:
|
|
52
|
+
"""Runtime context for workflow execution.
|
|
53
|
+
|
|
54
|
+
Provides per-dataset bundles and workflow-wide settings.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
dataset_contexts: "Mapping[str, DatasetContext]" = field(default_factory=dict)
|
|
58
|
+
batch_size: int | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
TMetadata = TypeVar("TMetadata", bound="ResultMetadata")
|
|
62
|
+
TData = TypeVar("TData", bound=BaseModel)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _source_lines(meta: "ResultMetadata") -> list[str]:
|
|
66
|
+
"""Render source/dataset lines for the metadata block."""
|
|
67
|
+
lines: list[str] = []
|
|
68
|
+
source_descs = meta.source_descriptions
|
|
69
|
+
if source_descs:
|
|
70
|
+
label = " Source: "
|
|
71
|
+
continuation = " " * len(label)
|
|
72
|
+
for i, desc in enumerate(source_descs):
|
|
73
|
+
lines.append(f"{label if i == 0 else continuation}{desc}")
|
|
74
|
+
elif meta.dataset_id or meta.selection_id:
|
|
75
|
+
if meta.dataset_id:
|
|
76
|
+
ds_line = f" Dataset: {meta.dataset_id}"
|
|
77
|
+
if meta.label_source:
|
|
78
|
+
ds_line += f" ({meta.label_source})"
|
|
79
|
+
lines.append(ds_line)
|
|
80
|
+
if meta.selection_id:
|
|
81
|
+
lines.append(f" Selection: {meta.selection_id}")
|
|
82
|
+
return lines
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class WorkflowResult(Generic[TMetadata, TData]):
|
|
87
|
+
"""Standardized workflow result.
|
|
88
|
+
|
|
89
|
+
``metadata`` carries the JATIC-required envelope (timestamp, tool info,
|
|
90
|
+
dataset identifiers) plus any workflow-specific extras. The orchestrator
|
|
91
|
+
populates timing and dataset fields after execution; workflows construct
|
|
92
|
+
the appropriate ``ResultMetadata`` subclass at creation time.
|
|
93
|
+
|
|
94
|
+
Parameterize with metadata and data subclasses for typed access to
|
|
95
|
+
workflow-specific fields, e.g.
|
|
96
|
+
``WorkflowResult[DataCleaningMetadata, DataCleaningOutputs]``.
|
|
97
|
+
|
|
98
|
+
The optional ``dataset`` field holds the resolved, post-selection dataset
|
|
99
|
+
used during workflow execution. This is *not* serialized by
|
|
100
|
+
:meth:`report`; it is provided purely for downstream programmatic use
|
|
101
|
+
(visualization, filtering, export).
|
|
102
|
+
|
|
103
|
+
The optional ``sources`` field maps source names to their resolved,
|
|
104
|
+
post-selection datasets. Multi-split workflows (e.g. data-analysis)
|
|
105
|
+
populate this so callers can visualize images from any split without
|
|
106
|
+
re-loading.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
name: str
|
|
110
|
+
success: bool
|
|
111
|
+
data: TData
|
|
112
|
+
metadata: TMetadata
|
|
113
|
+
errors: Sequence[str] = field(default_factory=list)
|
|
114
|
+
dataset: "AnnotatedDataset[Any] | None" = None
|
|
115
|
+
sources: "dict[str, AnnotatedDataset[Any]] | None" = None
|
|
116
|
+
|
|
117
|
+
def report(self, *, detailed: bool = True) -> str:
|
|
118
|
+
"""Return a human-readable text report.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
detailed : bool
|
|
123
|
+
When ``True`` (default), the report includes detail sections
|
|
124
|
+
for each finding. When ``False``, only the summary is shown.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
str
|
|
129
|
+
Formatted text report suitable for ``print()``.
|
|
130
|
+
"""
|
|
131
|
+
report_obj = getattr(self.data, "report", None)
|
|
132
|
+
if report_obj is None:
|
|
133
|
+
return f"{self.name}: no report available"
|
|
134
|
+
|
|
135
|
+
findings = getattr(report_obj, "findings", [])
|
|
136
|
+
lines: list[str] = []
|
|
137
|
+
lines.extend(self._title_lines(report_obj.summary))
|
|
138
|
+
lines.extend(self._metadata_lines())
|
|
139
|
+
lines.extend(self._summary_lines(findings))
|
|
140
|
+
if detailed:
|
|
141
|
+
lines.extend(self._detail_lines(findings))
|
|
142
|
+
lines.extend(_render_config_section(self.metadata.resolved_config))
|
|
143
|
+
lines.append("")
|
|
144
|
+
lines.append("=" * _WIDTH)
|
|
145
|
+
return "\n".join(lines)
|
|
146
|
+
|
|
147
|
+
def _title_lines(self, summary: str) -> list[str]:
|
|
148
|
+
"""Banner with the report summary title."""
|
|
149
|
+
lines = ["", "=" * _WIDTH]
|
|
150
|
+
lines.extend(f" {part.strip().upper()}" for part in summary.split("\n"))
|
|
151
|
+
lines.append("=" * _WIDTH)
|
|
152
|
+
return lines
|
|
153
|
+
|
|
154
|
+
def _metadata_lines(self) -> list[str]:
|
|
155
|
+
"""Human-readable metadata block with trailing separator."""
|
|
156
|
+
meta = self.metadata
|
|
157
|
+
lines: list[str] = []
|
|
158
|
+
if meta.timestamp:
|
|
159
|
+
lines.append(f" Timestamp: {meta.timestamp.isoformat()}")
|
|
160
|
+
if meta.execution_time_s is not None:
|
|
161
|
+
lines.append(f" Duration: {meta.execution_time_s:.2f}s")
|
|
162
|
+
lines.extend(_source_lines(meta))
|
|
163
|
+
if meta.model_id:
|
|
164
|
+
lines.append(f" Model: {meta.model_id}")
|
|
165
|
+
if meta.preprocessor_id:
|
|
166
|
+
lines.append(f" Preprocessor: {meta.preprocessor_id}")
|
|
167
|
+
if lines:
|
|
168
|
+
lines.append("-" * _WIDTH)
|
|
169
|
+
return lines
|
|
170
|
+
|
|
171
|
+
def _summary_lines(self, findings: list) -> list[str]:
|
|
172
|
+
"""Summary section with per-finding one-liners and health status."""
|
|
173
|
+
if not findings:
|
|
174
|
+
return [" No findings to report."]
|
|
175
|
+
|
|
176
|
+
warnings = sum(1 for f in findings if getattr(f, "severity", "info") == "warning")
|
|
177
|
+
lines = ["", " SUMMARY", " -------"]
|
|
178
|
+
lines.extend(_summary_line(f) for f in findings)
|
|
179
|
+
lines.append("")
|
|
180
|
+
if warnings:
|
|
181
|
+
lines.append(f" Health: {warnings} warning(s) [!!] — review flagged findings")
|
|
182
|
+
else:
|
|
183
|
+
lines.append(" Health: All checks passed [ok]")
|
|
184
|
+
return lines
|
|
185
|
+
|
|
186
|
+
def _detail_lines(self, findings: list) -> list[str]:
|
|
187
|
+
"""Expanded detail sections for each finding."""
|
|
188
|
+
lines: list[str] = []
|
|
189
|
+
for finding in findings:
|
|
190
|
+
lines.extend(_render_detail_section(finding))
|
|
191
|
+
return lines
|
|
192
|
+
|
|
193
|
+
@overload
|
|
194
|
+
def export(self, path: str | Path, *, fmt: Literal["json", "yaml"] = "json") -> Path: ...
|
|
195
|
+
@overload
|
|
196
|
+
def export(self, path: None = None, *, fmt: Literal["json", "yaml"] = "json") -> str: ...
|
|
197
|
+
|
|
198
|
+
def export(
|
|
199
|
+
self,
|
|
200
|
+
path: str | Path | None = None,
|
|
201
|
+
*,
|
|
202
|
+
fmt: Literal["json", "yaml"] = "json",
|
|
203
|
+
) -> str | Path:
|
|
204
|
+
"""Serialize result data to JSON or YAML.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
path : str | Path | None
|
|
209
|
+
File or directory path. If a directory, writes
|
|
210
|
+
``results.<ext>`` inside it. If ``None``, returns the
|
|
211
|
+
serialized string.
|
|
212
|
+
fmt : {"json", "yaml"}
|
|
213
|
+
Serialization format. Defaults to ``"json"``.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
str | Path
|
|
218
|
+
Serialized string when ``path`` is ``None``, otherwise the
|
|
219
|
+
``Path`` to the written file.
|
|
220
|
+
"""
|
|
221
|
+
output = self.to_dict()
|
|
222
|
+
|
|
223
|
+
if fmt == "json":
|
|
224
|
+
from json import dumps
|
|
225
|
+
|
|
226
|
+
content = dumps(output, indent=2)
|
|
227
|
+
ext = "json"
|
|
228
|
+
else:
|
|
229
|
+
from yaml import dump
|
|
230
|
+
|
|
231
|
+
content = dump(output, default_flow_style=False)
|
|
232
|
+
ext = "yaml"
|
|
233
|
+
|
|
234
|
+
if path is None:
|
|
235
|
+
return content
|
|
236
|
+
|
|
237
|
+
dest = Path(path)
|
|
238
|
+
if dest.is_dir() or not dest.suffix:
|
|
239
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
240
|
+
dest = dest / f"results.{ext}"
|
|
241
|
+
else:
|
|
242
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
|
|
244
|
+
dest.write_text(content, encoding="utf-8")
|
|
245
|
+
return dest
|
|
246
|
+
|
|
247
|
+
def to_dict(self) -> dict[str, object]:
|
|
248
|
+
"""Return the result as a plain dictionary (metadata + data fields)."""
|
|
249
|
+
return {
|
|
250
|
+
"metadata": self.metadata.model_dump(mode="json"),
|
|
251
|
+
**self.data.model_dump(),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@runtime_checkable
|
|
256
|
+
class WorkflowProtocol(Protocol[TMetadata, TData]):
|
|
257
|
+
"""Workflow protocol with schema properties."""
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def name(self) -> str:
|
|
261
|
+
"""Workflow identifier."""
|
|
262
|
+
...
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def description(self) -> str:
|
|
266
|
+
"""Human-readable description."""
|
|
267
|
+
...
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def params_schema(self) -> type[BaseModel] | None:
|
|
271
|
+
"""Pydantic model for workflow parameters, or None."""
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def output_schema(self) -> type[BaseModel]:
|
|
276
|
+
"""Pydantic model for workflow output."""
|
|
277
|
+
...
|
|
278
|
+
|
|
279
|
+
def execute(self, context: WorkflowContext, params: BaseModel | None = None) -> "WorkflowResult[TMetadata, TData]":
|
|
280
|
+
"""Execute the workflow."""
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# Workflow discovery (replaces WorkflowRegistry)
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
_WORKFLOWS: "dict[str, WorkflowProtocol[ResultMetadata, BaseModel]]" = {}
|
|
289
|
+
_initialized: bool = False
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _ensure_initialized() -> None:
|
|
293
|
+
global _initialized
|
|
294
|
+
if not _initialized:
|
|
295
|
+
from dataeval_flow.workflows.analysis.workflow import DataAnalysisWorkflow
|
|
296
|
+
from dataeval_flow.workflows.cleaning.workflow import DataCleaningWorkflow
|
|
297
|
+
from dataeval_flow.workflows.drift.workflow import DriftMonitoringWorkflow
|
|
298
|
+
from dataeval_flow.workflows.ood.workflow import OODDetectionWorkflow
|
|
299
|
+
from dataeval_flow.workflows.prioritization.workflow import DataPrioritizationWorkflow
|
|
300
|
+
from dataeval_flow.workflows.splitting.workflow import DataSplittingWorkflow
|
|
301
|
+
|
|
302
|
+
workflows = [
|
|
303
|
+
DataAnalysisWorkflow,
|
|
304
|
+
DataCleaningWorkflow,
|
|
305
|
+
DataPrioritizationWorkflow,
|
|
306
|
+
DataSplittingWorkflow,
|
|
307
|
+
DriftMonitoringWorkflow,
|
|
308
|
+
OODDetectionWorkflow,
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
for workflow in workflows:
|
|
312
|
+
wf = workflow()
|
|
313
|
+
_WORKFLOWS[wf.name] = cast("WorkflowProtocol[ResultMetadata, BaseModel]", wf)
|
|
314
|
+
_initialized = True
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_workflow(name: str) -> "WorkflowProtocol[ResultMetadata, BaseModel]":
|
|
318
|
+
"""Look up a workflow by name. Raises ValueError if unknown."""
|
|
319
|
+
_ensure_initialized()
|
|
320
|
+
if name not in _WORKFLOWS:
|
|
321
|
+
raise ValueError(f"Unknown workflow: '{name}'. Available: {list(_WORKFLOWS)}")
|
|
322
|
+
return _WORKFLOWS[name]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def list_workflows() -> list[dict[str, str]]:
|
|
326
|
+
"""Return available workflows with name + description (for discovery)."""
|
|
327
|
+
_ensure_initialized()
|
|
328
|
+
return [{"name": w.name, "description": w.description} for w in _WORKFLOWS.values()]
|