flowyml 1.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.
- flowyml/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
flowyml/utils/debug.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Pipeline and step debugging tools."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
import contextlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StepDebugger:
|
|
9
|
+
"""Debug individual pipeline steps.
|
|
10
|
+
|
|
11
|
+
Features:
|
|
12
|
+
- Breakpoints
|
|
13
|
+
- Input/output inspection
|
|
14
|
+
- Exception debugging
|
|
15
|
+
- Step profiling
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
>>> from flowyml import step, StepDebugger
|
|
19
|
+
>>> debugger = StepDebugger()
|
|
20
|
+
>>> @step(outputs=["processed"])
|
|
21
|
+
... @debugger.breakpoint()
|
|
22
|
+
... def process_data(data):
|
|
23
|
+
... # Debugger will stop here
|
|
24
|
+
... return data * 2
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.breakpoints = set()
|
|
29
|
+
self.step_history = []
|
|
30
|
+
self.enabled = True
|
|
31
|
+
|
|
32
|
+
def break_at(self, condition: Callable | None = None):
|
|
33
|
+
"""Add a breakpoint to a step.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
condition: Optional condition function. Break only if returns True.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def decorator(func):
|
|
40
|
+
@wraps(func)
|
|
41
|
+
def wrapper(*args, **kwargs):
|
|
42
|
+
if not self.enabled:
|
|
43
|
+
return func(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
# Check condition
|
|
46
|
+
should_break = True
|
|
47
|
+
if condition:
|
|
48
|
+
should_break = condition(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
if should_break:
|
|
51
|
+
while True:
|
|
52
|
+
cmd = input("\n(debug) ").strip()
|
|
53
|
+
|
|
54
|
+
if cmd == "c":
|
|
55
|
+
break
|
|
56
|
+
if cmd == "i":
|
|
57
|
+
pass
|
|
58
|
+
elif cmd.startswith("p "):
|
|
59
|
+
expr = cmd[2:]
|
|
60
|
+
with contextlib.suppress(Exception):
|
|
61
|
+
# Evaluate in context
|
|
62
|
+
result = eval(expr, {"args": args, "kwargs": kwargs})
|
|
63
|
+
elif cmd == "pdb":
|
|
64
|
+
import pdb # noqa: T100
|
|
65
|
+
|
|
66
|
+
pdb.set_trace()
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
# Execute function
|
|
70
|
+
try:
|
|
71
|
+
result = func(*args, **kwargs)
|
|
72
|
+
|
|
73
|
+
# Log execution
|
|
74
|
+
self.step_history.append(
|
|
75
|
+
{
|
|
76
|
+
"step": func.__name__,
|
|
77
|
+
"inputs": {"args": args, "kwargs": kwargs},
|
|
78
|
+
"output": result,
|
|
79
|
+
"success": True,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
except Exception as e:
|
|
85
|
+
# Log error
|
|
86
|
+
self.step_history.append(
|
|
87
|
+
{
|
|
88
|
+
"step": func.__name__,
|
|
89
|
+
"inputs": {"args": args, "kwargs": kwargs},
|
|
90
|
+
"error": str(e),
|
|
91
|
+
"success": False,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
return wrapper
|
|
97
|
+
|
|
98
|
+
return decorator
|
|
99
|
+
|
|
100
|
+
def trace(self):
|
|
101
|
+
"""Enable step tracing (print inputs/outputs)."""
|
|
102
|
+
|
|
103
|
+
def decorator(func):
|
|
104
|
+
@wraps(func)
|
|
105
|
+
def wrapper(*args, **kwargs):
|
|
106
|
+
if not self.enabled:
|
|
107
|
+
return func(*args, **kwargs)
|
|
108
|
+
|
|
109
|
+
result = func(*args, **kwargs)
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
return wrapper
|
|
114
|
+
|
|
115
|
+
return decorator
|
|
116
|
+
|
|
117
|
+
def profile(self):
|
|
118
|
+
"""Profile step execution time."""
|
|
119
|
+
|
|
120
|
+
def decorator(func):
|
|
121
|
+
@wraps(func)
|
|
122
|
+
def wrapper(*args, **kwargs):
|
|
123
|
+
import time
|
|
124
|
+
|
|
125
|
+
start = time.time()
|
|
126
|
+
result = func(*args, **kwargs)
|
|
127
|
+
time.time() - start
|
|
128
|
+
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
return wrapper
|
|
132
|
+
|
|
133
|
+
return decorator
|
|
134
|
+
|
|
135
|
+
def get_history(self):
|
|
136
|
+
"""Get step execution history."""
|
|
137
|
+
return self.step_history
|
|
138
|
+
|
|
139
|
+
def clear_history(self) -> None:
|
|
140
|
+
"""Clear execution history."""
|
|
141
|
+
self.step_history = []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class PipelineDebugger:
|
|
145
|
+
"""Debug entire pipelines.
|
|
146
|
+
|
|
147
|
+
Features:
|
|
148
|
+
- Step-by-step execution
|
|
149
|
+
- DAG visualization
|
|
150
|
+
- Execution replay
|
|
151
|
+
- Error analysis
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, pipeline):
|
|
155
|
+
self.pipeline = pipeline
|
|
156
|
+
self.execution_log = []
|
|
157
|
+
|
|
158
|
+
def step_through(self) -> None:
|
|
159
|
+
"""Execute pipeline step-by-step with breaks."""
|
|
160
|
+
self.pipeline.build()
|
|
161
|
+
order = self.pipeline.dag.topological_sort()
|
|
162
|
+
|
|
163
|
+
for _ in order:
|
|
164
|
+
response = input("\nExecute this step? [Y/n/q]: ").lower()
|
|
165
|
+
|
|
166
|
+
if response == "q":
|
|
167
|
+
break
|
|
168
|
+
if response == "n":
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Execute step would happen here in actual implementation
|
|
172
|
+
|
|
173
|
+
def visualize_dag(self) -> None:
|
|
174
|
+
"""Visualize the pipeline DAG."""
|
|
175
|
+
self.pipeline.build()
|
|
176
|
+
|
|
177
|
+
def analyze_errors(self, run_id: str) -> None:
|
|
178
|
+
"""Analyze errors from a failed run."""
|
|
179
|
+
# Load run metadata
|
|
180
|
+
metadata = self.pipeline.metadata_store.load_run(run_id)
|
|
181
|
+
|
|
182
|
+
if not metadata:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
steps_metadata = metadata.get("steps", {})
|
|
186
|
+
|
|
187
|
+
failed_steps = []
|
|
188
|
+
for step_name, step_data in steps_metadata.items():
|
|
189
|
+
if not step_data.get("success", True):
|
|
190
|
+
failed_steps.append((step_name, step_data))
|
|
191
|
+
|
|
192
|
+
if not failed_steps:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
for _, step_data in failed_steps:
|
|
196
|
+
if step_data.get("source_code"):
|
|
197
|
+
for _ in step_data["source_code"].split("\n")[:10]:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
def replay_run(self, run_id: str, start_from: str | None = None) -> None:
|
|
201
|
+
"""Replay a previous run, optionally starting from a specific step."""
|
|
202
|
+
if start_from:
|
|
203
|
+
pass
|
|
204
|
+
_ = run_id # Unused in placeholder
|
|
205
|
+
|
|
206
|
+
# Implementation would load state and re-execute
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def inspect_step(step) -> None:
|
|
210
|
+
"""Inspect a step's metadata.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
step: Step to inspect
|
|
214
|
+
"""
|
|
215
|
+
if step.source_code:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def print_dag(pipeline) -> None:
|
|
220
|
+
"""Pretty print pipeline DAG."""
|
|
221
|
+
pipeline.build()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Global debugger instance
|
|
225
|
+
_global_debugger = StepDebugger()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def debug_step(*args, **kwargs):
|
|
229
|
+
"""Convenience function to debug a step."""
|
|
230
|
+
return _global_debugger.break_at(*args, **kwargs)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def trace_step():
|
|
234
|
+
"""Convenience function to trace a step."""
|
|
235
|
+
return _global_debugger.trace()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def profile_step():
|
|
239
|
+
"""Convenience function to profile a step."""
|
|
240
|
+
return _global_debugger.profile()
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Environment capture utilities for reproducibility."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import platform
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_python_info() -> dict[str, str]:
|
|
12
|
+
"""Get Python version and implementation info.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Dictionary with Python information
|
|
16
|
+
"""
|
|
17
|
+
return {
|
|
18
|
+
"version": sys.version,
|
|
19
|
+
"version_info": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
20
|
+
"implementation": platform.python_implementation(),
|
|
21
|
+
"compiler": platform.python_compiler(),
|
|
22
|
+
"executable": sys.executable,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_system_info() -> dict[str, str]:
|
|
27
|
+
"""Get system and hardware information.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dictionary with system information
|
|
31
|
+
"""
|
|
32
|
+
info = {
|
|
33
|
+
"platform": platform.platform(),
|
|
34
|
+
"system": platform.system(),
|
|
35
|
+
"release": platform.release(),
|
|
36
|
+
"machine": platform.machine(),
|
|
37
|
+
"processor": platform.processor(),
|
|
38
|
+
"hostname": platform.node(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Add CPU count if available
|
|
42
|
+
try:
|
|
43
|
+
import multiprocessing
|
|
44
|
+
|
|
45
|
+
info["cpu_count"] = str(multiprocessing.cpu_count())
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Add GPU info if available
|
|
50
|
+
try:
|
|
51
|
+
import torch
|
|
52
|
+
|
|
53
|
+
if torch.cuda.is_available():
|
|
54
|
+
info["cuda_available"] = "true"
|
|
55
|
+
info["cuda_version"] = torch.version.cuda
|
|
56
|
+
info["gpu_count"] = str(torch.cuda.device_count())
|
|
57
|
+
info["gpu_name"] = torch.cuda.get_device_name(0)
|
|
58
|
+
else:
|
|
59
|
+
info["cuda_available"] = "false"
|
|
60
|
+
except ImportError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
return info
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_installed_packages() -> dict[str, str]:
|
|
67
|
+
"""Get list of installed packages and versions.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Dictionary mapping package names to versions
|
|
71
|
+
"""
|
|
72
|
+
packages = {}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
import pkg_resources
|
|
76
|
+
|
|
77
|
+
for dist in pkg_resources.working_set:
|
|
78
|
+
packages[dist.project_name] = dist.version
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Alternative method using importlib.metadata (Python 3.8+)
|
|
83
|
+
if not packages:
|
|
84
|
+
try:
|
|
85
|
+
from importlib import metadata
|
|
86
|
+
|
|
87
|
+
for dist in metadata.distributions():
|
|
88
|
+
packages[dist.name] = dist.version
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
return packages
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_key_packages() -> dict[str, str]:
|
|
96
|
+
"""Get versions of key ML/data packages.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Dictionary with key package versions
|
|
100
|
+
"""
|
|
101
|
+
key_packages = [
|
|
102
|
+
"numpy",
|
|
103
|
+
"pandas",
|
|
104
|
+
"torch",
|
|
105
|
+
"tensorflow",
|
|
106
|
+
"scikit-learn",
|
|
107
|
+
"transformers",
|
|
108
|
+
"pydantic",
|
|
109
|
+
"flowyml",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
versions = {}
|
|
113
|
+
all_packages = get_installed_packages()
|
|
114
|
+
|
|
115
|
+
for pkg in key_packages:
|
|
116
|
+
if pkg in all_packages:
|
|
117
|
+
versions[pkg] = all_packages[pkg]
|
|
118
|
+
|
|
119
|
+
return versions
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_environment_variables(include_all: bool = False) -> dict[str, str]:
|
|
123
|
+
"""Get relevant environment variables.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
include_all: Include all environment variables (may contain secrets!)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dictionary of environment variables
|
|
130
|
+
"""
|
|
131
|
+
if include_all:
|
|
132
|
+
return dict(os.environ)
|
|
133
|
+
|
|
134
|
+
# Only include safe, ML-relevant environment variables
|
|
135
|
+
safe_vars = [
|
|
136
|
+
"CUDA_VISIBLE_DEVICES",
|
|
137
|
+
"OMP_NUM_THREADS",
|
|
138
|
+
"MKL_NUM_THREADS",
|
|
139
|
+
"PYTHONPATH",
|
|
140
|
+
"PATH",
|
|
141
|
+
"HOME",
|
|
142
|
+
"USER",
|
|
143
|
+
"SHELL",
|
|
144
|
+
"TERM",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
env_vars = {}
|
|
148
|
+
for var in safe_vars:
|
|
149
|
+
if var in os.environ:
|
|
150
|
+
env_vars[var] = os.environ[var]
|
|
151
|
+
|
|
152
|
+
return env_vars
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_working_directory() -> str:
|
|
156
|
+
"""Get current working directory.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Working directory path
|
|
160
|
+
"""
|
|
161
|
+
return str(Path.cwd())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def capture_environment(
|
|
165
|
+
include_packages: bool = True,
|
|
166
|
+
include_git: bool = True,
|
|
167
|
+
include_system: bool = True,
|
|
168
|
+
project_root: Path | None = None,
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
"""Capture complete environment information.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
include_packages: Include installed packages
|
|
174
|
+
include_git: Include git information (from project root only)
|
|
175
|
+
include_system: Include system information
|
|
176
|
+
project_root: Project root directory (for git tracking)
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dictionary with environment information
|
|
180
|
+
"""
|
|
181
|
+
env_info = {
|
|
182
|
+
"captured_at": datetime.now().isoformat(),
|
|
183
|
+
"python": get_python_info(),
|
|
184
|
+
"working_directory": get_working_directory(),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if include_system:
|
|
188
|
+
env_info["system"] = get_system_info()
|
|
189
|
+
|
|
190
|
+
if include_packages:
|
|
191
|
+
env_info["key_packages"] = get_key_packages()
|
|
192
|
+
|
|
193
|
+
if include_git:
|
|
194
|
+
# Only get git info from the project directory (not flowyml's directory)
|
|
195
|
+
from flowyml.utils.git import get_git_info
|
|
196
|
+
|
|
197
|
+
git_path = project_root or Path.cwd()
|
|
198
|
+
git_info = get_git_info(git_path)
|
|
199
|
+
|
|
200
|
+
if git_info.is_available:
|
|
201
|
+
env_info["git"] = git_info.to_dict()
|
|
202
|
+
|
|
203
|
+
env_info["environment_variables"] = get_environment_variables()
|
|
204
|
+
|
|
205
|
+
return env_info
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def save_environment(
|
|
209
|
+
output_path: Path,
|
|
210
|
+
include_packages: bool = True,
|
|
211
|
+
include_git: bool = True,
|
|
212
|
+
include_system: bool = True,
|
|
213
|
+
project_root: Path | None = None,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Save environment information to file.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
output_path: Path to save environment info
|
|
219
|
+
include_packages: Include installed packages
|
|
220
|
+
include_git: Include git information
|
|
221
|
+
include_system: Include system information
|
|
222
|
+
project_root: Project root directory
|
|
223
|
+
"""
|
|
224
|
+
import json
|
|
225
|
+
|
|
226
|
+
env_info = capture_environment(
|
|
227
|
+
include_packages=include_packages,
|
|
228
|
+
include_git=include_git,
|
|
229
|
+
include_system=include_system,
|
|
230
|
+
project_root=project_root,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
|
|
235
|
+
with open(output_path, "w") as f:
|
|
236
|
+
json.dump(env_info, f, indent=2)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def export_requirements(output_path: Path, export_format: str = "pip") -> None:
|
|
240
|
+
"""Export installed packages to requirements file.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
output_path: Path to save requirements
|
|
244
|
+
export_format: Format (pip, conda, poetry)
|
|
245
|
+
"""
|
|
246
|
+
packages = get_installed_packages()
|
|
247
|
+
|
|
248
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
249
|
+
|
|
250
|
+
if export_format == "pip":
|
|
251
|
+
with open(output_path, "w") as f:
|
|
252
|
+
for name, version in sorted(packages.items()):
|
|
253
|
+
f.write(f"{name}=={version}\n")
|
|
254
|
+
|
|
255
|
+
elif export_format == "conda":
|
|
256
|
+
with open(output_path, "w") as f:
|
|
257
|
+
f.write("name: flowyml-env\n")
|
|
258
|
+
f.write("channels:\n")
|
|
259
|
+
f.write(" - defaults\n")
|
|
260
|
+
f.write(" - conda-forge\n")
|
|
261
|
+
f.write("dependencies:\n")
|
|
262
|
+
for name, version in sorted(packages.items()):
|
|
263
|
+
f.write(f" - {name}=={version}\n")
|
|
264
|
+
|
|
265
|
+
elif export_format == "poetry":
|
|
266
|
+
with open(output_path, "w") as f:
|
|
267
|
+
f.write("[tool.poetry.dependencies]\n")
|
|
268
|
+
f.write(f'python = "^{sys.version_info.major}.{sys.version_info.minor}"\n')
|
|
269
|
+
for name, version in sorted(packages.items()):
|
|
270
|
+
if name != "python":
|
|
271
|
+
f.write(f'{name} = "^{version}"\n')
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def compare_environments(env1: dict[str, Any], env2: dict[str, Any]) -> dict[str, Any]:
|
|
275
|
+
"""Compare two environment captures.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
env1: First environment
|
|
279
|
+
env2: Second environment
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dictionary with differences
|
|
283
|
+
"""
|
|
284
|
+
differences = {}
|
|
285
|
+
|
|
286
|
+
# Compare Python versions
|
|
287
|
+
if env1.get("python", {}).get("version_info") != env2.get("python", {}).get("version_info"):
|
|
288
|
+
differences["python_version"] = {
|
|
289
|
+
"env1": env1.get("python", {}).get("version_info"),
|
|
290
|
+
"env2": env2.get("python", {}).get("version_info"),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Compare packages
|
|
294
|
+
packages1 = set(env1.get("key_packages", {}).items())
|
|
295
|
+
packages2 = set(env2.get("key_packages", {}).items())
|
|
296
|
+
|
|
297
|
+
if packages1 != packages2:
|
|
298
|
+
differences["package_differences"] = {
|
|
299
|
+
"only_in_env1": dict(packages1 - packages2),
|
|
300
|
+
"only_in_env2": dict(packages2 - packages1),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Compare git info
|
|
304
|
+
git1 = env1.get("git", {})
|
|
305
|
+
git2 = env2.get("git", {})
|
|
306
|
+
|
|
307
|
+
if git1.get("commit_hash") != git2.get("commit_hash"):
|
|
308
|
+
differences["git_commit"] = {
|
|
309
|
+
"env1": git1.get("commit_hash"),
|
|
310
|
+
"env2": git2.get("commit_hash"),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return differences
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def detect_environment_type() -> str:
|
|
317
|
+
"""Detect the type of environment (local, docker, cloud, etc.).
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Environment type string
|
|
321
|
+
"""
|
|
322
|
+
# Check for Docker
|
|
323
|
+
if Path("/.dockerenv").exists():
|
|
324
|
+
return "docker"
|
|
325
|
+
|
|
326
|
+
# Check for cloud environments
|
|
327
|
+
if "KUBERNETES_SERVICE_HOST" in os.environ:
|
|
328
|
+
return "kubernetes"
|
|
329
|
+
|
|
330
|
+
if "AWS_EXECUTION_ENV" in os.environ:
|
|
331
|
+
return "aws_lambda"
|
|
332
|
+
|
|
333
|
+
if "GOOGLE_CLOUD_PROJECT" in os.environ:
|
|
334
|
+
return "gcp"
|
|
335
|
+
|
|
336
|
+
if "AZURE_FUNCTIONS_ENVIRONMENT" in os.environ:
|
|
337
|
+
return "azure_functions"
|
|
338
|
+
|
|
339
|
+
# Check for notebooks
|
|
340
|
+
try:
|
|
341
|
+
get_ipython() # type: ignore
|
|
342
|
+
return "jupyter"
|
|
343
|
+
except NameError:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
return "local"
|