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,93 @@
|
|
|
1
|
+
"""DataEval Workflows - Data evaluation and monitoring pipelines.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from dataeval_flow import load_config, run_tasks
|
|
7
|
+
|
|
8
|
+
config = load_config(Path("/path/to/data/config.yaml"))
|
|
9
|
+
results = run_tasks(config, data_dir=Path("/path/to/data"))
|
|
10
|
+
print(results[0].report()) # text report
|
|
11
|
+
results[0].export("output/") # write result JSON
|
|
12
|
+
|
|
13
|
+
Or build a pipeline programmatically::
|
|
14
|
+
|
|
15
|
+
from dataeval_flow import (
|
|
16
|
+
PipelineConfig, HuggingFaceDatasetConfig, FlattenExtractorConfig,
|
|
17
|
+
SourceConfig, DataCleaningWorkflowConfig, TaskConfig, run_tasks,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
Discovery helpers::
|
|
21
|
+
|
|
22
|
+
>>> from dataeval_flow import list_workflows
|
|
23
|
+
>>> list_workflows()
|
|
24
|
+
[{'name': 'data-cleaning', ...}, {'name': 'drift-monitoring', ...}]
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from dataeval_flow.config import (
|
|
28
|
+
BoVWExtractorConfig,
|
|
29
|
+
CocoDatasetConfig,
|
|
30
|
+
DataCleaningTaskConfig,
|
|
31
|
+
DataCleaningWorkflowConfig,
|
|
32
|
+
DatasetProtocolConfig,
|
|
33
|
+
DriftMonitoringTaskConfig,
|
|
34
|
+
DriftMonitoringWorkflowConfig,
|
|
35
|
+
FlattenExtractorConfig,
|
|
36
|
+
HuggingFaceDatasetConfig,
|
|
37
|
+
ImageFolderDatasetConfig,
|
|
38
|
+
OnnxExtractorConfig,
|
|
39
|
+
PipelineConfig,
|
|
40
|
+
PreprocessorConfig,
|
|
41
|
+
SelectionConfig,
|
|
42
|
+
SelectionStep,
|
|
43
|
+
SourceConfig,
|
|
44
|
+
TaskConfig,
|
|
45
|
+
TorchExtractorConfig,
|
|
46
|
+
UncertaintyExtractorConfig,
|
|
47
|
+
YoloDatasetConfig,
|
|
48
|
+
export_params_schema,
|
|
49
|
+
load_config,
|
|
50
|
+
load_config_folder,
|
|
51
|
+
)
|
|
52
|
+
from dataeval_flow.dataset import load_dataset
|
|
53
|
+
from dataeval_flow.workflow import WorkflowResult, get_workflow, list_workflows, run_tasks
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
# --- Core workflow ---
|
|
57
|
+
"load_config",
|
|
58
|
+
"load_config_folder",
|
|
59
|
+
"run_tasks",
|
|
60
|
+
"PipelineConfig",
|
|
61
|
+
"WorkflowResult",
|
|
62
|
+
# --- Discovery ---
|
|
63
|
+
"list_workflows",
|
|
64
|
+
"get_workflow",
|
|
65
|
+
# --- Dataset configs ---
|
|
66
|
+
"HuggingFaceDatasetConfig",
|
|
67
|
+
"ImageFolderDatasetConfig",
|
|
68
|
+
"CocoDatasetConfig",
|
|
69
|
+
"YoloDatasetConfig",
|
|
70
|
+
"DatasetProtocolConfig",
|
|
71
|
+
# --- Extractor configs ---
|
|
72
|
+
"OnnxExtractorConfig",
|
|
73
|
+
"BoVWExtractorConfig",
|
|
74
|
+
"FlattenExtractorConfig",
|
|
75
|
+
"TorchExtractorConfig",
|
|
76
|
+
"UncertaintyExtractorConfig",
|
|
77
|
+
# --- Workflow configs ---
|
|
78
|
+
"DataCleaningWorkflowConfig",
|
|
79
|
+
"DriftMonitoringWorkflowConfig",
|
|
80
|
+
# --- Task configs ---
|
|
81
|
+
"TaskConfig",
|
|
82
|
+
"DataCleaningTaskConfig",
|
|
83
|
+
"DriftMonitoringTaskConfig",
|
|
84
|
+
# --- Composition ---
|
|
85
|
+
"SourceConfig",
|
|
86
|
+
"PreprocessorConfig",
|
|
87
|
+
"SelectionConfig",
|
|
88
|
+
"SelectionStep",
|
|
89
|
+
# --- Utilities ---
|
|
90
|
+
"load_dataset",
|
|
91
|
+
"export_params_schema",
|
|
92
|
+
]
|
|
93
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI entry point for standalone usage: python -m dataeval_flow."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import NoReturn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
"""Build the argument parser with subcommands."""
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="dataeval_flow",
|
|
15
|
+
description="DataEval Flow - Data evaluation and monitoring pipelines",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Headless execution flags (top-level, no subcommand needed)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"-v",
|
|
21
|
+
"--verbose",
|
|
22
|
+
action="count",
|
|
23
|
+
default=0,
|
|
24
|
+
help="Increase verbosity: -v text report, -vv +INFO logs, -vvv +DEBUG logs.",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-c",
|
|
28
|
+
"--config",
|
|
29
|
+
type=Path,
|
|
30
|
+
default=None,
|
|
31
|
+
help="Path to config file or folder. If omitted, auto-discovers YAML/JSON at the data root.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_data_default = os.environ.get("DATAEVAL_DATA")
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-d",
|
|
37
|
+
"--data",
|
|
38
|
+
type=Path,
|
|
39
|
+
default=Path(_data_default) if _data_default else None,
|
|
40
|
+
help="Root directory for data files (default: $DATAEVAL_DATA or current directory)",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_output_default = os.environ.get("DATAEVAL_OUTPUT")
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"-o",
|
|
46
|
+
"--output",
|
|
47
|
+
type=Path,
|
|
48
|
+
default=Path(_output_default) if _output_default else None,
|
|
49
|
+
help="Path to output directory for artifacts (default: $DATAEVAL_OUTPUT or None).",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_cache_default = os.environ.get("DATAEVAL_CACHE")
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"-k",
|
|
55
|
+
"--cache",
|
|
56
|
+
type=Path,
|
|
57
|
+
default=Path(_cache_default) if _cache_default else None,
|
|
58
|
+
help="Directory for disk-backed computation cache (default: $DATAEVAL_CACHE or None).",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
62
|
+
|
|
63
|
+
# --- app (interactive TUI) ---
|
|
64
|
+
app_parser = subparsers.add_parser(
|
|
65
|
+
"app",
|
|
66
|
+
help="Launch interactive TUI dashboard",
|
|
67
|
+
description="Launch the interactive TUI dashboard. Requires: pip install dataeval-flow[app]",
|
|
68
|
+
)
|
|
69
|
+
app_parser.add_argument(
|
|
70
|
+
"--config",
|
|
71
|
+
type=Path,
|
|
72
|
+
default=None,
|
|
73
|
+
help="Path to an existing config file or folder to load on startup",
|
|
74
|
+
)
|
|
75
|
+
app_parser.add_argument(
|
|
76
|
+
"-d",
|
|
77
|
+
"--data",
|
|
78
|
+
type=Path,
|
|
79
|
+
default=None,
|
|
80
|
+
help="Root directory for data files (default: $DATAEVAL_DATA or current directory)",
|
|
81
|
+
)
|
|
82
|
+
app_parser.add_argument(
|
|
83
|
+
"-k",
|
|
84
|
+
"--cache",
|
|
85
|
+
type=Path,
|
|
86
|
+
default=None,
|
|
87
|
+
help="Directory for disk-backed computation cache (embeddings, metadata, stats).",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# --- config (simple CLI builder) ---
|
|
91
|
+
config_parser = subparsers.add_parser(
|
|
92
|
+
"config",
|
|
93
|
+
help="Create or edit config files (simple CLI)",
|
|
94
|
+
description="Interactive CLI config builder. Create and edit pipeline config files.",
|
|
95
|
+
)
|
|
96
|
+
config_parser.add_argument(
|
|
97
|
+
"--config",
|
|
98
|
+
type=Path,
|
|
99
|
+
default=None,
|
|
100
|
+
help="Path to an existing config file or folder to load on startup",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return parser
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_args() -> argparse.Namespace:
|
|
107
|
+
"""Parse CLI arguments."""
|
|
108
|
+
parser = _build_parser()
|
|
109
|
+
return parser.parse_args()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> NoReturn:
|
|
113
|
+
"""CLI entry point."""
|
|
114
|
+
args = parse_args()
|
|
115
|
+
|
|
116
|
+
if args.command == "app":
|
|
117
|
+
try:
|
|
118
|
+
from dataeval_flow._app.app import run_builder
|
|
119
|
+
except ImportError:
|
|
120
|
+
print("ERROR: The interactive TUI requires the 'app' extra.")
|
|
121
|
+
print("")
|
|
122
|
+
print("Install with:")
|
|
123
|
+
print(" pip install dataeval-flow[app]")
|
|
124
|
+
print("")
|
|
125
|
+
print("For the simple CLI config editor, use:")
|
|
126
|
+
print(" dataeval-flow config")
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
run_builder(config_path=args.config, data_dir=args.data, cache_dir=args.cache)
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
if args.command == "config":
|
|
133
|
+
from dataeval_flow._app.cli import run_cli_builder
|
|
134
|
+
|
|
135
|
+
run_cli_builder(config_path=args.config)
|
|
136
|
+
sys.exit(0)
|
|
137
|
+
|
|
138
|
+
# Headless execution (no subcommand)
|
|
139
|
+
try:
|
|
140
|
+
from dataeval_flow.runner import run
|
|
141
|
+
|
|
142
|
+
sys.exit(run(args.config, args.output, data_dir=args.data, verbosity=args.verbose, cache_dir=args.cache))
|
|
143
|
+
except (FileNotFoundError, ValueError, ImportError) as e:
|
|
144
|
+
print(f"ERROR: {e}")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__": # pragma: no cover
|
|
149
|
+
main()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Value coercion and validation for the configuration builder.
|
|
2
|
+
|
|
3
|
+
Pure functions that convert string inputs to typed values based on
|
|
4
|
+
type hints or field descriptors. No UI or state dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from dataeval_flow._app._model._introspect import FieldDescriptor, FieldKind
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"coerce_field_value",
|
|
16
|
+
"coerce_value",
|
|
17
|
+
"validate_value",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _split_type_alternatives(type_hint: str) -> list[str]:
|
|
22
|
+
"""Split a type hint like ``"int | str"`` into alternatives."""
|
|
23
|
+
parts: list[str] = []
|
|
24
|
+
depth = 0
|
|
25
|
+
current: list[str] = []
|
|
26
|
+
for ch in type_hint:
|
|
27
|
+
if ch in ("(", "["):
|
|
28
|
+
depth += 1
|
|
29
|
+
current.append(ch)
|
|
30
|
+
elif ch in (")", "]"):
|
|
31
|
+
depth -= 1
|
|
32
|
+
current.append(ch)
|
|
33
|
+
elif ch == "|" and depth == 0:
|
|
34
|
+
parts.append("".join(current).strip())
|
|
35
|
+
current = []
|
|
36
|
+
else:
|
|
37
|
+
current.append(ch)
|
|
38
|
+
parts.append("".join(current).strip())
|
|
39
|
+
return [p for p in parts if p]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_value(value: str, hint: str) -> bool:
|
|
43
|
+
"""Check whether *value* is compatible with *hint*."""
|
|
44
|
+
h = hint.strip()
|
|
45
|
+
if h in ("any", "str", "string"):
|
|
46
|
+
return True
|
|
47
|
+
if h == "bool":
|
|
48
|
+
return value.lower() in ("true", "false", "1", "0", "yes", "no")
|
|
49
|
+
if h == "int":
|
|
50
|
+
try:
|
|
51
|
+
int(value)
|
|
52
|
+
return True
|
|
53
|
+
except ValueError:
|
|
54
|
+
return False
|
|
55
|
+
if h == "float":
|
|
56
|
+
try:
|
|
57
|
+
float(value)
|
|
58
|
+
return True
|
|
59
|
+
except ValueError:
|
|
60
|
+
return False
|
|
61
|
+
if h.startswith("list") or h.startswith("tuple"):
|
|
62
|
+
try:
|
|
63
|
+
parsed = json.loads(value)
|
|
64
|
+
return isinstance(parsed, (list, tuple))
|
|
65
|
+
except (ValueError, TypeError):
|
|
66
|
+
return False
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_COERCE_SCALAR: dict[str, type] = {"int": int, "float": float}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _try_scalar_coercion(value: str, alternatives: list[str]) -> tuple[bool, Any]:
|
|
74
|
+
"""Try int/float coercion. Returns ``(matched, result)``."""
|
|
75
|
+
for alt in alternatives:
|
|
76
|
+
converter = _COERCE_SCALAR.get(alt)
|
|
77
|
+
if converter is not None:
|
|
78
|
+
try:
|
|
79
|
+
return True, converter(value)
|
|
80
|
+
except (ValueError, TypeError):
|
|
81
|
+
continue
|
|
82
|
+
return False, value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def coerce_value(value: str, type_hint: str) -> Any:
|
|
86
|
+
"""Coerce a string *value* according to *type_hint*.
|
|
87
|
+
|
|
88
|
+
JSON parsing is only attempted for complex types (list, dict).
|
|
89
|
+
"""
|
|
90
|
+
if not value:
|
|
91
|
+
return value
|
|
92
|
+
alternatives = [a.strip() for a in _split_type_alternatives(type_hint)]
|
|
93
|
+
|
|
94
|
+
if {"str", "string"} & set(alternatives):
|
|
95
|
+
matched, result = _try_scalar_coercion(value, alternatives)
|
|
96
|
+
return result if matched else value
|
|
97
|
+
|
|
98
|
+
matched, result = _try_scalar_coercion(value, alternatives)
|
|
99
|
+
if matched:
|
|
100
|
+
return result
|
|
101
|
+
if "bool" in alternatives:
|
|
102
|
+
return value.lower() in ("true", "1", "yes")
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
return json.loads(value)
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
pass
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def coerce_field_value(value: str, desc: FieldDescriptor) -> Any:
|
|
112
|
+
"""Coerce *value* using a FieldDescriptor's kind."""
|
|
113
|
+
if not value:
|
|
114
|
+
return value
|
|
115
|
+
if desc.kind == FieldKind.INT:
|
|
116
|
+
return int(value)
|
|
117
|
+
if desc.kind == FieldKind.FLOAT:
|
|
118
|
+
return float(value)
|
|
119
|
+
if desc.kind == FieldKind.BOOL:
|
|
120
|
+
return value.lower() in ("true", "1", "yes")
|
|
121
|
+
if desc.kind in (FieldKind.LIST, FieldKind.NESTED):
|
|
122
|
+
try:
|
|
123
|
+
return json.loads(value)
|
|
124
|
+
except (ValueError, TypeError):
|
|
125
|
+
return value
|
|
126
|
+
return value
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Runtime discovery of available transforms and selection classes.
|
|
2
|
+
|
|
3
|
+
Introspects torchvision.transforms.v2 and dataeval.selection to provide
|
|
4
|
+
dropdown options and parameter schemas for the builder TUI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import collections.abc
|
|
10
|
+
import inspect
|
|
11
|
+
import types
|
|
12
|
+
import typing
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from typing import Any, get_args, get_origin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ParamInfo:
|
|
20
|
+
"""Describes a single constructor parameter for a transform or selection class."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
type_hint: str # human-readable type string
|
|
24
|
+
required: bool
|
|
25
|
+
default: Any = None
|
|
26
|
+
choices: list[str] = field(default_factory=list) # for Literal types
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_PRIMITIVE_NAMES: dict[type, str] = {int: "int", float: "float", bool: "bool", str: "str"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _simplify_union(annotation: Any) -> tuple[str, list[str]]:
|
|
33
|
+
"""Simplify a Union or Optional type annotation."""
|
|
34
|
+
args = [a for a in get_args(annotation) if a is not type(None)]
|
|
35
|
+
if len(args) == 1:
|
|
36
|
+
return _simplify_type(args[0])
|
|
37
|
+
parts = []
|
|
38
|
+
for a in args:
|
|
39
|
+
t, _ = _simplify_type(a)
|
|
40
|
+
parts.append(t)
|
|
41
|
+
return " | ".join(dict.fromkeys(parts)), []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _simplify_type(annotation: Any) -> tuple[str, list[str]]:
|
|
45
|
+
"""Convert a type annotation to a human-readable string + optional choices."""
|
|
46
|
+
if annotation is inspect.Parameter.empty or annotation is Any:
|
|
47
|
+
return "any", []
|
|
48
|
+
|
|
49
|
+
if get_origin(annotation) is typing.Literal:
|
|
50
|
+
return "select", [str(v) for v in get_args(annotation)]
|
|
51
|
+
|
|
52
|
+
origin = get_origin(annotation)
|
|
53
|
+
if origin is typing.Union or isinstance(annotation, types.UnionType):
|
|
54
|
+
return _simplify_union(annotation)
|
|
55
|
+
|
|
56
|
+
if annotation in _PRIMITIVE_NAMES:
|
|
57
|
+
return _PRIMITIVE_NAMES[annotation], []
|
|
58
|
+
|
|
59
|
+
if origin in (list, tuple, collections.abc.Sequence):
|
|
60
|
+
inner_args = get_args(annotation)
|
|
61
|
+
if inner_args:
|
|
62
|
+
inner_t, choices = _simplify_type(inner_args[0])
|
|
63
|
+
if choices:
|
|
64
|
+
return "select", choices
|
|
65
|
+
return f"list[{inner_t}]", []
|
|
66
|
+
return "list", []
|
|
67
|
+
|
|
68
|
+
return getattr(annotation, "__name__", str(annotation)), []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _introspect_params(cls: type) -> list[ParamInfo]:
|
|
72
|
+
"""Extract constructor parameters from a class."""
|
|
73
|
+
try:
|
|
74
|
+
sig = inspect.signature(cls.__init__)
|
|
75
|
+
except (ValueError, TypeError):
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
params: list[ParamInfo] = []
|
|
79
|
+
for pname, param in sig.parameters.items():
|
|
80
|
+
if pname == "self":
|
|
81
|
+
continue
|
|
82
|
+
# Skip *args/**kwargs
|
|
83
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
required = param.default is inspect.Parameter.empty
|
|
87
|
+
default = None if required else param.default
|
|
88
|
+
type_hint, choices = _simplify_type(param.annotation)
|
|
89
|
+
|
|
90
|
+
params.append(
|
|
91
|
+
ParamInfo(
|
|
92
|
+
name=pname,
|
|
93
|
+
type_hint=type_hint,
|
|
94
|
+
required=required,
|
|
95
|
+
default=default,
|
|
96
|
+
choices=choices,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return params
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@lru_cache(maxsize=1)
|
|
104
|
+
def list_transforms() -> list[str]:
|
|
105
|
+
"""Return sorted names of available torchvision.transforms.v2 classes."""
|
|
106
|
+
from torchvision.transforms import v2
|
|
107
|
+
|
|
108
|
+
skip = {
|
|
109
|
+
"Transform",
|
|
110
|
+
"Compose",
|
|
111
|
+
"Identity",
|
|
112
|
+
"RandomApply",
|
|
113
|
+
"RandomChoice",
|
|
114
|
+
"RandomOrder",
|
|
115
|
+
"AutoAugmentPolicy",
|
|
116
|
+
"InterpolationMode",
|
|
117
|
+
"Lambda",
|
|
118
|
+
}
|
|
119
|
+
names = []
|
|
120
|
+
for name, obj in inspect.getmembers(v2):
|
|
121
|
+
if (
|
|
122
|
+
inspect.isclass(obj)
|
|
123
|
+
and not name.startswith("_")
|
|
124
|
+
and name[0].isupper()
|
|
125
|
+
and name not in skip
|
|
126
|
+
and callable(obj)
|
|
127
|
+
):
|
|
128
|
+
names.append(name)
|
|
129
|
+
return sorted(set(names))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@lru_cache(maxsize=1)
|
|
133
|
+
def list_selection_classes() -> list[str]:
|
|
134
|
+
"""Return sorted names of available dataeval.selection classes."""
|
|
135
|
+
from dataeval import selection
|
|
136
|
+
from dataeval.selection._select import Selection
|
|
137
|
+
|
|
138
|
+
skip = {"Selection", "Subselection", "SelectionStage", "Select"}
|
|
139
|
+
names = []
|
|
140
|
+
for name, obj in inspect.getmembers(selection):
|
|
141
|
+
if (
|
|
142
|
+
inspect.isclass(obj)
|
|
143
|
+
and not name.startswith("_")
|
|
144
|
+
and name not in skip
|
|
145
|
+
and issubclass(obj, Selection)
|
|
146
|
+
and obj is not Selection
|
|
147
|
+
):
|
|
148
|
+
names.append(name)
|
|
149
|
+
return sorted(names)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@lru_cache(maxsize=64)
|
|
153
|
+
def get_transform_params(name: str) -> list[ParamInfo]:
|
|
154
|
+
"""Get parameter info for a torchvision.transforms.v2 class."""
|
|
155
|
+
from torchvision.transforms import v2
|
|
156
|
+
|
|
157
|
+
cls = getattr(v2, name, None)
|
|
158
|
+
if cls is None:
|
|
159
|
+
return []
|
|
160
|
+
return _introspect_params(cls)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@lru_cache(maxsize=64)
|
|
164
|
+
def get_selection_params(name: str) -> list[ParamInfo]:
|
|
165
|
+
"""Get parameter info for a dataeval.selection class."""
|
|
166
|
+
from dataeval import selection
|
|
167
|
+
|
|
168
|
+
cls = getattr(selection, name, None)
|
|
169
|
+
if cls is None:
|
|
170
|
+
return []
|
|
171
|
+
return _introspect_params(cls)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Per-task execution state tracking for the dashboard.
|
|
2
|
+
|
|
3
|
+
Ephemeral — not saved to config files. Thread-safe since Textual
|
|
4
|
+
workers run in background threads.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from dataeval_flow.workflow import WorkflowResult
|
|
16
|
+
|
|
17
|
+
__all__ = ["ExecutionState", "TaskExecution"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TaskExecution:
|
|
22
|
+
"""Runtime state for a single task execution."""
|
|
23
|
+
|
|
24
|
+
task_name: str
|
|
25
|
+
status: Literal["idle", "running", "completed", "failed"] = "idle"
|
|
26
|
+
result: WorkflowResult[Any, Any] | None = None
|
|
27
|
+
error: str | None = None
|
|
28
|
+
started_at: datetime | None = None
|
|
29
|
+
finished_at: datetime | None = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def elapsed_s(self) -> float | None:
|
|
33
|
+
"""Return elapsed seconds, or ``None`` if not completed."""
|
|
34
|
+
if self.started_at and self.finished_at:
|
|
35
|
+
return (self.finished_at - self.started_at).total_seconds()
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ExecutionState:
|
|
40
|
+
"""Tracks per-task execution status. Thread-safe."""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self._lock = threading.Lock()
|
|
44
|
+
self._tasks: dict[str, TaskExecution] = {}
|
|
45
|
+
|
|
46
|
+
def mark_running(self, name: str) -> TaskExecution:
|
|
47
|
+
"""Set task to running. Clears any previous result."""
|
|
48
|
+
with self._lock:
|
|
49
|
+
entry = TaskExecution(
|
|
50
|
+
task_name=name,
|
|
51
|
+
status="running",
|
|
52
|
+
started_at=datetime.now(timezone.utc),
|
|
53
|
+
)
|
|
54
|
+
self._tasks[name] = entry
|
|
55
|
+
return entry
|
|
56
|
+
|
|
57
|
+
def mark_completed(self, name: str, result: WorkflowResult[Any, Any]) -> TaskExecution:
|
|
58
|
+
"""Set task to completed with its result."""
|
|
59
|
+
with self._lock:
|
|
60
|
+
entry = self._tasks.get(name)
|
|
61
|
+
if entry is None:
|
|
62
|
+
entry = TaskExecution(task_name=name)
|
|
63
|
+
self._tasks[name] = entry
|
|
64
|
+
entry.status = "completed"
|
|
65
|
+
entry.result = result
|
|
66
|
+
entry.error = None
|
|
67
|
+
entry.finished_at = datetime.now(timezone.utc)
|
|
68
|
+
return entry
|
|
69
|
+
|
|
70
|
+
def mark_failed(self, name: str, error: str) -> TaskExecution:
|
|
71
|
+
"""Set task to failed with an error message."""
|
|
72
|
+
with self._lock:
|
|
73
|
+
entry = self._tasks.get(name)
|
|
74
|
+
if entry is None:
|
|
75
|
+
entry = TaskExecution(task_name=name)
|
|
76
|
+
self._tasks[name] = entry
|
|
77
|
+
entry.status = "failed"
|
|
78
|
+
entry.error = error
|
|
79
|
+
entry.result = None
|
|
80
|
+
entry.finished_at = datetime.now(timezone.utc)
|
|
81
|
+
return entry
|
|
82
|
+
|
|
83
|
+
def get(self, name: str) -> TaskExecution | None:
|
|
84
|
+
"""Return execution entry for *name*, or ``None``."""
|
|
85
|
+
with self._lock:
|
|
86
|
+
return self._tasks.get(name)
|
|
87
|
+
|
|
88
|
+
def clear(self, name: str | None = None) -> None:
|
|
89
|
+
"""Clear execution state. If *name* is ``None``, clear all."""
|
|
90
|
+
with self._lock:
|
|
91
|
+
if name is None:
|
|
92
|
+
self._tasks.clear()
|
|
93
|
+
else:
|
|
94
|
+
self._tasks.pop(name, None)
|
|
95
|
+
|
|
96
|
+
def entries(self) -> list[TaskExecution]:
|
|
97
|
+
"""Return a snapshot of all execution entries."""
|
|
98
|
+
with self._lock:
|
|
99
|
+
return list(self._tasks.values())
|
|
100
|
+
|
|
101
|
+
def completed_results(self) -> list[tuple[str, WorkflowResult[Any, Any]]]:
|
|
102
|
+
"""Return ``(name, result)`` pairs for all completed tasks."""
|
|
103
|
+
with self._lock:
|
|
104
|
+
return [
|
|
105
|
+
(e.task_name, e.result)
|
|
106
|
+
for e in self._tasks.values()
|
|
107
|
+
if e.status == "completed" and e.result is not None
|
|
108
|
+
]
|