flowyml 1.2.0__py3-none-any.whl → 1.3.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.
- flowyml/__init__.py +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +22 -5
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +166 -5
- flowyml/ui/backend/main.py +41 -1
- flowyml/ui/backend/routers/assets.py +356 -15
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +48 -12
- flowyml/ui/backend/routers/metrics.py +213 -0
- flowyml/ui/backend/routers/pipelines.py +63 -7
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +150 -8
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +42 -4
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +89 -52
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
flowyml/core/pipeline.py
CHANGED
|
@@ -19,11 +19,15 @@ class PipelineResult:
|
|
|
19
19
|
self.run_id = run_id
|
|
20
20
|
self.pipeline_name = pipeline_name
|
|
21
21
|
self.success = False
|
|
22
|
+
self.state = "pending"
|
|
22
23
|
self.step_results: dict[str, ExecutionResult] = {}
|
|
23
24
|
self.outputs: dict[str, Any] = {}
|
|
24
25
|
self.start_time = datetime.now()
|
|
25
26
|
self.end_time: datetime | None = None
|
|
26
27
|
self.duration_seconds: float = 0.0
|
|
28
|
+
self.resource_config: Any | None = None
|
|
29
|
+
self.docker_config: Any | None = None
|
|
30
|
+
self.remote_job_id: str | None = None
|
|
27
31
|
|
|
28
32
|
def add_step_result(self, result: ExecutionResult) -> None:
|
|
29
33
|
"""Add result from a step execution."""
|
|
@@ -37,9 +41,21 @@ class PipelineResult:
|
|
|
37
41
|
def finalize(self, success: bool) -> None:
|
|
38
42
|
"""Mark pipeline as complete."""
|
|
39
43
|
self.success = success
|
|
44
|
+
self.state = "completed" if success else "failed"
|
|
40
45
|
self.end_time = datetime.now()
|
|
41
46
|
self.duration_seconds = (self.end_time - self.start_time).total_seconds()
|
|
42
47
|
|
|
48
|
+
def attach_configs(self, resource_config: Any | None, docker_config: Any | None) -> None:
|
|
49
|
+
"""Store execution configs for downstream inspection."""
|
|
50
|
+
self.resource_config = resource_config
|
|
51
|
+
self.docker_config = docker_config
|
|
52
|
+
|
|
53
|
+
def mark_submitted(self, job_id: str) -> None:
|
|
54
|
+
"""Mark result as remotely submitted."""
|
|
55
|
+
self.success = True
|
|
56
|
+
self.state = "submitted"
|
|
57
|
+
self.remote_job_id = job_id
|
|
58
|
+
|
|
43
59
|
def __getitem__(self, key: str) -> Any:
|
|
44
60
|
"""Allow dict-style access to outputs."""
|
|
45
61
|
return self.outputs.get(key)
|
|
@@ -50,9 +66,17 @@ class PipelineResult:
|
|
|
50
66
|
"run_id": self.run_id,
|
|
51
67
|
"pipeline_name": self.pipeline_name,
|
|
52
68
|
"success": self.success,
|
|
69
|
+
"state": self.state,
|
|
53
70
|
"start_time": self.start_time.isoformat(),
|
|
54
71
|
"end_time": self.end_time.isoformat() if self.end_time else None,
|
|
55
72
|
"duration_seconds": self.duration_seconds,
|
|
73
|
+
"resource_config": self.resource_config.to_dict()
|
|
74
|
+
if hasattr(self.resource_config, "to_dict")
|
|
75
|
+
else self.resource_config,
|
|
76
|
+
"docker_config": self.docker_config.to_dict()
|
|
77
|
+
if hasattr(self.docker_config, "to_dict")
|
|
78
|
+
else self.docker_config,
|
|
79
|
+
"remote_job_id": self.remote_job_id,
|
|
56
80
|
"steps": {
|
|
57
81
|
name: {
|
|
58
82
|
"success": result.success,
|
|
@@ -67,10 +91,19 @@ class PipelineResult:
|
|
|
67
91
|
|
|
68
92
|
def summary(self) -> str:
|
|
69
93
|
"""Generate execution summary."""
|
|
94
|
+
if self.state == "submitted":
|
|
95
|
+
status_line = f"Status: ⏳ SUBMITTED (job: {self.remote_job_id})"
|
|
96
|
+
elif self.success:
|
|
97
|
+
status_line = "Status: ✓ SUCCESS"
|
|
98
|
+
elif self.state == "failed":
|
|
99
|
+
status_line = "Status: ✗ FAILED"
|
|
100
|
+
else:
|
|
101
|
+
status_line = f"Status: {self.state.upper()}"
|
|
102
|
+
|
|
70
103
|
lines = [
|
|
71
104
|
f"Pipeline: {self.pipeline_name}",
|
|
72
105
|
f"Run ID: {self.run_id}",
|
|
73
|
-
|
|
106
|
+
status_line,
|
|
74
107
|
f"Duration: {self.duration_seconds:.2f}s",
|
|
75
108
|
"",
|
|
76
109
|
"Steps:",
|
|
@@ -127,7 +160,9 @@ class Pipeline:
|
|
|
127
160
|
self.name = name
|
|
128
161
|
self.context = context or Context()
|
|
129
162
|
self.enable_cache = enable_cache
|
|
130
|
-
self.stack =
|
|
163
|
+
self.stack = None # Will be assigned via _apply_stack
|
|
164
|
+
self._stack_locked = stack is not None
|
|
165
|
+
self._provided_executor = executor
|
|
131
166
|
|
|
132
167
|
self.steps: list[Step] = []
|
|
133
168
|
self.dag = DAG()
|
|
@@ -146,15 +181,14 @@ class Pipeline:
|
|
|
146
181
|
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
|
147
182
|
|
|
148
183
|
# Initialize components from stack or defaults
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
else:
|
|
153
|
-
self.executor = executor or LocalExecutor()
|
|
154
|
-
# Metadata store for UI integration
|
|
155
|
-
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
184
|
+
self.executor = executor or LocalExecutor()
|
|
185
|
+
# Metadata store for UI integration
|
|
186
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
156
187
|
|
|
157
|
-
|
|
188
|
+
self.metadata_store = SQLiteMetadataStore()
|
|
189
|
+
|
|
190
|
+
if stack:
|
|
191
|
+
self._apply_stack(stack, locked=True)
|
|
158
192
|
|
|
159
193
|
# Handle Project Attachment
|
|
160
194
|
if project:
|
|
@@ -179,6 +213,18 @@ class Pipeline:
|
|
|
179
213
|
self._built = False
|
|
180
214
|
self.step_groups: list[Any] = [] # Will hold StepGroup objects
|
|
181
215
|
|
|
216
|
+
def _apply_stack(self, stack: Any | None, locked: bool) -> None:
|
|
217
|
+
"""Attach a stack and update executors/metadata."""
|
|
218
|
+
if not stack:
|
|
219
|
+
return
|
|
220
|
+
self.stack = stack
|
|
221
|
+
self._stack_locked = locked
|
|
222
|
+
if self._provided_executor:
|
|
223
|
+
self.executor = self._provided_executor
|
|
224
|
+
else:
|
|
225
|
+
self.executor = stack.executor
|
|
226
|
+
self.metadata_store = stack.metadata_store
|
|
227
|
+
|
|
182
228
|
def add_step(self, step: Step) -> "Pipeline":
|
|
183
229
|
"""Add a step to the pipeline.
|
|
184
230
|
|
|
@@ -234,6 +280,7 @@ class Pipeline:
|
|
|
234
280
|
resources: Any | None = None, # ResourceConfig
|
|
235
281
|
docker_config: Any | None = None, # DockerConfig
|
|
236
282
|
context: dict[str, Any] | None = None, # Context vars override
|
|
283
|
+
**kwargs,
|
|
237
284
|
) -> PipelineResult:
|
|
238
285
|
"""Execute the pipeline.
|
|
239
286
|
|
|
@@ -244,25 +291,34 @@ class Pipeline:
|
|
|
244
291
|
resources: Resource configuration for execution
|
|
245
292
|
docker_config: Docker configuration for containerized execution
|
|
246
293
|
context: Context variables override
|
|
294
|
+
**kwargs: Additional arguments passed to the orchestrator
|
|
247
295
|
|
|
248
296
|
Returns:
|
|
249
297
|
PipelineResult with outputs and execution info
|
|
250
298
|
"""
|
|
251
299
|
import uuid
|
|
300
|
+
from flowyml.core.orchestrator import LocalOrchestrator
|
|
252
301
|
|
|
253
302
|
run_id = str(uuid.uuid4())
|
|
254
303
|
|
|
255
|
-
#
|
|
304
|
+
# Determine stack for this run
|
|
256
305
|
if stack is not None:
|
|
257
|
-
self.stack =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
306
|
+
self._apply_stack(stack, locked=True)
|
|
307
|
+
elif not self._stack_locked:
|
|
308
|
+
active_stack = None
|
|
309
|
+
try:
|
|
310
|
+
from flowyml.stacks.registry import get_active_stack
|
|
311
|
+
except ImportError:
|
|
312
|
+
get_active_stack = None
|
|
313
|
+
if get_active_stack:
|
|
314
|
+
active_stack = get_active_stack()
|
|
315
|
+
if active_stack:
|
|
316
|
+
self._apply_stack(active_stack, locked=False)
|
|
317
|
+
|
|
318
|
+
# Determine orchestrator
|
|
319
|
+
orchestrator = getattr(self.stack, "orchestrator", None) if self.stack else None
|
|
320
|
+
if orchestrator is None:
|
|
321
|
+
orchestrator = LocalOrchestrator()
|
|
266
322
|
|
|
267
323
|
# Update context with provided values
|
|
268
324
|
if context:
|
|
@@ -272,218 +328,30 @@ class Pipeline:
|
|
|
272
328
|
if not self._built:
|
|
273
329
|
self.build()
|
|
274
330
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
from flowyml.core.step_grouping import get_execution_units
|
|
289
|
-
|
|
290
|
-
execution_units = get_execution_units(self.dag, self.steps)
|
|
291
|
-
|
|
292
|
-
# Execute steps/groups in order
|
|
293
|
-
for unit in execution_units:
|
|
294
|
-
# Check if unit is a group or individual step
|
|
295
|
-
from flowyml.core.step_grouping import StepGroup
|
|
296
|
-
|
|
297
|
-
if isinstance(unit, StepGroup):
|
|
298
|
-
# Execute entire group
|
|
299
|
-
if debug:
|
|
300
|
-
pass
|
|
301
|
-
|
|
302
|
-
# Get context parameters (use first step's function as representative)
|
|
303
|
-
first_step = unit.steps[0]
|
|
304
|
-
context_params = self.context.inject_params(first_step.func)
|
|
305
|
-
|
|
306
|
-
# Execute the group
|
|
307
|
-
group_results = self.executor.execute_step_group(
|
|
308
|
-
step_group=unit,
|
|
309
|
-
inputs=step_outputs,
|
|
310
|
-
context_params=context_params,
|
|
311
|
-
cache_store=self.cache_store,
|
|
312
|
-
artifact_store=artifact_store,
|
|
313
|
-
run_id=run_id,
|
|
314
|
-
project_name=self.name,
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
# Process each step result
|
|
318
|
-
for step_result in group_results:
|
|
319
|
-
result.add_step_result(step_result)
|
|
320
|
-
|
|
321
|
-
if debug:
|
|
322
|
-
pass
|
|
323
|
-
|
|
324
|
-
# Handle failure
|
|
325
|
-
if not step_result.success and not step_result.skipped:
|
|
326
|
-
result.finalize(success=False)
|
|
327
|
-
self._save_run(result)
|
|
328
|
-
return result
|
|
329
|
-
|
|
330
|
-
# Store outputs for next steps/groups
|
|
331
|
-
if step_result.output is not None:
|
|
332
|
-
# Find step definition to get output names
|
|
333
|
-
step_def = next((s for s in self.steps if s.name == step_result.step_name), None)
|
|
334
|
-
if step_def:
|
|
335
|
-
if len(step_def.outputs) == 1:
|
|
336
|
-
step_outputs[step_def.outputs[0]] = step_result.output
|
|
337
|
-
result.outputs[step_def.outputs[0]] = step_result.output
|
|
338
|
-
elif isinstance(step_result.output, (list, tuple)) and len(step_result.output) == len(
|
|
339
|
-
step_def.outputs,
|
|
340
|
-
):
|
|
341
|
-
for name, val in zip(step_def.outputs, step_result.output, strict=False):
|
|
342
|
-
step_outputs[name] = val
|
|
343
|
-
result.outputs[name] = val
|
|
344
|
-
elif isinstance(step_result.output, dict):
|
|
345
|
-
for name in step_def.outputs:
|
|
346
|
-
if name in step_result.output:
|
|
347
|
-
step_outputs[name] = step_result.output[name]
|
|
348
|
-
result.outputs[name] = step_result.output[name]
|
|
349
|
-
else:
|
|
350
|
-
if step_def.outputs:
|
|
351
|
-
step_outputs[step_def.outputs[0]] = step_result.output
|
|
352
|
-
result.outputs[step_def.outputs[0]] = step_result.output
|
|
353
|
-
|
|
354
|
-
else:
|
|
355
|
-
# Execute single ungrouped step
|
|
356
|
-
step = unit
|
|
357
|
-
|
|
358
|
-
if debug:
|
|
359
|
-
pass
|
|
360
|
-
|
|
361
|
-
# Prepare step inputs
|
|
362
|
-
step_inputs = {}
|
|
363
|
-
|
|
364
|
-
# Get function signature to map inputs to parameters
|
|
365
|
-
import inspect
|
|
366
|
-
|
|
367
|
-
sig = inspect.signature(step.func)
|
|
368
|
-
params = list(sig.parameters.values())
|
|
369
|
-
|
|
370
|
-
# Filter out self/cls
|
|
371
|
-
params = [p for p in params if p.name not in ("self", "cls")]
|
|
372
|
-
|
|
373
|
-
# Strategy:
|
|
374
|
-
# 1. Map inputs to parameters
|
|
375
|
-
# - If input name matches param name, use it
|
|
376
|
-
# - If not, use positional mapping (input[i] -> param[i])
|
|
377
|
-
|
|
378
|
-
# Track which parameters have been assigned
|
|
379
|
-
assigned_params = set()
|
|
380
|
-
|
|
381
|
-
if step.inputs:
|
|
382
|
-
for i, input_name in enumerate(step.inputs):
|
|
383
|
-
if input_name not in step_outputs:
|
|
384
|
-
continue
|
|
385
|
-
|
|
386
|
-
val = step_outputs[input_name]
|
|
387
|
-
|
|
388
|
-
# Check if input name matches a parameter
|
|
389
|
-
param_match = next((p for p in params if p.name == input_name), None)
|
|
390
|
-
|
|
391
|
-
if param_match:
|
|
392
|
-
step_inputs[param_match.name] = val
|
|
393
|
-
assigned_params.add(param_match.name)
|
|
394
|
-
elif i < len(params):
|
|
395
|
-
# Positional fallback
|
|
396
|
-
# Only if this parameter hasn't been assigned yet
|
|
397
|
-
target_param = params[i]
|
|
398
|
-
if target_param.name not in assigned_params:
|
|
399
|
-
step_inputs[target_param.name] = val
|
|
400
|
-
assigned_params.add(target_param.name)
|
|
401
|
-
|
|
402
|
-
# Auto-map parameters from available outputs if they match function signature
|
|
403
|
-
# This allows passing inputs to run() without declaring them as asset dependencies
|
|
404
|
-
for param in params:
|
|
405
|
-
if param.name in step_outputs and param.name not in step_inputs:
|
|
406
|
-
step_inputs[param.name] = step_outputs[param.name]
|
|
407
|
-
assigned_params.add(param.name)
|
|
408
|
-
|
|
409
|
-
# Validate context parameters
|
|
410
|
-
# Exclude parameters that are already provided in step_inputs
|
|
411
|
-
exclude_params = list(step.inputs) + list(step_inputs.keys())
|
|
412
|
-
missing_params = self.context.validate_for_step(step.func, exclude=exclude_params)
|
|
413
|
-
if missing_params:
|
|
414
|
-
if debug:
|
|
415
|
-
pass
|
|
416
|
-
|
|
417
|
-
error_msg = f"Missing required parameters: {missing_params}"
|
|
418
|
-
step_result = ExecutionResult(
|
|
419
|
-
step_name=step.name,
|
|
420
|
-
success=False,
|
|
421
|
-
error=error_msg,
|
|
422
|
-
)
|
|
423
|
-
result.add_step_result(step_result)
|
|
424
|
-
result.finalize(success=False)
|
|
425
|
-
self._save_run(result) # Save run before returning
|
|
426
|
-
self._save_pipeline_definition() # Save definition even on failure
|
|
427
|
-
print("DEBUG: Pipeline failed at step execution")
|
|
428
|
-
return result
|
|
429
|
-
|
|
430
|
-
# Get context parameters for this step
|
|
431
|
-
context_params = self.context.inject_params(step.func)
|
|
432
|
-
|
|
433
|
-
# Execute step
|
|
434
|
-
step_result = self.executor.execute_step(
|
|
435
|
-
step,
|
|
436
|
-
step_inputs,
|
|
437
|
-
context_params,
|
|
438
|
-
self.cache_store,
|
|
439
|
-
artifact_store=artifact_store,
|
|
440
|
-
run_id=run_id,
|
|
441
|
-
project_name=self.name,
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
result.add_step_result(step_result)
|
|
445
|
-
|
|
446
|
-
if debug:
|
|
447
|
-
pass
|
|
448
|
-
|
|
449
|
-
# Handle failure
|
|
450
|
-
if not step_result.success:
|
|
451
|
-
if debug and not step_result.error:
|
|
452
|
-
pass
|
|
453
|
-
result.finalize(success=False)
|
|
454
|
-
self._save_run(result)
|
|
455
|
-
self._save_pipeline_definition() # Save definition even on failure
|
|
456
|
-
print("DEBUG: Pipeline failed at step execution")
|
|
457
|
-
return result
|
|
458
|
-
|
|
459
|
-
# Store outputs for next steps
|
|
460
|
-
if step_result.output is not None:
|
|
461
|
-
if len(step.outputs) == 1:
|
|
462
|
-
step_outputs[step.outputs[0]] = step_result.output
|
|
463
|
-
result.outputs[step.outputs[0]] = step_result.output
|
|
464
|
-
elif isinstance(step_result.output, (list, tuple)) and len(step_result.output) == len(step.outputs):
|
|
465
|
-
for name, val in zip(step.outputs, step_result.output, strict=False):
|
|
466
|
-
step_outputs[name] = val
|
|
467
|
-
result.outputs[name] = val
|
|
468
|
-
elif isinstance(step_result.output, dict):
|
|
469
|
-
for name in step.outputs:
|
|
470
|
-
if name in step_result.output:
|
|
471
|
-
step_outputs[name] = step_result.output[name]
|
|
472
|
-
result.outputs[name] = step_result.output[name]
|
|
473
|
-
else:
|
|
474
|
-
# Fallback: assign to first output if available
|
|
475
|
-
if step.outputs:
|
|
476
|
-
step_outputs[step.outputs[0]] = step_result.output
|
|
477
|
-
result.outputs[step.outputs[0]] = step_result.output
|
|
478
|
-
|
|
479
|
-
# Success!
|
|
480
|
-
result.finalize(success=True)
|
|
331
|
+
resource_config = self._coerce_resource_config(resources)
|
|
332
|
+
docker_cfg = self._coerce_docker_config(docker_config)
|
|
333
|
+
|
|
334
|
+
# Run the pipeline via orchestrator
|
|
335
|
+
result = orchestrator.run_pipeline(
|
|
336
|
+
self,
|
|
337
|
+
run_id=run_id,
|
|
338
|
+
resources=resource_config,
|
|
339
|
+
docker_config=docker_cfg,
|
|
340
|
+
inputs=inputs,
|
|
341
|
+
context=context,
|
|
342
|
+
**kwargs,
|
|
343
|
+
)
|
|
481
344
|
|
|
482
|
-
|
|
483
|
-
|
|
345
|
+
# If result is just a job ID (remote execution), wrap it in a basic result
|
|
346
|
+
if isinstance(result, str):
|
|
347
|
+
# Create a submitted result wrapper
|
|
348
|
+
wrapper = PipelineResult(run_id, self.name)
|
|
349
|
+
wrapper.attach_configs(resource_config, docker_cfg)
|
|
350
|
+
wrapper.mark_submitted(result)
|
|
351
|
+
self._save_run(wrapper)
|
|
352
|
+
self._save_pipeline_definition()
|
|
353
|
+
return wrapper
|
|
484
354
|
|
|
485
|
-
self._save_run(result)
|
|
486
|
-
self._save_pipeline_definition() # Save pipeline structure for scheduling
|
|
487
355
|
return result
|
|
488
356
|
|
|
489
357
|
def to_definition(self) -> dict:
|
|
@@ -527,6 +395,36 @@ class Pipeline:
|
|
|
527
395
|
# Don't fail the run if definition saving fails
|
|
528
396
|
print(f"Warning: Failed to save pipeline definition: {e}")
|
|
529
397
|
|
|
398
|
+
def _coerce_resource_config(self, resources: Any | None):
|
|
399
|
+
"""Convert resources input to ResourceConfig if necessary."""
|
|
400
|
+
if resources is None:
|
|
401
|
+
return None
|
|
402
|
+
try:
|
|
403
|
+
from flowyml.stacks.components import ResourceConfig
|
|
404
|
+
except Exception:
|
|
405
|
+
return resources
|
|
406
|
+
|
|
407
|
+
if isinstance(resources, ResourceConfig):
|
|
408
|
+
return resources
|
|
409
|
+
if isinstance(resources, dict):
|
|
410
|
+
return ResourceConfig(**resources)
|
|
411
|
+
return resources
|
|
412
|
+
|
|
413
|
+
def _coerce_docker_config(self, docker_config: Any | None):
|
|
414
|
+
"""Convert docker input to DockerConfig if necessary."""
|
|
415
|
+
if docker_config is None:
|
|
416
|
+
return None
|
|
417
|
+
try:
|
|
418
|
+
from flowyml.stacks.components import DockerConfig
|
|
419
|
+
except Exception:
|
|
420
|
+
return docker_config
|
|
421
|
+
|
|
422
|
+
if isinstance(docker_config, DockerConfig):
|
|
423
|
+
return docker_config
|
|
424
|
+
if isinstance(docker_config, dict):
|
|
425
|
+
return DockerConfig(**docker_config)
|
|
426
|
+
return docker_config
|
|
427
|
+
|
|
530
428
|
def _save_run(self, result: PipelineResult) -> None:
|
|
531
429
|
"""Save run results to disk and metadata database."""
|
|
532
430
|
# Save to JSON file
|
|
@@ -576,7 +474,7 @@ class Pipeline:
|
|
|
576
474
|
metadata = {
|
|
577
475
|
"run_id": result.run_id,
|
|
578
476
|
"pipeline_name": result.pipeline_name,
|
|
579
|
-
"status":
|
|
477
|
+
"status": result.state,
|
|
580
478
|
"start_time": result.start_time.isoformat(),
|
|
581
479
|
"end_time": result.end_time.isoformat() if result.end_time else None,
|
|
582
480
|
"duration": result.duration_seconds,
|
|
@@ -584,6 +482,13 @@ class Pipeline:
|
|
|
584
482
|
"context": self.context._params if hasattr(self.context, "_params") else {},
|
|
585
483
|
"steps": steps_metadata,
|
|
586
484
|
"dag": dag_data,
|
|
485
|
+
"resources": result.resource_config.to_dict()
|
|
486
|
+
if hasattr(result.resource_config, "to_dict")
|
|
487
|
+
else result.resource_config,
|
|
488
|
+
"docker": result.docker_config.to_dict()
|
|
489
|
+
if hasattr(result.docker_config, "to_dict")
|
|
490
|
+
else result.docker_config,
|
|
491
|
+
"remote_job_id": result.remote_job_id,
|
|
587
492
|
}
|
|
588
493
|
self.metadata_store.save_run(result.run_id, metadata)
|
|
589
494
|
|
|
@@ -762,3 +667,60 @@ class Pipeline:
|
|
|
762
667
|
|
|
763
668
|
def __repr__(self) -> str:
|
|
764
669
|
return f"Pipeline(name='{self.name}', steps={len(self.steps)})"
|
|
670
|
+
|
|
671
|
+
def schedule(
|
|
672
|
+
self,
|
|
673
|
+
schedule_type: str,
|
|
674
|
+
value: str | int,
|
|
675
|
+
**kwargs,
|
|
676
|
+
) -> Any:
|
|
677
|
+
"""Schedule this pipeline to run automatically.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
schedule_type: Type of schedule ('cron', 'interval', 'daily', 'hourly')
|
|
681
|
+
value: Schedule value (cron expression, seconds, 'HH:MM', or minute)
|
|
682
|
+
**kwargs: Additional arguments for scheduler
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
Schedule object
|
|
686
|
+
"""
|
|
687
|
+
from flowyml.core.scheduler import PipelineScheduler
|
|
688
|
+
|
|
689
|
+
scheduler = PipelineScheduler()
|
|
690
|
+
|
|
691
|
+
if schedule_type == "cron":
|
|
692
|
+
return scheduler.schedule_cron(self.name, self.run, str(value), **kwargs)
|
|
693
|
+
elif schedule_type == "interval":
|
|
694
|
+
return scheduler.schedule_interval(self.name, self.run, seconds=int(value), **kwargs)
|
|
695
|
+
elif schedule_type == "daily":
|
|
696
|
+
if isinstance(value, str) and ":" in value:
|
|
697
|
+
h, m = map(int, value.split(":"))
|
|
698
|
+
return scheduler.schedule_daily(self.name, self.run, hour=h, minute=m, **kwargs)
|
|
699
|
+
else:
|
|
700
|
+
raise ValueError("Daily schedule value must be 'HH:MM'")
|
|
701
|
+
elif schedule_type == "hourly":
|
|
702
|
+
return scheduler.schedule_hourly(self.name, self.run, minute=int(value), **kwargs)
|
|
703
|
+
else:
|
|
704
|
+
raise ValueError(f"Unknown schedule type: {schedule_type}")
|
|
705
|
+
|
|
706
|
+
def check_cache(self) -> dict[str, Any] | None:
|
|
707
|
+
"""Check if a successful run of this pipeline already exists.
|
|
708
|
+
|
|
709
|
+
Returns:
|
|
710
|
+
Metadata of the last successful run, or None if not found.
|
|
711
|
+
"""
|
|
712
|
+
# Query metadata store for successful runs of this pipeline
|
|
713
|
+
try:
|
|
714
|
+
runs = self.metadata_store.query(
|
|
715
|
+
pipeline_name=self.name,
|
|
716
|
+
status="completed",
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
if runs:
|
|
720
|
+
# Return the most recent one (query returns ordered by created_at DESC)
|
|
721
|
+
return runs[0]
|
|
722
|
+
except Exception as e:
|
|
723
|
+
# Don't fail if metadata store is not available or errors
|
|
724
|
+
print(f"Warning: Failed to check cache: {e}")
|
|
725
|
+
|
|
726
|
+
return None
|
flowyml/core/project.py
CHANGED
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any, TYPE_CHECKING
|
|
6
6
|
from datetime import datetime
|
|
7
|
+
from flowyml.utils.config import get_config
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from flowyml.core.pipeline import Pipeline
|
|
@@ -186,6 +187,36 @@ class Project:
|
|
|
186
187
|
with open(output_file, "w") as f:
|
|
187
188
|
json.dump(export_data, f, indent=2)
|
|
188
189
|
|
|
190
|
+
def log_model_metrics(
|
|
191
|
+
self,
|
|
192
|
+
model_name: str,
|
|
193
|
+
metrics: dict[str, float],
|
|
194
|
+
run_id: str | None = None,
|
|
195
|
+
environment: str | None = None,
|
|
196
|
+
tags: dict | None = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Log production metrics scoped to this project."""
|
|
199
|
+
self.metadata_store.log_model_metrics(
|
|
200
|
+
project=self.name,
|
|
201
|
+
model_name=model_name,
|
|
202
|
+
metrics=metrics,
|
|
203
|
+
run_id=run_id,
|
|
204
|
+
environment=environment,
|
|
205
|
+
tags=tags,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def list_model_metrics(
|
|
209
|
+
self,
|
|
210
|
+
model_name: str | None = None,
|
|
211
|
+
limit: int = 100,
|
|
212
|
+
) -> list[dict]:
|
|
213
|
+
"""List production metrics for this project."""
|
|
214
|
+
return self.metadata_store.list_model_metrics(
|
|
215
|
+
project=self.name,
|
|
216
|
+
model_name=model_name,
|
|
217
|
+
limit=limit,
|
|
218
|
+
)
|
|
219
|
+
|
|
189
220
|
def __repr__(self) -> str:
|
|
190
221
|
return f"Project(name='{self.name}', pipelines={len(self.metadata['pipelines'])})"
|
|
191
222
|
|
|
@@ -205,8 +236,9 @@ class ProjectManager:
|
|
|
205
236
|
>>> project = manager.get_project("recommendation_system")
|
|
206
237
|
"""
|
|
207
238
|
|
|
208
|
-
def __init__(self, projects_dir: str =
|
|
209
|
-
|
|
239
|
+
def __init__(self, projects_dir: str | None = None):
|
|
240
|
+
config = get_config()
|
|
241
|
+
self.projects_dir = Path(projects_dir or config.projects_dir)
|
|
210
242
|
self.projects_dir.mkdir(parents=True, exist_ok=True)
|
|
211
243
|
|
|
212
244
|
def create_project(self, name: str, description: str = "") -> Project:
|