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,280 @@
1
+ """Pydantic schema introspection for dynamic form generation.
2
+
3
+ Walks Pydantic model fields and produces ``FieldDescriptor`` objects that
4
+ the TUI can render as appropriate widgets (inputs, selects, checkboxes, etc.)
5
+ without hard-coding knowledge of each workflow's parameter model.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import collections.abc
11
+ import typing
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+ from typing import Any, get_args, get_origin
15
+
16
+ from pydantic import BaseModel
17
+ from pydantic.fields import FieldInfo
18
+ from pydantic_core import PydanticUndefinedType
19
+
20
+
21
+ class FieldKind(Enum):
22
+ """Widget hint for the TUI layer."""
23
+
24
+ STRING = "string"
25
+ INT = "int"
26
+ FLOAT = "float"
27
+ BOOL = "bool"
28
+ SELECT = "select"
29
+ MULTI_SELECT = "multi_select"
30
+ NESTED = "nested"
31
+ LIST = "list"
32
+
33
+
34
+ @dataclass
35
+ class FieldDescriptor:
36
+ """Metadata extracted from a single Pydantic field."""
37
+
38
+ name: str
39
+ kind: FieldKind
40
+ description: str
41
+ required: bool
42
+ default: Any = None
43
+ choices: list[str] = field(default_factory=list)
44
+ constraints: dict[str, Any] = field(default_factory=dict)
45
+ nested_model: type[BaseModel] | None = None
46
+ item_descriptors: list[FieldDescriptor] | None = None # for lists of nested models
47
+ discriminator: str | None = None # for discriminated unions
48
+ union_variants: dict[str, type[BaseModel]] | None = None # discriminator value -> model
49
+
50
+
51
+ def _unwrap_optional(annotation: Any) -> tuple[Any, bool]:
52
+ """Strip ``Optional[X]`` / ``X | None`` and return (inner_type, is_optional)."""
53
+ import types
54
+
55
+ origin = get_origin(annotation)
56
+ if origin is typing.Union or isinstance(annotation, types.UnionType):
57
+ args = [a for a in get_args(annotation) if a is not type(None)]
58
+ if len(args) == 1:
59
+ return args[0], True
60
+ return annotation, False
61
+ return annotation, False
62
+
63
+
64
+ def _extract_literal_values(annotation: Any) -> list[str] | None:
65
+ """Return literal values if annotation is ``Literal[...]``."""
66
+ if get_origin(annotation) is typing.Literal:
67
+ return [str(v) for v in get_args(annotation)]
68
+ return None
69
+
70
+
71
+ def _is_pydantic_model(tp: Any) -> bool:
72
+ return isinstance(tp, type) and issubclass(tp, BaseModel)
73
+
74
+
75
+ def _extract_constraints(field_info: FieldInfo) -> dict[str, Any]:
76
+ """Pull numeric constraints (ge, le, gt, lt) from field metadata."""
77
+ constraints: dict[str, Any] = {}
78
+ for meta in field_info.metadata:
79
+ for attr in ("ge", "le", "gt", "lt", "min_length", "max_length"):
80
+ val = getattr(meta, attr, None)
81
+ if val is not None:
82
+ constraints[attr] = val
83
+ return constraints
84
+
85
+
86
+ def _resolve_discriminated_union(
87
+ annotation: Any,
88
+ field_info: FieldInfo,
89
+ ) -> tuple[str | None, dict[str, type[BaseModel]] | None]:
90
+ """Detect discriminated unions and return (discriminator_field, {value: model})."""
91
+ disc = field_info.discriminator
92
+ if disc is None:
93
+ return None, None
94
+
95
+ disc_field = disc if isinstance(disc, str) else None
96
+ if disc_field is None:
97
+ return None, None
98
+
99
+ # The annotation is Annotated[Union[A, B, ...], Field(discriminator=...)]
100
+ inner = annotation
101
+ if get_origin(annotation) is typing.Annotated:
102
+ inner = get_args(annotation)[0]
103
+
104
+ import types as _types
105
+
106
+ union_args = get_args(inner) if (get_origin(inner) is typing.Union or isinstance(inner, _types.UnionType)) else []
107
+ variants: dict[str, type[BaseModel]] = {}
108
+ for variant in union_args:
109
+ if _is_pydantic_model(variant):
110
+ disc_info = variant.model_fields.get(disc_field)
111
+ if disc_info and disc_info.default is not None:
112
+ variants[str(disc_info.default)] = variant
113
+
114
+ return disc_field, variants if variants else None
115
+
116
+
117
+ _PRIMITIVE_KINDS: dict[type, FieldKind] = {
118
+ bool: FieldKind.BOOL,
119
+ int: FieldKind.INT,
120
+ float: FieldKind.FLOAT,
121
+ }
122
+
123
+
124
+ def _introspect_list_field(
125
+ inner: Any,
126
+ name: str,
127
+ description: str,
128
+ required: bool,
129
+ default: Any,
130
+ constraints: dict[str, Any],
131
+ ) -> FieldDescriptor | None:
132
+ """Attempt to classify a list/Sequence annotation, returning a descriptor or None."""
133
+ list_args = get_args(inner)
134
+ if list_args:
135
+ list_inner = list_args[0]
136
+ list_literals = _extract_literal_values(list_inner)
137
+ if list_literals:
138
+ return FieldDescriptor(
139
+ name=name,
140
+ kind=FieldKind.MULTI_SELECT,
141
+ description=description,
142
+ required=required,
143
+ default=default,
144
+ choices=list_literals,
145
+ constraints=constraints,
146
+ )
147
+ if _is_pydantic_model(list_inner):
148
+ return FieldDescriptor(
149
+ name=name,
150
+ kind=FieldKind.LIST,
151
+ description=description,
152
+ required=required,
153
+ default=default,
154
+ nested_model=list_inner,
155
+ constraints=constraints,
156
+ )
157
+ # Check for list of discriminated union
158
+ if list_args and get_origin(list_args[0]) is typing.Annotated:
159
+ ann_args = get_args(list_args[0])
160
+ union_type = ann_args[0]
161
+ field_meta = [a for a in ann_args[1:] if isinstance(a, FieldInfo)]
162
+ import types as _types
163
+
164
+ if field_meta and (get_origin(union_type) is typing.Union or isinstance(union_type, _types.UnionType)):
165
+ d_field, d_variants = _resolve_discriminated_union(list_args[0], field_meta[0])
166
+ if d_field and d_variants:
167
+ return FieldDescriptor(
168
+ name=name,
169
+ kind=FieldKind.LIST,
170
+ description=description,
171
+ required=required,
172
+ default=default,
173
+ discriminator=d_field,
174
+ union_variants=d_variants,
175
+ constraints=constraints,
176
+ )
177
+ return None
178
+
179
+
180
+ def introspect_model(model: type[BaseModel]) -> list[FieldDescriptor]:
181
+ """Walk a Pydantic model and return field descriptors for each field."""
182
+ descriptors: list[FieldDescriptor] = []
183
+
184
+ for name, field_info in model.model_fields.items():
185
+ annotation = field_info.annotation
186
+ if annotation is None:
187
+ continue
188
+
189
+ inner, is_optional = _unwrap_optional(annotation)
190
+
191
+ required = field_info.is_required()
192
+ raw_default = field_info.default
193
+ default = None if required or isinstance(raw_default, PydanticUndefinedType) else raw_default
194
+ description = field_info.description or ""
195
+ constraints = _extract_constraints(field_info)
196
+
197
+ # Check for discriminated union first
198
+ disc_field, union_variants = _resolve_discriminated_union(annotation, field_info)
199
+ if disc_field and union_variants:
200
+ descriptors.append(
201
+ FieldDescriptor(
202
+ name=name,
203
+ kind=FieldKind.NESTED,
204
+ description=description,
205
+ required=required,
206
+ default=default,
207
+ discriminator=disc_field,
208
+ union_variants=union_variants,
209
+ )
210
+ )
211
+ continue
212
+
213
+ # Literal -> select
214
+ literal_vals = _extract_literal_values(inner)
215
+ if literal_vals:
216
+ descriptors.append(
217
+ FieldDescriptor(
218
+ name=name,
219
+ kind=FieldKind.SELECT,
220
+ description=description,
221
+ required=required,
222
+ default=str(default) if default is not None else None,
223
+ choices=literal_vals,
224
+ constraints=constraints,
225
+ )
226
+ )
227
+ continue
228
+
229
+ # list[...] / Sequence[...]
230
+ if get_origin(inner) in (list, collections.abc.Sequence):
231
+ list_desc = _introspect_list_field(inner, name, description, required, default, constraints)
232
+ if list_desc:
233
+ descriptors.append(list_desc)
234
+ continue
235
+ # Plain list
236
+ descriptors.append(
237
+ FieldDescriptor(
238
+ name=name,
239
+ kind=FieldKind.LIST,
240
+ description=description,
241
+ required=required,
242
+ default=default,
243
+ constraints=constraints,
244
+ )
245
+ )
246
+ continue
247
+
248
+ # Nested BaseModel
249
+ if _is_pydantic_model(inner):
250
+ sub_descs = introspect_model(inner)
251
+ expandable_kinds = {FieldKind.BOOL, FieldKind.INT, FieldKind.FLOAT, FieldKind.STRING, FieldKind.SELECT}
252
+ expandable = sub_descs and all(d.kind in expandable_kinds and not d.required for d in sub_descs)
253
+ descriptors.append(
254
+ FieldDescriptor(
255
+ name=name,
256
+ kind=FieldKind.NESTED,
257
+ description=description,
258
+ required=required,
259
+ default=default,
260
+ nested_model=inner,
261
+ constraints=constraints,
262
+ item_descriptors=sub_descs if expandable else None,
263
+ )
264
+ )
265
+ continue
266
+
267
+ # Primitives
268
+ kind = _PRIMITIVE_KINDS.get(inner, FieldKind.STRING)
269
+ descriptors.append(
270
+ FieldDescriptor(
271
+ name=name,
272
+ kind=kind,
273
+ description=description,
274
+ required=required,
275
+ default=default,
276
+ constraints=constraints,
277
+ )
278
+ )
279
+
280
+ return descriptors
@@ -0,0 +1,213 @@
1
+ """Item assembly, field collection, step-builder validation/coercion, and action constants.
2
+
3
+ Pure functions shared by both the TUI and CLI for building config item
4
+ dicts, collecting/coercing field values, and validating step parameters.
5
+ No UI or state dependencies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from dataeval_flow._app._model._coerce import (
14
+ _split_type_alternatives,
15
+ coerce_field_value,
16
+ coerce_value,
17
+ validate_value,
18
+ )
19
+ from dataeval_flow._app._model._introspect import FieldDescriptor
20
+ from dataeval_flow._app._model._registry import (
21
+ STEP_BUILDER_SECTIONS,
22
+ get_discriminator_field,
23
+ )
24
+
25
+ __all__ = [
26
+ "DELETE_SENTINEL",
27
+ "SKIP",
28
+ "build_item_dict",
29
+ "coerce_step_params",
30
+ "collect_field_value",
31
+ "collect_multi_select_value",
32
+ "diagnose_collect_failure",
33
+ "finalize_item",
34
+ "validate_step_params",
35
+ ]
36
+
37
+ # Sentinel indicating a field value should be omitted from the result dict.
38
+ SKIP = object()
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Sentinel for delete actions
42
+ # ---------------------------------------------------------------------------
43
+
44
+ DELETE_SENTINEL = "__DELETE__"
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Item assembly helpers
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ def finalize_item(section: str, item: dict[str, Any]) -> dict[str, Any]:
53
+ """Apply section-specific defaults to an assembled item dict.
54
+
55
+ Currently handles tasks defaulting to ``enabled=True``.
56
+ """
57
+ if section == "tasks":
58
+ item.setdefault("enabled", True)
59
+ return item
60
+
61
+
62
+ def build_item_dict(
63
+ section: str,
64
+ name: str,
65
+ variant_value: str | None,
66
+ field_values: dict[str, Any],
67
+ ) -> dict[str, Any]:
68
+ """Assemble a config item dict from its component parts.
69
+
70
+ Combines name, discriminator, and field values into a single dict,
71
+ then applies section-specific defaults via :func:`finalize_item`.
72
+ """
73
+ item: dict[str, Any] = {"name": name}
74
+ disc_field = get_discriminator_field(section)
75
+ if disc_field and variant_value:
76
+ item[disc_field] = variant_value
77
+ item.update(field_values)
78
+ return finalize_item(section, item)
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Step-builder validation and coercion (pure functions)
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ def validate_step_params(params: list[Any], values: dict[str, str | bool | None]) -> list[str]:
87
+ """Validate step parameter values. Returns a list of error messages.
88
+
89
+ *params* is a list of ``ParamInfo``-like objects with ``name``,
90
+ ``type_hint``, ``required``, and ``choices`` attributes.
91
+ *values* maps param names to raw string values (or ``None``).
92
+ Params whose name is not in *values* are skipped (widget not found).
93
+ """
94
+ errors: list[str] = []
95
+ for p in params:
96
+ if p.name not in values:
97
+ continue
98
+ val = values.get(p.name)
99
+ if p.choices:
100
+ if p.required and not val:
101
+ errors.append(f"'{p.name}' is required (select a value).")
102
+ elif p.type_hint == "bool":
103
+ pass # booleans are always valid
104
+ else:
105
+ if p.required and not val:
106
+ errors.append(f"'{p.name}' is required.")
107
+ elif val:
108
+ val_str = str(val)
109
+ alternatives = _split_type_alternatives(p.type_hint)
110
+ if not any(validate_value(val_str, alt) for alt in alternatives):
111
+ expected = " or ".join(alternatives)
112
+ errors.append(f"'{p.name}' must be {expected} (got '{val_str}').")
113
+ return errors
114
+
115
+
116
+ def coerce_step_params(params: list[Any], values: dict[str, str | bool | None]) -> dict[str, Any]:
117
+ """Coerce raw string values for step parameters. Returns the result dict.
118
+
119
+ *params* is a list of ``ParamInfo``-like objects.
120
+ *values* maps param names to raw string values (or ``None``).
121
+ Params whose name is not in *values* are skipped (widget not found).
122
+ """
123
+ result: dict[str, Any] = {}
124
+ for p in params:
125
+ if p.name not in values:
126
+ continue
127
+ val = values.get(p.name)
128
+ if p.choices:
129
+ if val:
130
+ result[p.name] = val
131
+ elif p.type_hint == "bool":
132
+ bool_val = values.get(p.name)
133
+ if isinstance(bool_val, bool):
134
+ default = p.default if isinstance(p.default, bool) else False
135
+ if bool_val != default:
136
+ result[p.name] = bool_val
137
+ else:
138
+ if val:
139
+ result[p.name] = coerce_value(str(val), p.type_hint)
140
+ return result
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Field value collection (pure logic, no widget access)
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ def collect_field_value(desc: FieldDescriptor, raw: str) -> Any:
149
+ """Coerce a raw string from a widget into a typed value.
150
+
151
+ Returns :data:`SKIP` if the value is empty/should be omitted.
152
+ """
153
+ if not raw:
154
+ return SKIP
155
+ return coerce_field_value(raw, desc)
156
+
157
+
158
+ def collect_json_value(raw: str) -> Any:
159
+ """Parse a raw JSON string. Returns :data:`SKIP` if empty."""
160
+ if not raw:
161
+ return SKIP
162
+ try:
163
+ return json.loads(raw)
164
+ except (ValueError, TypeError):
165
+ return raw
166
+
167
+
168
+ def collect_multi_select_value(
169
+ selected: list[str],
170
+ section: str,
171
+ field_name: str,
172
+ ) -> Any:
173
+ """Process a multi-select result. Returns :data:`SKIP` if empty.
174
+
175
+ For task sources, a single-element list is unwrapped to a plain string.
176
+ """
177
+ if not selected:
178
+ return SKIP
179
+ if len(selected) == 1 and section == "tasks" and field_name == "sources":
180
+ return selected[0]
181
+ return selected
182
+
183
+
184
+ def collect_bool_value(value: bool, default: Any) -> Any:
185
+ """Return *value* if it differs from *default*, otherwise :data:`SKIP`."""
186
+ resolved_default = default if isinstance(default, bool) else False
187
+ return value if value != resolved_default else SKIP
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # Validation error diagnosis (pure logic)
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ def diagnose_collect_failure(
196
+ section: str,
197
+ name: str,
198
+ steps: list[dict[str, Any]],
199
+ descriptors: list[FieldDescriptor],
200
+ result: dict[str, Any] | None,
201
+ ) -> str:
202
+ """Return a human-readable error message when ``_collect_raw`` returns None."""
203
+ if not name:
204
+ return "Name is required."
205
+ if section in STEP_BUILDER_SECTIONS and not steps:
206
+ return "Add at least one step."
207
+ disc_field = get_discriminator_field(section)
208
+ if disc_field:
209
+ return f"{disc_field.replace('_', ' ')} is required."
210
+ missing = [d.name for d in descriptors if d.required and d.name not in (result or {})]
211
+ if missing:
212
+ return f"Required: {', '.join(missing)}"
213
+ return "Invalid input."