flowyml 1.7.1__py3-none-any.whl → 1.8.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/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
flowyml/core/routing.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""FlowyML Artifact Routing - Automatic Type-Based Artifact Routing.
|
|
2
|
+
|
|
3
|
+
This module provides automatic routing of step outputs to appropriate
|
|
4
|
+
infrastructure based on their Python types. When a step returns a
|
|
5
|
+
`Model`, `Dataset`, `Metrics`, or other artifact type, the runtime
|
|
6
|
+
automatically routes it to the configured stores and registries.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from flowyml.core.routing import route_artifact
|
|
10
|
+
|
|
11
|
+
# After step execution
|
|
12
|
+
result = step.func(**inputs)
|
|
13
|
+
|
|
14
|
+
# Route based on type and stack config
|
|
15
|
+
artifact_info = route_artifact(
|
|
16
|
+
output=result,
|
|
17
|
+
step_name="train_model",
|
|
18
|
+
run_id="run-123",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
The routing is configured via flowyml.yaml:
|
|
22
|
+
stacks:
|
|
23
|
+
gcp-prod:
|
|
24
|
+
artifact_routing:
|
|
25
|
+
Model: { store: gcs, register: true }
|
|
26
|
+
Dataset: { store: gcs }
|
|
27
|
+
Metrics: { log_to_tracker: true }
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
from typing import Any, get_type_hints
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class RoutingResult:
|
|
39
|
+
"""Result of artifact routing.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
artifact_type: Name of the artifact type (Model, Dataset, etc.)
|
|
43
|
+
store_uri: URI where the artifact was stored
|
|
44
|
+
registered: Whether the artifact was registered (e.g., in model registry)
|
|
45
|
+
deployed: Whether the artifact was deployed (e.g., to endpoint)
|
|
46
|
+
endpoint_uri: URI of the deployment endpoint
|
|
47
|
+
logged: Whether the artifact was logged (e.g., metrics to tracker)
|
|
48
|
+
metadata: Additional metadata from routing
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
artifact_type: str | None = None
|
|
52
|
+
store_uri: str | None = None
|
|
53
|
+
registered: bool = False
|
|
54
|
+
deployed: bool = False
|
|
55
|
+
endpoint_uri: str | None = None
|
|
56
|
+
logged: bool = False
|
|
57
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_step_return_type(step_func: callable) -> type | None:
|
|
61
|
+
"""Get the return type annotation from a step function.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
step_func: The step function to inspect.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The return type annotation, or None if not annotated.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
hints = get_type_hints(step_func)
|
|
71
|
+
return hints.get("return")
|
|
72
|
+
except Exception:
|
|
73
|
+
# Fallback to __annotations__ if get_type_hints fails
|
|
74
|
+
try:
|
|
75
|
+
return step_func.__annotations__.get("return")
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def detect_artifact_type(output: Any) -> str | None:
|
|
81
|
+
"""Detect the artifact type from an output value.
|
|
82
|
+
|
|
83
|
+
This checks if the output is an instance of one of our artifact types
|
|
84
|
+
or if it matches specific patterns (like dict for Metrics).
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
output: The step output value.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Type name string or None.
|
|
91
|
+
"""
|
|
92
|
+
# Import types here to avoid circular imports
|
|
93
|
+
from flowyml.core.types import Artifact, Model, Dataset, Metrics, Parameters
|
|
94
|
+
|
|
95
|
+
if isinstance(output, Model):
|
|
96
|
+
return "Model"
|
|
97
|
+
elif isinstance(output, Dataset):
|
|
98
|
+
return "Dataset"
|
|
99
|
+
elif isinstance(output, Metrics):
|
|
100
|
+
return "Metrics"
|
|
101
|
+
elif isinstance(output, Parameters):
|
|
102
|
+
return "Parameters"
|
|
103
|
+
elif isinstance(output, Artifact):
|
|
104
|
+
return type(output).__name__
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def route_artifact(
|
|
110
|
+
output: Any,
|
|
111
|
+
step_name: str,
|
|
112
|
+
run_id: str,
|
|
113
|
+
return_type: type | None = None,
|
|
114
|
+
project_name: str = "default",
|
|
115
|
+
) -> RoutingResult:
|
|
116
|
+
"""Route a step output to appropriate infrastructure based on type.
|
|
117
|
+
|
|
118
|
+
This is the main entry point for type-based artifact routing.
|
|
119
|
+
It inspects the output type and routes to configured stores/registries.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
output: The step output to route.
|
|
123
|
+
step_name: Name of the step that produced this output.
|
|
124
|
+
run_id: Current run identifier.
|
|
125
|
+
return_type: Optional return type annotation (if known).
|
|
126
|
+
project_name: Project name for namespacing.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
RoutingResult with routing information.
|
|
130
|
+
"""
|
|
131
|
+
result = RoutingResult()
|
|
132
|
+
|
|
133
|
+
# Skip None outputs
|
|
134
|
+
if output is None:
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
# Detect artifact type
|
|
138
|
+
artifact_type = detect_artifact_type(output)
|
|
139
|
+
|
|
140
|
+
# If not detected from value, try from type annotation
|
|
141
|
+
if artifact_type is None and return_type is not None:
|
|
142
|
+
try:
|
|
143
|
+
type_name = return_type.__name__ if hasattr(return_type, "__name__") else str(return_type)
|
|
144
|
+
if type_name in ("Model", "Dataset", "Metrics", "Parameters"):
|
|
145
|
+
artifact_type = type_name
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
if artifact_type is None:
|
|
150
|
+
# Not a routable artifact type
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
result.artifact_type = artifact_type
|
|
154
|
+
logger.debug(f"Routing {artifact_type} artifact from step '{step_name}'")
|
|
155
|
+
|
|
156
|
+
# Get routing configuration from active stack
|
|
157
|
+
try:
|
|
158
|
+
from flowyml.plugins.stack_config import get_routing_for_type, get_active_stack
|
|
159
|
+
|
|
160
|
+
routing_rule = get_routing_for_type(artifact_type)
|
|
161
|
+
stack = get_active_stack()
|
|
162
|
+
|
|
163
|
+
if routing_rule is None:
|
|
164
|
+
logger.debug(f"No routing rule for {artifact_type}, using defaults")
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
# Route to artifact store
|
|
168
|
+
if routing_rule.store:
|
|
169
|
+
result.store_uri = _save_to_store(
|
|
170
|
+
output=output,
|
|
171
|
+
artifact_type=artifact_type,
|
|
172
|
+
store_name=routing_rule.store,
|
|
173
|
+
path=routing_rule.format_path(
|
|
174
|
+
run_id=run_id,
|
|
175
|
+
step_name=step_name,
|
|
176
|
+
artifact_name=artifact_type.lower(),
|
|
177
|
+
),
|
|
178
|
+
stack=stack,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Register model if configured
|
|
182
|
+
if routing_rule.register and artifact_type == "Model":
|
|
183
|
+
result.registered = _register_model(
|
|
184
|
+
output=output,
|
|
185
|
+
step_name=step_name,
|
|
186
|
+
run_id=run_id,
|
|
187
|
+
stack=stack,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Deploy model if configured and conditions are met
|
|
191
|
+
# Note: deploy=True just enables deployment - actual deployment depends on deploy_condition
|
|
192
|
+
if routing_rule.deploy and artifact_type == "Model":
|
|
193
|
+
# Get metrics from model metadata for conditional deployment
|
|
194
|
+
model_metrics = None
|
|
195
|
+
if hasattr(output, "metadata") and output.metadata:
|
|
196
|
+
model_metrics = output.metadata.get("metrics", {})
|
|
197
|
+
|
|
198
|
+
# Check if auto-deployment should proceed
|
|
199
|
+
if routing_rule.should_auto_deploy(model_metrics):
|
|
200
|
+
endpoint_name = routing_rule.endpoint_name or f"{step_name}-endpoint"
|
|
201
|
+
result.deployed, result.endpoint_uri = _deploy_model(
|
|
202
|
+
output=output,
|
|
203
|
+
step_name=step_name,
|
|
204
|
+
run_id=run_id,
|
|
205
|
+
endpoint_name=endpoint_name,
|
|
206
|
+
stack=stack,
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
# Log that deployment is pending approval/manual action
|
|
210
|
+
condition = routing_rule.deploy_condition
|
|
211
|
+
if condition == "manual":
|
|
212
|
+
logger.info(
|
|
213
|
+
f"Model registered but not deployed (deploy_condition='manual'). "
|
|
214
|
+
f"Use 'flowyml model deploy {output.name}' to deploy.",
|
|
215
|
+
)
|
|
216
|
+
elif condition == "on_approval":
|
|
217
|
+
logger.info("Model registered, awaiting approval for deployment.")
|
|
218
|
+
elif condition == "auto" and routing_rule.deploy_min_metrics:
|
|
219
|
+
logger.info(
|
|
220
|
+
f"Model not deployed - metrics did not meet thresholds: " f"{routing_rule.deploy_min_metrics}",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Log metrics if configured
|
|
224
|
+
if routing_rule.log_to_tracker and artifact_type == "Metrics":
|
|
225
|
+
result.logged = _log_metrics(
|
|
226
|
+
output=output,
|
|
227
|
+
step_name=step_name,
|
|
228
|
+
run_id=run_id,
|
|
229
|
+
stack=stack,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Log parameters if configured
|
|
233
|
+
if routing_rule.log_to_tracker and artifact_type == "Parameters":
|
|
234
|
+
result.logged = _log_parameters(
|
|
235
|
+
output=output,
|
|
236
|
+
step_name=step_name,
|
|
237
|
+
run_id=run_id,
|
|
238
|
+
stack=stack,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Add routing metadata
|
|
242
|
+
result.metadata = {
|
|
243
|
+
"store": routing_rule.store,
|
|
244
|
+
"path": routing_rule.path,
|
|
245
|
+
"registered": result.registered,
|
|
246
|
+
"deployed": result.deployed,
|
|
247
|
+
"logged": result.logged,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
except ImportError:
|
|
251
|
+
logger.debug("Stack config not available, skipping routing")
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.warning(f"Error during artifact routing: {e}")
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _save_to_store(
|
|
259
|
+
output: Any,
|
|
260
|
+
artifact_type: str,
|
|
261
|
+
store_name: str,
|
|
262
|
+
path: str,
|
|
263
|
+
stack: Any,
|
|
264
|
+
) -> str | None:
|
|
265
|
+
"""Save artifact to the configured store.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
output: The artifact to save.
|
|
269
|
+
artifact_type: Type of the artifact.
|
|
270
|
+
store_name: Name of the store (gcs, s3, local).
|
|
271
|
+
path: Path within the store.
|
|
272
|
+
stack: Stack configuration.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
URI of the saved artifact or None.
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
# Get artifact store from stack
|
|
279
|
+
if store_name and stack and stack.artifact_stores:
|
|
280
|
+
store_config = stack.artifact_stores.get(store_name)
|
|
281
|
+
if store_config:
|
|
282
|
+
# Instantiate and use the store
|
|
283
|
+
from flowyml.plugins.config import get_artifact_store
|
|
284
|
+
|
|
285
|
+
store = get_artifact_store()
|
|
286
|
+
if store:
|
|
287
|
+
# Extract data if it's an Artifact wrapper
|
|
288
|
+
from flowyml.core.types import Artifact
|
|
289
|
+
|
|
290
|
+
data = output.data if isinstance(output, Artifact) else output
|
|
291
|
+
return store.save(data, path)
|
|
292
|
+
|
|
293
|
+
# Fallback to default artifact store
|
|
294
|
+
from flowyml.plugins.config import get_artifact_store
|
|
295
|
+
|
|
296
|
+
store = get_artifact_store()
|
|
297
|
+
if store:
|
|
298
|
+
from flowyml.core.types import Artifact
|
|
299
|
+
|
|
300
|
+
data = output.data if isinstance(output, Artifact) else output
|
|
301
|
+
return store.save(data, path)
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning(f"Failed to save artifact to store: {e}")
|
|
305
|
+
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _register_model(
|
|
310
|
+
output: Any,
|
|
311
|
+
step_name: str,
|
|
312
|
+
run_id: str,
|
|
313
|
+
stack: Any,
|
|
314
|
+
) -> bool:
|
|
315
|
+
"""Register a model in the model registry.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
output: The Model artifact.
|
|
319
|
+
step_name: Step that produced the model.
|
|
320
|
+
run_id: Current run ID.
|
|
321
|
+
stack: Stack configuration.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
True if registration was successful.
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
from flowyml.core.types import Model
|
|
328
|
+
|
|
329
|
+
if not isinstance(output, Model):
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
# Get model registry from plugins
|
|
333
|
+
from flowyml.plugins.config import get_config
|
|
334
|
+
|
|
335
|
+
config = get_config()
|
|
336
|
+
registry = config._get_plugin("model_registry")
|
|
337
|
+
|
|
338
|
+
if registry:
|
|
339
|
+
model_name = output.name or f"{step_name}_model"
|
|
340
|
+
model_uri = output.uri or f"runs/{run_id}/models/{step_name}"
|
|
341
|
+
|
|
342
|
+
registry.register_model(
|
|
343
|
+
name=model_name,
|
|
344
|
+
model_uri=model_uri,
|
|
345
|
+
version=output.version,
|
|
346
|
+
metadata={
|
|
347
|
+
"framework": output.framework,
|
|
348
|
+
"step_name": step_name,
|
|
349
|
+
"run_id": run_id,
|
|
350
|
+
**output.metadata,
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
logger.info(f"Registered model '{model_name}' to registry")
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
logger.warning(f"Failed to register model: {e}")
|
|
358
|
+
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _deploy_model(
|
|
363
|
+
output: Any,
|
|
364
|
+
step_name: str,
|
|
365
|
+
run_id: str,
|
|
366
|
+
endpoint_name: str,
|
|
367
|
+
stack: Any,
|
|
368
|
+
) -> tuple[bool, str | None]:
|
|
369
|
+
"""Deploy a model to an endpoint.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
output: The Model artifact.
|
|
373
|
+
step_name: Step that produced the model.
|
|
374
|
+
run_id: Current run ID.
|
|
375
|
+
endpoint_name: Name for the endpoint.
|
|
376
|
+
stack: Stack configuration.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Tuple of (success, endpoint_uri).
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
from flowyml.core.types import Model
|
|
383
|
+
|
|
384
|
+
if not isinstance(output, Model):
|
|
385
|
+
return False, None
|
|
386
|
+
|
|
387
|
+
# Get model deployer from stack config
|
|
388
|
+
if stack and stack.model_deployer:
|
|
389
|
+
deployer_config = stack.model_deployer
|
|
390
|
+
deployer_type = deployer_config.get("type", "")
|
|
391
|
+
|
|
392
|
+
deployer = None
|
|
393
|
+
|
|
394
|
+
# Instantiate the appropriate deployer
|
|
395
|
+
if "vertex" in deployer_type:
|
|
396
|
+
from flowyml.plugins.deployers.vertex import VertexEndpointDeployer
|
|
397
|
+
|
|
398
|
+
deployer = VertexEndpointDeployer(
|
|
399
|
+
project=deployer_config.get("project"),
|
|
400
|
+
location=deployer_config.get("location", "us-central1"),
|
|
401
|
+
)
|
|
402
|
+
elif "sagemaker" in deployer_type:
|
|
403
|
+
from flowyml.plugins.deployers.sagemaker import SageMakerEndpointDeployer
|
|
404
|
+
|
|
405
|
+
deployer = SageMakerEndpointDeployer(
|
|
406
|
+
region=deployer_config.get("region"),
|
|
407
|
+
role_arn=deployer_config.get("role_arn"),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if deployer:
|
|
411
|
+
deployer.initialize()
|
|
412
|
+
|
|
413
|
+
# Get model URI (from artifact store or output)
|
|
414
|
+
model_uri = output.uri or f"runs/{run_id}/models/{step_name}"
|
|
415
|
+
|
|
416
|
+
endpoint_uri = deployer.deploy(
|
|
417
|
+
model_uri=model_uri,
|
|
418
|
+
endpoint_name=endpoint_name,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
logger.info(f"Deployed model to endpoint: {endpoint_uri}")
|
|
422
|
+
return True, endpoint_uri
|
|
423
|
+
|
|
424
|
+
# No deployer configured
|
|
425
|
+
logger.debug("No model deployer configured in stack")
|
|
426
|
+
return False, None
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.warning(f"Failed to deploy model: {e}")
|
|
430
|
+
|
|
431
|
+
return False, None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _log_metrics(
|
|
435
|
+
output: Any,
|
|
436
|
+
step_name: str,
|
|
437
|
+
run_id: str,
|
|
438
|
+
stack: Any,
|
|
439
|
+
) -> bool:
|
|
440
|
+
"""Log metrics to the experiment tracker.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
output: The Metrics artifact (dict-like).
|
|
444
|
+
step_name: Step that produced the metrics.
|
|
445
|
+
run_id: Current run ID.
|
|
446
|
+
stack: Stack configuration.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
True if logging was successful.
|
|
450
|
+
"""
|
|
451
|
+
try:
|
|
452
|
+
from flowyml.core.types import Metrics
|
|
453
|
+
from flowyml.plugins.config import get_tracker
|
|
454
|
+
|
|
455
|
+
tracker = get_tracker()
|
|
456
|
+
if tracker:
|
|
457
|
+
# Get metrics values
|
|
458
|
+
if isinstance(output, Metrics):
|
|
459
|
+
metrics_dict = dict(output)
|
|
460
|
+
step_num = output._step
|
|
461
|
+
else:
|
|
462
|
+
metrics_dict = dict(output)
|
|
463
|
+
step_num = None
|
|
464
|
+
|
|
465
|
+
tracker.log_metrics(metrics_dict, step=step_num)
|
|
466
|
+
logger.debug(f"Logged metrics from step '{step_name}': {list(metrics_dict.keys())}")
|
|
467
|
+
return True
|
|
468
|
+
|
|
469
|
+
except Exception as e:
|
|
470
|
+
logger.warning(f"Failed to log metrics: {e}")
|
|
471
|
+
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _log_parameters(
|
|
476
|
+
output: Any,
|
|
477
|
+
step_name: str,
|
|
478
|
+
run_id: str,
|
|
479
|
+
stack: Any,
|
|
480
|
+
) -> bool:
|
|
481
|
+
"""Log parameters to the experiment tracker.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
output: The Parameters artifact (dict-like).
|
|
485
|
+
step_name: Step that uses the parameters.
|
|
486
|
+
run_id: Current run ID.
|
|
487
|
+
stack: Stack configuration.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
True if logging was successful.
|
|
491
|
+
"""
|
|
492
|
+
try:
|
|
493
|
+
from flowyml.core.types import Parameters
|
|
494
|
+
from flowyml.plugins.config import get_tracker
|
|
495
|
+
|
|
496
|
+
tracker = get_tracker()
|
|
497
|
+
if tracker:
|
|
498
|
+
# Get parameter values
|
|
499
|
+
if isinstance(output, Parameters):
|
|
500
|
+
params_dict = dict(output)
|
|
501
|
+
else:
|
|
502
|
+
params_dict = dict(output)
|
|
503
|
+
|
|
504
|
+
# Log parameters (with step prefix for clarity)
|
|
505
|
+
prefixed_params = {f"{step_name}/{k}": v for k, v in params_dict.items()}
|
|
506
|
+
tracker.log_params(prefixed_params)
|
|
507
|
+
logger.debug(f"Logged parameters from step '{step_name}': {list(params_dict.keys())}")
|
|
508
|
+
return True
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.warning(f"Failed to log parameters: {e}")
|
|
512
|
+
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def should_route(output: Any) -> bool:
|
|
517
|
+
"""Check if an output should be routed.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
output: The step output.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
True if the output should be routed.
|
|
524
|
+
"""
|
|
525
|
+
if output is None:
|
|
526
|
+
return False
|
|
527
|
+
|
|
528
|
+
from flowyml.core.types import is_artifact_type
|
|
529
|
+
|
|
530
|
+
return is_artifact_type(output)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def auto_route_metrics_and_params(
|
|
534
|
+
output: Any,
|
|
535
|
+
step_name: str,
|
|
536
|
+
run_id: str,
|
|
537
|
+
) -> bool:
|
|
538
|
+
"""Automatically route Metrics and Parameters without explicit config.
|
|
539
|
+
|
|
540
|
+
This is a convenience function that can be called to log Metrics
|
|
541
|
+
and Parameters even when no routing rule is configured.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
output: The step output.
|
|
545
|
+
step_name: Step name.
|
|
546
|
+
run_id: Run ID.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
True if logging was successful.
|
|
550
|
+
"""
|
|
551
|
+
from flowyml.core.types import Metrics, Parameters
|
|
552
|
+
|
|
553
|
+
if isinstance(output, Metrics):
|
|
554
|
+
return _log_metrics(output, step_name, run_id, None)
|
|
555
|
+
elif isinstance(output, Parameters):
|
|
556
|
+
return _log_parameters(output, step_name, run_id, None)
|
|
557
|
+
|
|
558
|
+
return False
|
flowyml/core/scheduler.py
CHANGED
|
@@ -224,6 +224,36 @@ class SchedulerPersistence:
|
|
|
224
224
|
logger.error(f"Failed to load schedule {name}: {e}")
|
|
225
225
|
return schedules
|
|
226
226
|
|
|
227
|
+
def load_schedule(self, name: str) -> Schedule | None:
|
|
228
|
+
"""Load a single schedule from database by name.
|
|
229
|
+
|
|
230
|
+
Returns None if not found. Creates a Schedule without a pipeline_func
|
|
231
|
+
(the schedule will be enabled/disabled but won't actually run until
|
|
232
|
+
a pipeline function is registered).
|
|
233
|
+
"""
|
|
234
|
+
with self.engine.connect() as conn:
|
|
235
|
+
stmt = select(self.schedules.c.name, self.schedules.c.data).where(
|
|
236
|
+
self.schedules.c.name == name,
|
|
237
|
+
)
|
|
238
|
+
result = conn.execute(stmt)
|
|
239
|
+
row = result.fetchone()
|
|
240
|
+
if row:
|
|
241
|
+
try:
|
|
242
|
+
data = json.loads(row.data)
|
|
243
|
+
# Create a minimal Schedule for enable/disable operations
|
|
244
|
+
# without requiring the pipeline function
|
|
245
|
+
return Schedule(
|
|
246
|
+
pipeline_name=data.get("pipeline_name", name),
|
|
247
|
+
pipeline_func=lambda: None, # Placeholder - not for execution
|
|
248
|
+
schedule_type=data.get("schedule_type", ""),
|
|
249
|
+
schedule_value=data.get("schedule_value", ""),
|
|
250
|
+
timezone=data.get("timezone", "UTC"),
|
|
251
|
+
enabled=data.get("enabled", True),
|
|
252
|
+
)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Failed to load schedule {name}: {e}")
|
|
255
|
+
return None
|
|
256
|
+
|
|
227
257
|
def delete_schedule(self, name: str) -> None:
|
|
228
258
|
"""Delete schedule from database using SQLAlchemy."""
|
|
229
259
|
with self.engine.connect() as conn:
|
|
@@ -251,6 +281,29 @@ class SchedulerPersistence:
|
|
|
251
281
|
conn.execute(stmt)
|
|
252
282
|
conn.commit()
|
|
253
283
|
|
|
284
|
+
def list_all_schedules(self) -> list[dict[str, Any]]:
|
|
285
|
+
"""List all schedules from database without requiring pipeline functions.
|
|
286
|
+
|
|
287
|
+
This is useful for displaying schedules in the UI regardless of whether
|
|
288
|
+
the pipeline code is loaded.
|
|
289
|
+
"""
|
|
290
|
+
schedules = []
|
|
291
|
+
with self.engine.connect() as conn:
|
|
292
|
+
stmt = select(self.schedules.c.name, self.schedules.c.data, self.schedules.c.updated_at)
|
|
293
|
+
result = conn.execute(stmt)
|
|
294
|
+
for row in result:
|
|
295
|
+
try:
|
|
296
|
+
data = json.loads(row.data)
|
|
297
|
+
data["name"] = row.name
|
|
298
|
+
if row.updated_at:
|
|
299
|
+
data["updated_at"] = (
|
|
300
|
+
row.updated_at.isoformat() if isinstance(row.updated_at, datetime) else str(row.updated_at)
|
|
301
|
+
)
|
|
302
|
+
schedules.append(data)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Failed to parse schedule {row.name}: {e}")
|
|
305
|
+
return schedules
|
|
306
|
+
|
|
254
307
|
def get_history(self, schedule_name: str, limit: int = 50) -> list[dict[str, Any]]:
|
|
255
308
|
"""Get execution history for a schedule using SQLAlchemy."""
|
|
256
309
|
history = []
|
|
@@ -533,11 +586,17 @@ class PipelineScheduler:
|
|
|
533
586
|
return schedule
|
|
534
587
|
|
|
535
588
|
def unschedule(self, name: str) -> None:
|
|
536
|
-
"""Remove a scheduled pipeline.
|
|
589
|
+
"""Remove a scheduled pipeline.
|
|
590
|
+
|
|
591
|
+
Handles both in-memory schedules and persisted schedules.
|
|
592
|
+
"""
|
|
593
|
+
# Remove from in-memory schedules if present
|
|
537
594
|
if name in self.schedules:
|
|
538
595
|
del self.schedules[name]
|
|
539
|
-
|
|
540
|
-
|
|
596
|
+
|
|
597
|
+
# Always try to remove from persistence (handles schedules created by other processes)
|
|
598
|
+
if self._persistence:
|
|
599
|
+
self._persistence.delete_schedule(name)
|
|
541
600
|
|
|
542
601
|
def clear(self) -> None:
|
|
543
602
|
"""Remove all schedules."""
|
|
@@ -550,18 +609,42 @@ class PipelineScheduler:
|
|
|
550
609
|
conn.commit()
|
|
551
610
|
|
|
552
611
|
def enable(self, name: str) -> None:
|
|
553
|
-
"""Enable a schedule.
|
|
612
|
+
"""Enable a schedule.
|
|
613
|
+
|
|
614
|
+
Handles both in-memory schedules and persisted schedules.
|
|
615
|
+
"""
|
|
554
616
|
if name in self.schedules:
|
|
555
617
|
self.schedules[name].enabled = True
|
|
556
618
|
if self._persistence:
|
|
557
619
|
self._persistence.save_schedule(self.schedules[name])
|
|
620
|
+
elif self._persistence:
|
|
621
|
+
# Schedule might be in persistence but not loaded in memory
|
|
622
|
+
# Load it, update, and save back
|
|
623
|
+
schedule = self._persistence.load_schedule(name)
|
|
624
|
+
if schedule:
|
|
625
|
+
schedule.enabled = True
|
|
626
|
+
self._persistence.save_schedule(schedule)
|
|
627
|
+
# Also add to in-memory schedules
|
|
628
|
+
self.schedules[name] = schedule
|
|
558
629
|
|
|
559
630
|
def disable(self, name: str) -> None:
|
|
560
|
-
"""Disable a schedule.
|
|
631
|
+
"""Disable a schedule.
|
|
632
|
+
|
|
633
|
+
Handles both in-memory schedules and persisted schedules.
|
|
634
|
+
"""
|
|
561
635
|
if name in self.schedules:
|
|
562
636
|
self.schedules[name].enabled = False
|
|
563
637
|
if self._persistence:
|
|
564
638
|
self._persistence.save_schedule(self.schedules[name])
|
|
639
|
+
elif self._persistence:
|
|
640
|
+
# Schedule might be in persistence but not loaded in memory
|
|
641
|
+
# Load it, update, and save back
|
|
642
|
+
schedule = self._persistence.load_schedule(name)
|
|
643
|
+
if schedule:
|
|
644
|
+
schedule.enabled = False
|
|
645
|
+
self._persistence.save_schedule(schedule)
|
|
646
|
+
# Also add to in-memory schedules
|
|
647
|
+
self.schedules[name] = schedule
|
|
565
648
|
|
|
566
649
|
def _run_pipeline(self, schedule: Schedule) -> None:
|
|
567
650
|
"""Run a scheduled pipeline."""
|