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/assets/report.py
ADDED
|
@@ -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}')"
|
flowyml/cli/__init__.py
ADDED
|
@@ -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
|