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.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. 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"