flowyml 1.7.2__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/metrics.py +5 -0
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +161 -26
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +22 -2
- flowyml/core/pipeline.py +34 -10
- flowyml/core/routing.py +558 -0
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- 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 +20 -1
- flowyml/ui/backend/routers/schedules.py +22 -17
- 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 +1404 -74
- flowyml/ui/frontend/package.json +3 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- 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/runs/[runId]/page.jsx +36 -24
- 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/AssetDetailsPanel.jsx +29 -7
- 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/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.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
- flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
- flowyml-1.7.2.dist-info/METADATA +0 -477
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""FlowyML Pipeline-Plugin Integration.
|
|
2
|
+
|
|
3
|
+
This module integrates the native plugin system with FlowyML pipelines,
|
|
4
|
+
allowing pipelines to automatically use the configured stack for:
|
|
5
|
+
- Experiment tracking (log params, metrics)
|
|
6
|
+
- Artifact storage (save outputs)
|
|
7
|
+
- Model registry (register models)
|
|
8
|
+
- Orchestration (run on configured platform)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
# flowyml.yaml
|
|
12
|
+
plugins:
|
|
13
|
+
experiment_tracker:
|
|
14
|
+
type: mlflow
|
|
15
|
+
artifact_store:
|
|
16
|
+
type: gcs
|
|
17
|
+
bucket: my-ml-artifacts
|
|
18
|
+
|
|
19
|
+
# In code - pipeline automatically uses configured stack
|
|
20
|
+
from flowyml import pipeline, step
|
|
21
|
+
from flowyml.plugins.integration import run_with_stack
|
|
22
|
+
|
|
23
|
+
@pipeline
|
|
24
|
+
def training_pipeline():
|
|
25
|
+
data = load_data()
|
|
26
|
+
model = train(data)
|
|
27
|
+
return model
|
|
28
|
+
|
|
29
|
+
# Run with automatic tracking and artifact storage
|
|
30
|
+
result = run_with_stack(training_pipeline)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
from typing import Any
|
|
35
|
+
from functools import wraps
|
|
36
|
+
import time
|
|
37
|
+
|
|
38
|
+
from flowyml.plugins.stack import (
|
|
39
|
+
start_run,
|
|
40
|
+
end_run,
|
|
41
|
+
log_params,
|
|
42
|
+
log_metrics,
|
|
43
|
+
set_tag,
|
|
44
|
+
save_artifact,
|
|
45
|
+
save_model,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class StackContext:
|
|
52
|
+
"""Context manager for running code with automatic stack integration.
|
|
53
|
+
|
|
54
|
+
Automatically:
|
|
55
|
+
- Starts an experiment run
|
|
56
|
+
- Logs timing and metadata
|
|
57
|
+
- Saves artifacts to configured store
|
|
58
|
+
- Ends the run with appropriate status
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
with StackContext("my_training") as ctx:
|
|
62
|
+
# Your code here - automatically tracked
|
|
63
|
+
model = train()
|
|
64
|
+
ctx.log_model(model, "classifier")
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
run_name: str,
|
|
70
|
+
experiment_name: str = None,
|
|
71
|
+
tags: dict = None,
|
|
72
|
+
log_system_info: bool = True,
|
|
73
|
+
):
|
|
74
|
+
"""Initialize the stack context.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
run_name: Name for this run.
|
|
78
|
+
experiment_name: Optional experiment name.
|
|
79
|
+
tags: Optional tags for the run.
|
|
80
|
+
log_system_info: If True, log system information.
|
|
81
|
+
"""
|
|
82
|
+
self.run_name = run_name
|
|
83
|
+
self.experiment_name = experiment_name
|
|
84
|
+
self.tags = tags or {}
|
|
85
|
+
self.log_system_info = log_system_info
|
|
86
|
+
self._run_id = None
|
|
87
|
+
self._start_time = None
|
|
88
|
+
self._artifacts = []
|
|
89
|
+
self._models = []
|
|
90
|
+
|
|
91
|
+
def __enter__(self) -> "StackContext":
|
|
92
|
+
"""Start the run."""
|
|
93
|
+
self._start_time = time.time()
|
|
94
|
+
self._run_id = start_run(
|
|
95
|
+
self.run_name,
|
|
96
|
+
experiment_name=self.experiment_name,
|
|
97
|
+
tags=self.tags,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if self.log_system_info:
|
|
101
|
+
self._log_system_info()
|
|
102
|
+
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
106
|
+
"""End the run."""
|
|
107
|
+
duration = time.time() - self._start_time
|
|
108
|
+
|
|
109
|
+
# Log final metrics
|
|
110
|
+
log_metrics({"duration_seconds": duration})
|
|
111
|
+
|
|
112
|
+
if exc_type:
|
|
113
|
+
set_tag("status", "FAILED")
|
|
114
|
+
set_tag("error_type", exc_type.__name__)
|
|
115
|
+
end_run("FAILED")
|
|
116
|
+
else:
|
|
117
|
+
set_tag("status", "COMPLETED")
|
|
118
|
+
end_run("FINISHED")
|
|
119
|
+
|
|
120
|
+
return False # Don't suppress exceptions
|
|
121
|
+
|
|
122
|
+
def _log_system_info(self):
|
|
123
|
+
"""Log system information."""
|
|
124
|
+
import platform
|
|
125
|
+
import sys
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
log_params(
|
|
129
|
+
{
|
|
130
|
+
"python_version": sys.version.split()[0],
|
|
131
|
+
"platform": platform.system(),
|
|
132
|
+
"machine": platform.machine(),
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.debug(f"Could not log system info: {e}")
|
|
137
|
+
|
|
138
|
+
def log_params(self, params: dict) -> None:
|
|
139
|
+
"""Log parameters."""
|
|
140
|
+
log_params(params)
|
|
141
|
+
|
|
142
|
+
def log_metrics(self, metrics: dict, step: int = None) -> None:
|
|
143
|
+
"""Log metrics."""
|
|
144
|
+
log_metrics(metrics, step)
|
|
145
|
+
|
|
146
|
+
def save_artifact(self, artifact: Any, path: str) -> str:
|
|
147
|
+
"""Save an artifact and track it.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
URI of the saved artifact.
|
|
151
|
+
"""
|
|
152
|
+
uri = save_artifact(artifact, path)
|
|
153
|
+
if uri:
|
|
154
|
+
self._artifacts.append(uri)
|
|
155
|
+
set_tag(f"artifact_{len(self._artifacts)}", uri)
|
|
156
|
+
return uri
|
|
157
|
+
|
|
158
|
+
def save_model(self, model: Any, path: str, model_type: str = None) -> str:
|
|
159
|
+
"""Save a model and track it.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
URI of the saved model.
|
|
163
|
+
"""
|
|
164
|
+
uri = save_model(model, path, model_type=model_type)
|
|
165
|
+
if uri:
|
|
166
|
+
self._models.append(uri)
|
|
167
|
+
set_tag(f"model_{len(self._models)}", uri)
|
|
168
|
+
return uri
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run_with_stack(
|
|
172
|
+
pipeline_or_func,
|
|
173
|
+
run_name: str = None,
|
|
174
|
+
experiment_name: str = None,
|
|
175
|
+
parameters: dict = None,
|
|
176
|
+
tags: dict = None,
|
|
177
|
+
):
|
|
178
|
+
"""Run a pipeline or function with automatic stack integration.
|
|
179
|
+
|
|
180
|
+
This wraps the execution with:
|
|
181
|
+
- Automatic experiment tracking
|
|
182
|
+
- Parameter logging
|
|
183
|
+
- Timing and metrics
|
|
184
|
+
- Error handling
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
pipeline_or_func: Pipeline or callable to run.
|
|
188
|
+
run_name: Name for this run (defaults to function name).
|
|
189
|
+
experiment_name: Optional experiment name.
|
|
190
|
+
parameters: Parameters to pass and log.
|
|
191
|
+
tags: Optional tags for the run.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Result of the pipeline/function.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
@pipeline
|
|
198
|
+
def my_training():
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
# Run with full stack integration
|
|
202
|
+
result = run_with_stack(
|
|
203
|
+
my_training,
|
|
204
|
+
run_name="training_v1",
|
|
205
|
+
parameters={"lr": 0.001}
|
|
206
|
+
)
|
|
207
|
+
"""
|
|
208
|
+
# Determine run name
|
|
209
|
+
if run_name is None:
|
|
210
|
+
if hasattr(pipeline_or_func, "name"):
|
|
211
|
+
run_name = pipeline_or_func.name
|
|
212
|
+
elif hasattr(pipeline_or_func, "__name__"):
|
|
213
|
+
run_name = pipeline_or_func.__name__
|
|
214
|
+
else:
|
|
215
|
+
run_name = "unnamed_run"
|
|
216
|
+
|
|
217
|
+
parameters = parameters or {}
|
|
218
|
+
tags = tags or {}
|
|
219
|
+
|
|
220
|
+
with StackContext(run_name, experiment_name, tags) as ctx:
|
|
221
|
+
# Log parameters
|
|
222
|
+
if parameters:
|
|
223
|
+
ctx.log_params(parameters)
|
|
224
|
+
|
|
225
|
+
# Execute
|
|
226
|
+
if parameters:
|
|
227
|
+
result = pipeline_or_func(**parameters)
|
|
228
|
+
else:
|
|
229
|
+
result = pipeline_or_func()
|
|
230
|
+
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def tracked(
|
|
235
|
+
experiment_name: str = None,
|
|
236
|
+
log_params: bool = True,
|
|
237
|
+
log_result: bool = True,
|
|
238
|
+
):
|
|
239
|
+
"""Decorator to add automatic tracking to any function.
|
|
240
|
+
|
|
241
|
+
The decorated function will automatically:
|
|
242
|
+
- Start an experiment run
|
|
243
|
+
- Log function parameters
|
|
244
|
+
- Log execution time
|
|
245
|
+
- End the run with status
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
experiment_name: Optional experiment name.
|
|
249
|
+
log_params: If True, log function parameters.
|
|
250
|
+
log_result: If True, log result summary.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
@tracked(experiment_name="model_training")
|
|
254
|
+
def train_model(lr=0.001, epochs=100):
|
|
255
|
+
model = ...
|
|
256
|
+
return model
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def decorator(func):
|
|
260
|
+
@wraps(func)
|
|
261
|
+
def wrapper(*args, **kwargs):
|
|
262
|
+
run_name = func.__name__
|
|
263
|
+
|
|
264
|
+
with StackContext(run_name, experiment_name) as ctx:
|
|
265
|
+
# Log parameters
|
|
266
|
+
if log_params and kwargs:
|
|
267
|
+
ctx.log_params(kwargs)
|
|
268
|
+
|
|
269
|
+
# Execute
|
|
270
|
+
result = func(*args, **kwargs)
|
|
271
|
+
|
|
272
|
+
# Log result summary if applicable
|
|
273
|
+
if log_result and isinstance(result, dict):
|
|
274
|
+
metrics = {k: v for k, v in result.items() if isinstance(v, (int, float))}
|
|
275
|
+
if metrics:
|
|
276
|
+
ctx.log_metrics(metrics)
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
return wrapper
|
|
281
|
+
|
|
282
|
+
return decorator
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class PipelinePluginIntegration:
|
|
286
|
+
"""Integration layer between FlowyML pipelines and plugins.
|
|
287
|
+
|
|
288
|
+
This class provides hooks for the pipeline executor to automatically
|
|
289
|
+
use the configured stack.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
def __init__(self):
|
|
293
|
+
"""Initialize the integration."""
|
|
294
|
+
self._current_run = None
|
|
295
|
+
self._step_outputs = {}
|
|
296
|
+
|
|
297
|
+
def on_pipeline_start(self, pipeline_name: str, context: dict = None):
|
|
298
|
+
"""Called when a pipeline starts."""
|
|
299
|
+
self._current_run = start_run(
|
|
300
|
+
pipeline_name,
|
|
301
|
+
tags={"type": "pipeline"},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if context:
|
|
305
|
+
log_params(context)
|
|
306
|
+
|
|
307
|
+
def on_pipeline_end(self, success: bool, error: Exception = None):
|
|
308
|
+
"""Called when a pipeline ends."""
|
|
309
|
+
if success:
|
|
310
|
+
end_run("FINISHED")
|
|
311
|
+
else:
|
|
312
|
+
if error:
|
|
313
|
+
set_tag("error", str(error))
|
|
314
|
+
end_run("FAILED")
|
|
315
|
+
|
|
316
|
+
self._current_run = None
|
|
317
|
+
|
|
318
|
+
def on_step_start(self, step_name: str, inputs: dict = None):
|
|
319
|
+
"""Called when a step starts."""
|
|
320
|
+
set_tag(f"step_{step_name}_status", "started")
|
|
321
|
+
|
|
322
|
+
if inputs:
|
|
323
|
+
# Log input sizes/shapes if applicable
|
|
324
|
+
for key, value in inputs.items():
|
|
325
|
+
if hasattr(value, "shape"):
|
|
326
|
+
set_tag(f"input_{key}_shape", str(value.shape))
|
|
327
|
+
|
|
328
|
+
def on_step_end(
|
|
329
|
+
self,
|
|
330
|
+
step_name: str,
|
|
331
|
+
outputs: dict = None,
|
|
332
|
+
duration: float = None,
|
|
333
|
+
cached: bool = False,
|
|
334
|
+
):
|
|
335
|
+
"""Called when a step ends."""
|
|
336
|
+
set_tag(f"step_{step_name}_status", "completed")
|
|
337
|
+
|
|
338
|
+
if duration:
|
|
339
|
+
log_metrics({f"{step_name}_duration": duration})
|
|
340
|
+
|
|
341
|
+
if cached:
|
|
342
|
+
set_tag(f"step_{step_name}_cached", "true")
|
|
343
|
+
|
|
344
|
+
# Store outputs for later saving
|
|
345
|
+
if outputs:
|
|
346
|
+
self._step_outputs[step_name] = outputs
|
|
347
|
+
|
|
348
|
+
def on_step_error(self, step_name: str, error: Exception):
|
|
349
|
+
"""Called when a step errors."""
|
|
350
|
+
set_tag(f"step_{step_name}_status", "failed")
|
|
351
|
+
set_tag(f"step_{step_name}_error", str(error))
|
|
352
|
+
|
|
353
|
+
def save_step_outputs(self, step_name: str, outputs: dict):
|
|
354
|
+
"""Save step outputs to the artifact store."""
|
|
355
|
+
for output_name, value in outputs.items():
|
|
356
|
+
path = f"steps/{step_name}/{output_name}"
|
|
357
|
+
save_artifact(value, path)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# Global integration instance
|
|
361
|
+
_integration: PipelinePluginIntegration | None = None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def get_integration() -> PipelinePluginIntegration:
|
|
365
|
+
"""Get the global pipeline-plugin integration."""
|
|
366
|
+
global _integration
|
|
367
|
+
if _integration is None:
|
|
368
|
+
_integration = PipelinePluginIntegration()
|
|
369
|
+
return _integration
|