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.
Files changed (94) hide show
  1. dataeval_flow/__init__.py +93 -0
  2. dataeval_flow/__main__.py +149 -0
  3. dataeval_flow/_app/__init__.py +5 -0
  4. dataeval_flow/_app/_model/__init__.py +5 -0
  5. dataeval_flow/_app/_model/_coerce.py +126 -0
  6. dataeval_flow/_app/_model/_discover.py +171 -0
  7. dataeval_flow/_app/_model/_execution.py +108 -0
  8. dataeval_flow/_app/_model/_introspect.py +280 -0
  9. dataeval_flow/_app/_model/_item.py +213 -0
  10. dataeval_flow/_app/_model/_registry.py +255 -0
  11. dataeval_flow/_app/_model/_state.py +322 -0
  12. dataeval_flow/_app/_model/_undo.py +61 -0
  13. dataeval_flow/_app/_panes/__init__.py +35 -0
  14. dataeval_flow/_app/_panes/_config_pane.py +173 -0
  15. dataeval_flow/_app/_panes/_result_pane.py +125 -0
  16. dataeval_flow/_app/_panes/_task_pane.py +91 -0
  17. dataeval_flow/_app/_panes/_widgets.py +111 -0
  18. dataeval_flow/_app/_screens/__init__.py +25 -0
  19. dataeval_flow/_app/_screens/_base.py +242 -0
  20. dataeval_flow/_app/_screens/_detail.py +333 -0
  21. dataeval_flow/_app/_screens/_model.py +102 -0
  22. dataeval_flow/_app/_screens/_params.py +80 -0
  23. dataeval_flow/_app/_screens/_pathpicker.py +68 -0
  24. dataeval_flow/_app/_screens/_section.py +621 -0
  25. dataeval_flow/_app/_screens/_settings.py +183 -0
  26. dataeval_flow/_app/_viewmodel/__init__.py +15 -0
  27. dataeval_flow/_app/_viewmodel/_builder_vm.py +272 -0
  28. dataeval_flow/_app/_viewmodel/_model_vm.py +70 -0
  29. dataeval_flow/_app/_viewmodel/_rendering.py +189 -0
  30. dataeval_flow/_app/_viewmodel/_result_vm.py +210 -0
  31. dataeval_flow/_app/_viewmodel/_section_vm.py +224 -0
  32. dataeval_flow/_app/app.py +742 -0
  33. dataeval_flow/_app/cli.py +592 -0
  34. dataeval_flow/_logging.py +102 -0
  35. dataeval_flow/cache.py +1355 -0
  36. dataeval_flow/config/__init__.py +80 -0
  37. dataeval_flow/config/_loader.py +79 -0
  38. dataeval_flow/config/_merge.py +92 -0
  39. dataeval_flow/config/_models.py +115 -0
  40. dataeval_flow/config/_paths.py +85 -0
  41. dataeval_flow/config/schemas/__init__.py +112 -0
  42. dataeval_flow/config/schemas/_dataset.py +111 -0
  43. dataeval_flow/config/schemas/_extractor.py +119 -0
  44. dataeval_flow/config/schemas/_metadata.py +28 -0
  45. dataeval_flow/config/schemas/_preprocessor.py +18 -0
  46. dataeval_flow/config/schemas/_selection.py +100 -0
  47. dataeval_flow/config/schemas/_task.py +89 -0
  48. dataeval_flow/config/schemas/_workflow.py +135 -0
  49. dataeval_flow/dataset.py +635 -0
  50. dataeval_flow/embeddings.py +135 -0
  51. dataeval_flow/metadata.py +48 -0
  52. dataeval_flow/preprocessing.py +141 -0
  53. dataeval_flow/py.typed +0 -0
  54. dataeval_flow/runner.py +118 -0
  55. dataeval_flow/selection.py +50 -0
  56. dataeval_flow/workflow/__init__.py +328 -0
  57. dataeval_flow/workflow/_text_report.py +511 -0
  58. dataeval_flow/workflow/base.py +69 -0
  59. dataeval_flow/workflow/orchestrator.py +454 -0
  60. dataeval_flow/workflows/__init__.py +1 -0
  61. dataeval_flow/workflows/analysis/__init__.py +38 -0
  62. dataeval_flow/workflows/analysis/outputs.py +202 -0
  63. dataeval_flow/workflows/analysis/params.py +114 -0
  64. dataeval_flow/workflows/analysis/workflow.py +1313 -0
  65. dataeval_flow/workflows/cleaning/__init__.py +23 -0
  66. dataeval_flow/workflows/cleaning/outputs.py +200 -0
  67. dataeval_flow/workflows/cleaning/params.py +160 -0
  68. dataeval_flow/workflows/cleaning/report.py +304 -0
  69. dataeval_flow/workflows/cleaning/workflow.py +794 -0
  70. dataeval_flow/workflows/drift/__init__.py +1 -0
  71. dataeval_flow/workflows/drift/outputs.py +144 -0
  72. dataeval_flow/workflows/drift/params.py +332 -0
  73. dataeval_flow/workflows/drift/report.py +201 -0
  74. dataeval_flow/workflows/drift/workflow.py +647 -0
  75. dataeval_flow/workflows/ood/__init__.py +1 -0
  76. dataeval_flow/workflows/ood/outputs.py +134 -0
  77. dataeval_flow/workflows/ood/params.py +161 -0
  78. dataeval_flow/workflows/ood/report.py +311 -0
  79. dataeval_flow/workflows/ood/workflow.py +728 -0
  80. dataeval_flow/workflows/prioritization/__init__.py +1 -0
  81. dataeval_flow/workflows/prioritization/outputs.py +122 -0
  82. dataeval_flow/workflows/prioritization/params.py +124 -0
  83. dataeval_flow/workflows/prioritization/report.py +117 -0
  84. dataeval_flow/workflows/prioritization/workflow.py +587 -0
  85. dataeval_flow/workflows/splitting/__init__.py +25 -0
  86. dataeval_flow/workflows/splitting/outputs.py +101 -0
  87. dataeval_flow/workflows/splitting/params.py +61 -0
  88. dataeval_flow/workflows/splitting/report.py +485 -0
  89. dataeval_flow/workflows/splitting/workflow.py +371 -0
  90. dataeval_flow-0.1.0.dist-info/METADATA +305 -0
  91. dataeval_flow-0.1.0.dist-info/RECORD +94 -0
  92. dataeval_flow-0.1.0.dist-info/WHEEL +4 -0
  93. dataeval_flow-0.1.0.dist-info/entry_points.txt +2 -0
  94. 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()]