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
@@ -0,0 +1,315 @@
1
+ """Report asset for generated reports and visualizations."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Literal
7
+
8
+ from flowyml.assets.base import Asset, AssetMetadata
9
+
10
+
11
+ ReportFormat = Literal["html", "pdf", "markdown", "json"]
12
+
13
+
14
+ @dataclass
15
+ class ReportMetadata(AssetMetadata):
16
+ """Metadata specific to Reports."""
17
+
18
+ report_format: str = "html"
19
+ title: str | None = None
20
+ author: str | None = None
21
+ generated_at: str | None = None
22
+ file_path: str | None = None
23
+ file_size_bytes: int = 0
24
+ sections: list[str] = field(default_factory=list)
25
+
26
+
27
+ class Report(Asset):
28
+ """Asset representing a generated report.
29
+
30
+ Reports can be HTML dashboards, PDF documents, markdown files, or JSON data.
31
+
32
+ Example:
33
+ ```python
34
+ from flowyml import Report
35
+
36
+ # Create HTML report
37
+ report = Report.create(
38
+ content=html_content,
39
+ name="experiment_report",
40
+ format="html",
41
+ title="Experiment Results",
42
+ sections=["Overview", "Metrics", "Conclusions"],
43
+ )
44
+
45
+ # Save to file
46
+ report.save_to_file("reports/experiment_report.html")
47
+ ```
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ name: str,
53
+ content: Any | None = None,
54
+ report_format: ReportFormat = "html",
55
+ title: str | None = None,
56
+ file_path: str | None = None,
57
+ **kwargs,
58
+ ):
59
+ """Initialize a Report.
60
+
61
+ Args:
62
+ name: Name of the report
63
+ content: Report content (string or bytes)
64
+ report_format: Report format
65
+ title: Report title
66
+ file_path: Path to saved report file
67
+ **kwargs: Additional metadata
68
+ """
69
+ # Extract Report-specific arguments before passing to parent
70
+ # report_format is already correct
71
+ report_title = title or name
72
+ report_file_path = file_path
73
+
74
+ # Pass only Asset-compatible kwargs to parent
75
+ super().__init__(name=name, **kwargs)
76
+
77
+ # Add Report-specific attributes after parent init
78
+ self.metadata.report_format = report_format
79
+ self.metadata.title = report_title
80
+ self.metadata.generated_at = datetime.now().isoformat()
81
+ self.metadata.file_path = report_file_path
82
+ self.metadata.file_size_bytes = 0
83
+ self.metadata.sections = []
84
+
85
+ self._content = content
86
+
87
+ if content and isinstance(content, (str, bytes)):
88
+ self.metadata.file_size_bytes = len(content)
89
+
90
+ @property
91
+ def content(self) -> Any | None:
92
+ """Get report content."""
93
+ return self._content
94
+
95
+ @property
96
+ def report_format(self) -> str:
97
+ """Get report format."""
98
+ return self.metadata.report_format
99
+
100
+ @property
101
+ def title(self) -> str | None:
102
+ """Get report title."""
103
+ return self.metadata.title
104
+
105
+ @property
106
+ def file_path(self) -> str | None:
107
+ """Get file path if saved."""
108
+ return self.metadata.file_path
109
+
110
+ @property
111
+ def sections(self) -> list[str]:
112
+ """Get report sections."""
113
+ return self.metadata.sections
114
+
115
+ @classmethod
116
+ def create(
117
+ cls,
118
+ content: Any,
119
+ name: str | None = None,
120
+ report_format: ReportFormat = "html",
121
+ title: str | None = None,
122
+ sections: list[str] | None = None,
123
+ **kwargs,
124
+ ) -> "Report":
125
+ """Factory method to create a Report.
126
+
127
+ Args:
128
+ content: Report content
129
+ name: Report name (auto-generated if not provided)
130
+ report_format: Report format
131
+ title: Report title
132
+ sections: List of section names
133
+ **kwargs: Additional metadata
134
+
135
+ Returns:
136
+ New Report instance
137
+ """
138
+ if name is None:
139
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
140
+ name = f"report_{timestamp}"
141
+
142
+ if sections:
143
+ kwargs["sections"] = sections
144
+
145
+ return cls(
146
+ name=name,
147
+ content=content,
148
+ report_format=report_format,
149
+ title=title or name,
150
+ **kwargs,
151
+ )
152
+
153
+ def save_to_file(self, path: Path | str) -> Path:
154
+ """Save report content to file.
155
+
156
+ Args:
157
+ path: File path to save to
158
+
159
+ Returns:
160
+ Path where file was saved
161
+
162
+ Raises:
163
+ ValueError: If report has no content
164
+ """
165
+ if self._content is None:
166
+ raise ValueError("Cannot save report with no content")
167
+
168
+ path = Path(path)
169
+ path.parent.mkdir(parents=True, exist_ok=True)
170
+
171
+ # Write content
172
+ if isinstance(self._content, bytes):
173
+ with open(path, "wb") as f:
174
+ f.write(self._content)
175
+ else:
176
+ with open(path, "w", encoding="utf-8") as f:
177
+ f.write(str(self._content))
178
+
179
+ # Update metadata
180
+ self.metadata.file_path = str(path)
181
+ self.metadata.file_size_bytes = path.stat().st_size
182
+
183
+ return path
184
+
185
+ @classmethod
186
+ def load_from_file(cls, path: Path | str, name: str | None = None) -> "Report":
187
+ """Load report from file.
188
+
189
+ Args:
190
+ path: File path to load from
191
+ name: Report name (uses filename if not provided)
192
+
193
+ Returns:
194
+ Report instance
195
+
196
+ Raises:
197
+ FileNotFoundError: If file doesn't exist
198
+ """
199
+ path = Path(path)
200
+
201
+ if not path.exists():
202
+ raise FileNotFoundError(f"Report file not found: {path}")
203
+
204
+ # Detect format from extension
205
+ format_map = {
206
+ ".html": "html",
207
+ ".htm": "html",
208
+ ".pdf": "pdf",
209
+ ".md": "markdown",
210
+ ".markdown": "markdown",
211
+ ".json": "json",
212
+ }
213
+ report_format = format_map.get(path.suffix.lower(), "html")
214
+
215
+ # Read content
216
+ if report_format == "pdf":
217
+ with open(path, "rb") as f:
218
+ content = f.read()
219
+ else:
220
+ with open(path, encoding="utf-8") as f:
221
+ content = f.read()
222
+
223
+ # Use filename as name if not provided
224
+ if name is None:
225
+ name = path.stem
226
+
227
+ return cls(
228
+ name=name,
229
+ content=content,
230
+ report_format=report_format,
231
+ file_path=str(path),
232
+ )
233
+
234
+ def to_html(self) -> str:
235
+ """Convert report to HTML string.
236
+
237
+ Returns:
238
+ HTML content
239
+ """
240
+ if self.report_format == "html":
241
+ return str(self._content)
242
+
243
+ elif self.report_format == "markdown":
244
+ try:
245
+ import markdown
246
+
247
+ return markdown.markdown(str(self._content))
248
+ except ImportError:
249
+ # Fallback: wrap in pre tags
250
+ return f"<html><body><pre>{self._content}</pre></body></html>"
251
+
252
+ elif self.report_format == "json":
253
+ import json
254
+
255
+ data = json.loads(str(self._content))
256
+ # Simple JSON to HTML conversion
257
+ html = "<html><body><pre>"
258
+ html += json.dumps(data, indent=2)
259
+ html += "</pre></body></html>"
260
+ return html
261
+
262
+ else:
263
+ return f"<html><body><p>Format {self.report_format} not supported for HTML conversion</p></body></html>"
264
+
265
+ def open_in_browser(self) -> None:
266
+ """Open report in web browser.
267
+
268
+ Works best with HTML and PDF formats.
269
+ """
270
+ import webbrowser
271
+ import tempfile
272
+
273
+ if self.file_path:
274
+ # Open existing file
275
+ webbrowser.open(f"file://{Path(self.file_path).absolute()}")
276
+ else:
277
+ # Create temporary file
278
+ with tempfile.NamedTemporaryFile(
279
+ mode="w" if isinstance(self._content, str) else "wb",
280
+ suffix=f".{self.report_format}",
281
+ delete=False,
282
+ ) as f:
283
+ if isinstance(self._content, bytes):
284
+ f.write(self._content)
285
+ else:
286
+ f.write(str(self._content))
287
+
288
+ temp_path = f.name
289
+
290
+ webbrowser.open(f"file://{Path(temp_path).absolute()}")
291
+
292
+ def to_dict(self) -> dict[str, Any]:
293
+ """Convert Report to dictionary.
294
+
295
+ Returns:
296
+ Dictionary representation (excluding large content)
297
+ """
298
+ return {
299
+ "asset_id": self.asset_id,
300
+ "name": self.name,
301
+ "type": self.metadata.asset_type,
302
+ "report_format": self.report_format,
303
+ "title": self.title,
304
+ "file_path": self.file_path,
305
+ "file_size_bytes": self.metadata.file_size_bytes,
306
+ "sections": self.sections,
307
+ "generated_at": self.metadata.generated_at,
308
+ "created_at": self.created_at.isoformat(),
309
+ "tags": self.tags,
310
+ "properties": self.properties,
311
+ }
312
+
313
+ def __repr__(self) -> str:
314
+ size_kb = self.metadata.file_size_bytes / 1024 if self.metadata.file_size_bytes else 0
315
+ return f"Report(name='{self.name}', format='{self.report_format}', size={size_kb:.1f}KB, title='{self.title}')"
@@ -0,0 +1,5 @@
1
+ """CLI module for flowyml."""
2
+
3
+ from flowyml.cli.main import cli
4
+
5
+ __all__ = ["cli"]
@@ -0,0 +1,232 @@
1
+ """Experiment tracking CLI commands."""
2
+
3
+ from typing import Any
4
+ from pathlib import Path
5
+ import json
6
+
7
+
8
+ def list_experiments_cmd(limit: int = 10, pipeline: str | None = None) -> list[dict[str, Any]]:
9
+ """List experiments.
10
+
11
+ Args:
12
+ limit: Maximum number of experiments to return
13
+ pipeline: Filter by pipeline name
14
+
15
+ Returns:
16
+ List of experiment dictionaries
17
+ """
18
+ experiments_dir = Path(".flowyml/experiments")
19
+
20
+ if not experiments_dir.exists():
21
+ return []
22
+
23
+ experiments = []
24
+
25
+ for exp_file in experiments_dir.glob("*.json"):
26
+ try:
27
+ with open(exp_file) as f:
28
+ exp_data = json.load(f)
29
+
30
+ # Filter by pipeline if specified
31
+ if pipeline and exp_data.get("pipeline_name") != pipeline:
32
+ continue
33
+
34
+ experiments.append(exp_data)
35
+ except Exception:
36
+ continue
37
+
38
+ # Sort by created_at
39
+ experiments.sort(key=lambda x: x.get("created_at", ""), reverse=True)
40
+
41
+ return experiments[:limit]
42
+
43
+
44
+ def compare_runs(run_ids: list[str]) -> str:
45
+ """Compare multiple experiment runs.
46
+
47
+ Args:
48
+ run_ids: List of run IDs to compare
49
+
50
+ Returns:
51
+ Formatted comparison string
52
+ """
53
+ from flowyml.tracking.runs import Run
54
+
55
+ runs = []
56
+ for run_id in run_ids:
57
+ run_path = Path(f".flowyml/runs/{run_id}.json")
58
+ if run_path.exists():
59
+ run = Run.load(run_path)
60
+ runs.append(run)
61
+
62
+ if not runs:
63
+ return "No runs found"
64
+
65
+ # Build comparison table
66
+ lines = []
67
+ lines.append("\nRun Comparison:")
68
+ lines.append("-" * 80)
69
+
70
+ # Headers
71
+ lines.append(f"{'Metric':<30} " + " ".join(f"{r.id[:8]:>12}" for r in runs))
72
+ lines.append("-" * 80)
73
+
74
+ # Get all metric names
75
+ all_metrics = set()
76
+ for run in runs:
77
+ all_metrics.update(run.metadata.metrics.keys())
78
+
79
+ # Print metrics
80
+ for metric in sorted(all_metrics):
81
+ values = []
82
+ for run in runs:
83
+ value = run.metadata.metrics.get(metric, "-")
84
+ if isinstance(value, float):
85
+ values.append(f"{value:>12.4f}")
86
+ else:
87
+ values.append(f"{str(value):>12}")
88
+
89
+ lines.append(f"{metric:<30} " + " ".join(values))
90
+
91
+ lines.append("-" * 80)
92
+
93
+ # Get all parameter names
94
+ all_params = set()
95
+ for run in runs:
96
+ all_params.update(run.metadata.parameters.keys())
97
+
98
+ if all_params:
99
+ lines.append("\nParameters:")
100
+ lines.append("-" * 80)
101
+
102
+ for param in sorted(all_params):
103
+ values = []
104
+ for run in runs:
105
+ value = run.metadata.parameters.get(param, "-")
106
+ values.append(f"{str(value):>12}")
107
+
108
+ lines.append(f"{param:<30} " + " ".join(values))
109
+
110
+ lines.append("-" * 80)
111
+
112
+ # Duration
113
+ lines.append("\nDuration:")
114
+ durations = [f"{r.metadata.duration:>12.2f}s" if r.metadata.duration else f"{'N/A':>12}" for r in runs]
115
+ lines.append(f"{'Time':<30} " + " ".join(durations))
116
+
117
+ lines.append("-" * 80)
118
+
119
+ return "\n".join(lines)
120
+
121
+
122
+ def export_experiment(run_id: str, export_format: str = "html") -> str:
123
+ """Export experiment run.
124
+
125
+ Args:
126
+ run_id: Run ID to export
127
+ export_format: Export format (html, json, markdown)
128
+
129
+ Returns:
130
+ Path to exported file
131
+ """
132
+ from flowyml.tracking.runs import Run
133
+
134
+ run_path = Path(f".flowyml/runs/{run_id}.json")
135
+ if not run_path.exists():
136
+ raise FileNotFoundError(f"Run {run_id} not found")
137
+
138
+ run = Run.load(run_path)
139
+
140
+ output_dir = Path(".flowyml/exports")
141
+ output_dir.mkdir(parents=True, exist_ok=True)
142
+
143
+ if export_format == "json":
144
+ output_file = output_dir / f"{run_id}.json"
145
+ run.save(output_file)
146
+
147
+ elif export_format == "markdown":
148
+ output_file = output_dir / f"{run_id}.md"
149
+ markdown = generate_markdown_report(run)
150
+ with open(output_file, "w") as f:
151
+ f.write(markdown)
152
+
153
+ elif export_format == "html":
154
+ output_file = output_dir / f"{run_id}.html"
155
+ html = generate_html_report(run)
156
+ with open(output_file, "w") as f:
157
+ f.write(html)
158
+
159
+ else:
160
+ raise ValueError(f"Unknown format: {export_format}")
161
+
162
+ return str(output_file)
163
+
164
+
165
+ def generate_markdown_report(run) -> str:
166
+ """Generate markdown report for run."""
167
+ lines = []
168
+ lines.append(f"# Run {run.id}")
169
+ lines.append(f"\n**Status:** {run.status}")
170
+ lines.append(f"**Duration:** {run.metadata.duration:.2f}s")
171
+ lines.append(f"**Started:** {run.metadata.start_time}")
172
+
173
+ if run.metadata.metrics:
174
+ lines.append("\n## Metrics")
175
+ for key, value in run.metadata.metrics.items():
176
+ lines.append(f"- **{key}:** {value}")
177
+
178
+ if run.metadata.parameters:
179
+ lines.append("\n## Parameters")
180
+ for key, value in run.metadata.parameters.items():
181
+ lines.append(f"- **{key}:** {value}")
182
+
183
+ return "\n".join(lines)
184
+
185
+
186
+ def generate_html_report(run) -> str:
187
+ """Generate HTML report for run."""
188
+ html = f"""
189
+ <!DOCTYPE html>
190
+ <html>
191
+ <head>
192
+ <title>Run {run.id}</title>
193
+ <style>
194
+ body {{ font-family: Arial, sans-serif; margin: 40px; }}
195
+ h1 {{ color: #333; }}
196
+ table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
197
+ th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
198
+ th {{ background-color: #4CAF50; color: white; }}
199
+ .status {{ font-weight: bold; color: #4CAF50; }}
200
+ </style>
201
+ </head>
202
+ <body>
203
+ <h1>Run {run.id}</h1>
204
+ <p><strong>Status:</strong> <span class="status">{run.status}</span></p>
205
+ <p><strong>Duration:</strong> {run.metadata.duration:.2f}s</p>
206
+ <p><strong>Started:</strong> {run.metadata.start_time}</p>
207
+
208
+ <h2>Metrics</h2>
209
+ <table>
210
+ <tr><th>Metric</th><th>Value</th></tr>
211
+ """
212
+
213
+ for key, value in run.metadata.metrics.items():
214
+ html += f" <tr><td>{key}</td><td>{value}</td></tr>\n"
215
+
216
+ html += """
217
+ </table>
218
+
219
+ <h2>Parameters</h2>
220
+ <table>
221
+ <tr><th>Parameter</th><th>Value</th></tr>
222
+ """
223
+
224
+ for key, value in run.metadata.parameters.items():
225
+ html += f" <tr><td>{key}</td><td>{value}</td></tr>\n"
226
+
227
+ html += """
228
+ </table>
229
+ </body>
230
+ </html>
231
+ """
232
+ return html