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,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,5 @@
1
+ """Interactive configuration builder for DataEval Workflows.
2
+
3
+ The TUI (Textual) app requires ``pip install dataeval-flow[app]``.
4
+ The lightweight CLI builder uses ``click`` (included with ``datasets``).
5
+ """
@@ -0,0 +1,5 @@
1
+ """Model layer for the configuration builder.
2
+
3
+ Pure data structures, business logic, and schema introspection.
4
+ No UI framework dependencies.
5
+ """
@@ -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
+ ]