expops 0.1.3__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.
- expops-0.1.3.dist-info/METADATA +826 -0
- expops-0.1.3.dist-info/RECORD +86 -0
- expops-0.1.3.dist-info/WHEEL +5 -0
- expops-0.1.3.dist-info/entry_points.txt +3 -0
- expops-0.1.3.dist-info/licenses/LICENSE +674 -0
- expops-0.1.3.dist-info/top_level.txt +1 -0
- mlops/__init__.py +0 -0
- mlops/__main__.py +11 -0
- mlops/_version.py +34 -0
- mlops/adapters/__init__.py +12 -0
- mlops/adapters/base.py +86 -0
- mlops/adapters/config_schema.py +89 -0
- mlops/adapters/custom/__init__.py +3 -0
- mlops/adapters/custom/custom_adapter.py +447 -0
- mlops/adapters/plugin_manager.py +113 -0
- mlops/adapters/sklearn/__init__.py +3 -0
- mlops/adapters/sklearn/adapter.py +94 -0
- mlops/cluster/__init__.py +3 -0
- mlops/cluster/controller.py +496 -0
- mlops/cluster/process_runner.py +91 -0
- mlops/cluster/providers.py +258 -0
- mlops/core/__init__.py +95 -0
- mlops/core/custom_model_base.py +38 -0
- mlops/core/dask_networkx_executor.py +1265 -0
- mlops/core/executor_worker.py +1239 -0
- mlops/core/experiment_tracker.py +81 -0
- mlops/core/graph_types.py +64 -0
- mlops/core/networkx_parser.py +135 -0
- mlops/core/payload_spill.py +278 -0
- mlops/core/pipeline_utils.py +162 -0
- mlops/core/process_hashing.py +216 -0
- mlops/core/step_state_manager.py +1298 -0
- mlops/core/step_system.py +956 -0
- mlops/core/workspace.py +99 -0
- mlops/environment/__init__.py +10 -0
- mlops/environment/base.py +43 -0
- mlops/environment/conda_manager.py +307 -0
- mlops/environment/factory.py +70 -0
- mlops/environment/pyenv_manager.py +146 -0
- mlops/environment/setup_env.py +31 -0
- mlops/environment/system_manager.py +66 -0
- mlops/environment/utils.py +105 -0
- mlops/environment/venv_manager.py +134 -0
- mlops/main.py +527 -0
- mlops/managers/project_manager.py +400 -0
- mlops/managers/reproducibility_manager.py +575 -0
- mlops/platform.py +996 -0
- mlops/reporting/__init__.py +16 -0
- mlops/reporting/context.py +187 -0
- mlops/reporting/entrypoint.py +292 -0
- mlops/reporting/kv_utils.py +77 -0
- mlops/reporting/registry.py +50 -0
- mlops/runtime/__init__.py +9 -0
- mlops/runtime/context.py +34 -0
- mlops/runtime/env_export.py +113 -0
- mlops/storage/__init__.py +12 -0
- mlops/storage/adapters/__init__.py +9 -0
- mlops/storage/adapters/gcp_kv_store.py +778 -0
- mlops/storage/adapters/gcs_object_store.py +96 -0
- mlops/storage/adapters/memory_store.py +240 -0
- mlops/storage/adapters/redis_store.py +438 -0
- mlops/storage/factory.py +199 -0
- mlops/storage/interfaces/__init__.py +6 -0
- mlops/storage/interfaces/kv_store.py +118 -0
- mlops/storage/path_utils.py +38 -0
- mlops/templates/premier-league/charts/plot_metrics.js +70 -0
- mlops/templates/premier-league/charts/plot_metrics.py +145 -0
- mlops/templates/premier-league/charts/requirements.txt +6 -0
- mlops/templates/premier-league/configs/cluster_config.yaml +13 -0
- mlops/templates/premier-league/configs/project_config.yaml +207 -0
- mlops/templates/premier-league/data/England CSV.csv +12154 -0
- mlops/templates/premier-league/models/premier_league_model.py +638 -0
- mlops/templates/premier-league/requirements.txt +8 -0
- mlops/templates/sklearn-basic/README.md +22 -0
- mlops/templates/sklearn-basic/charts/plot_metrics.py +85 -0
- mlops/templates/sklearn-basic/charts/requirements.txt +3 -0
- mlops/templates/sklearn-basic/configs/project_config.yaml +64 -0
- mlops/templates/sklearn-basic/data/train.csv +14 -0
- mlops/templates/sklearn-basic/models/model.py +62 -0
- mlops/templates/sklearn-basic/requirements.txt +10 -0
- mlops/web/__init__.py +3 -0
- mlops/web/server.py +585 -0
- mlops/web/ui/index.html +52 -0
- mlops/web/ui/mlops-charts.js +357 -0
- mlops/web/ui/script.js +1244 -0
- mlops/web/ui/styles.css +248 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MLOps Dynamic Charts SDK
|
|
3
|
+
*
|
|
4
|
+
* This library provides a user-friendly API for creating dynamic charts that
|
|
5
|
+
* listen to metrics in real-time, similar to the Python @chart() decorator.
|
|
6
|
+
*
|
|
7
|
+
* System logic is abstracted here, while users write chart definitions in their
|
|
8
|
+
* project-specific chart files.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ==================== System Internal Classes ====================
|
|
12
|
+
|
|
13
|
+
class MetricsListener {
|
|
14
|
+
constructor(projectId, runId, apiBase) {
|
|
15
|
+
this.projectId = projectId;
|
|
16
|
+
this.runId = runId;
|
|
17
|
+
this.apiBase = apiBase;
|
|
18
|
+
this.listeners = new Map(); // probe_path -> { callbacks, currentData, pollTimer }
|
|
19
|
+
this.runStatusTimer = null;
|
|
20
|
+
this.runFinished = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to metrics updates for a probe path
|
|
25
|
+
* @param {string} probePath - The probe path (e.g., "nn_training_a/train_and_evaluate_nn")
|
|
26
|
+
* @param {Function} callback - Called when metrics update: (metrics) => void
|
|
27
|
+
* @returns {Function} Unsubscribe function
|
|
28
|
+
*/
|
|
29
|
+
subscribe(probePath, callback) {
|
|
30
|
+
if (!this.listeners.has(probePath)) {
|
|
31
|
+
const listenerData = {
|
|
32
|
+
callbacks: new Set(),
|
|
33
|
+
currentData: null,
|
|
34
|
+
pollTimer: null
|
|
35
|
+
};
|
|
36
|
+
this.listeners.set(probePath, listenerData);
|
|
37
|
+
|
|
38
|
+
// Start polling for this probe
|
|
39
|
+
this._startPolling(probePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const listenerData = this.listeners.get(probePath);
|
|
43
|
+
listenerData.callbacks.add(callback);
|
|
44
|
+
|
|
45
|
+
// Immediately invoke callback if we have data
|
|
46
|
+
if (listenerData.currentData) {
|
|
47
|
+
callback(listenerData.currentData);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Return unsubscribe function
|
|
51
|
+
return () => {
|
|
52
|
+
listenerData.callbacks.delete(callback);
|
|
53
|
+
if (listenerData.callbacks.size === 0) {
|
|
54
|
+
this._stopPolling(probePath);
|
|
55
|
+
this.listeners.delete(probePath);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to multiple probe paths
|
|
62
|
+
* @param {Object} probePaths - Map of { key: probePath }
|
|
63
|
+
* @param {Function} callback - Called with { key: metrics, ... }
|
|
64
|
+
* @returns {Function} Unsubscribe function
|
|
65
|
+
*/
|
|
66
|
+
subscribeAll(probePaths, callback) {
|
|
67
|
+
const aggregatedData = {};
|
|
68
|
+
const unsubscribers = [];
|
|
69
|
+
|
|
70
|
+
for (const [key, probePath] of Object.entries(probePaths)) {
|
|
71
|
+
const unsub = this.subscribe(probePath, (metrics) => {
|
|
72
|
+
aggregatedData[key] = metrics;
|
|
73
|
+
callback(aggregatedData);
|
|
74
|
+
});
|
|
75
|
+
unsubscribers.push(unsub);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
unsubscribers.forEach(unsub => unsub());
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async _fetchMetrics(probePath) {
|
|
84
|
+
try {
|
|
85
|
+
const encodedPath = encodeURIComponent(probePath);
|
|
86
|
+
const url = `${this.apiBase}/projects/${encodeURIComponent(this.projectId)}/runs/${encodeURIComponent(this.runId)}/metrics/${encodedPath}`;
|
|
87
|
+
const response = await fetch(url);
|
|
88
|
+
if (!response.ok) return null;
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
return data.metrics || null;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(`Failed to fetch metrics for ${probePath}:`, error);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_startPolling(probePath) {
|
|
98
|
+
const listenerData = this.listeners.get(probePath);
|
|
99
|
+
if (!listenerData) return;
|
|
100
|
+
|
|
101
|
+
const poll = async () => {
|
|
102
|
+
if (this.runFinished) {
|
|
103
|
+
this._stopPolling(probePath);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const metrics = await this._fetchMetrics(probePath);
|
|
108
|
+
if (metrics !== null) {
|
|
109
|
+
listenerData.currentData = metrics;
|
|
110
|
+
listenerData.callbacks.forEach(callback => {
|
|
111
|
+
try {
|
|
112
|
+
callback(metrics);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Error in metrics callback:', error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Initial fetch
|
|
121
|
+
poll();
|
|
122
|
+
|
|
123
|
+
// Poll every 2 seconds
|
|
124
|
+
listenerData.pollTimer = setInterval(poll, 2000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_stopPolling(probePath) {
|
|
128
|
+
const listenerData = this.listeners.get(probePath);
|
|
129
|
+
if (listenerData && listenerData.pollTimer) {
|
|
130
|
+
clearInterval(listenerData.pollTimer);
|
|
131
|
+
listenerData.pollTimer = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Start monitoring run status and stop all polling when run finishes
|
|
137
|
+
*/
|
|
138
|
+
startRunStatusMonitoring() {
|
|
139
|
+
if (this.runStatusTimer) return;
|
|
140
|
+
|
|
141
|
+
const checkStatus = async () => {
|
|
142
|
+
try {
|
|
143
|
+
const url = `${this.apiBase}/projects/${encodeURIComponent(this.projectId)}/runs/${encodeURIComponent(this.runId)}/status`;
|
|
144
|
+
const response = await fetch(url);
|
|
145
|
+
if (!response.ok) return;
|
|
146
|
+
const data = await response.json();
|
|
147
|
+
const status = (data.status || '').toLowerCase();
|
|
148
|
+
|
|
149
|
+
if (['completed', 'failed', 'cancelled'].includes(status)) {
|
|
150
|
+
this.runFinished = true;
|
|
151
|
+
this.stopAll();
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Error checking run status:', error);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
this.runStatusTimer = setInterval(checkStatus, 5000);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stopAll() {
|
|
162
|
+
// Stop all polling
|
|
163
|
+
for (const [probePath, _] of this.listeners) {
|
|
164
|
+
this._stopPolling(probePath);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (this.runStatusTimer) {
|
|
168
|
+
clearInterval(this.runStatusTimer);
|
|
169
|
+
this.runStatusTimer = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.listeners.clear();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class ChartContext {
|
|
177
|
+
constructor(projectId, runId, chartName, containerElement, apiBase) {
|
|
178
|
+
this.projectId = projectId;
|
|
179
|
+
this.runId = runId;
|
|
180
|
+
this.chartName = chartName;
|
|
181
|
+
this.containerElement = containerElement;
|
|
182
|
+
this.apiBase = apiBase;
|
|
183
|
+
this.chartInstance = null; // For Chart.js instances
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Helper to convert metrics data to time series array
|
|
188
|
+
* Handles both dict {step: value} and array formats
|
|
189
|
+
*/
|
|
190
|
+
toSeries(data) {
|
|
191
|
+
if (typeof data === 'object' && !Array.isArray(data) && data !== null) {
|
|
192
|
+
// Dict format: {0: val, 1: val, ...}
|
|
193
|
+
const items = Object.entries(data).sort((a, b) => {
|
|
194
|
+
const aNum = parseInt(a[0]);
|
|
195
|
+
const bNum = parseInt(b[0]);
|
|
196
|
+
return (isNaN(aNum) ? 0 : aNum) - (isNaN(bNum) ? 0 : bNum);
|
|
197
|
+
});
|
|
198
|
+
return items.map(([_, v]) => parseFloat(v));
|
|
199
|
+
} else if (Array.isArray(data)) {
|
|
200
|
+
return data.map(v => parseFloat(v));
|
|
201
|
+
}
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get the latest scalar value from metrics data
|
|
207
|
+
* Useful for static metrics or getting the final value
|
|
208
|
+
*/
|
|
209
|
+
getValue(data) {
|
|
210
|
+
if (typeof data === 'number') {
|
|
211
|
+
return data;
|
|
212
|
+
} else if (typeof data === 'object' && !Array.isArray(data) && data !== null) {
|
|
213
|
+
const items = Object.entries(data).sort((a, b) => {
|
|
214
|
+
const aNum = parseInt(a[0]);
|
|
215
|
+
const bNum = parseInt(b[0]);
|
|
216
|
+
return (isNaN(aNum) ? 0 : aNum) - (isNaN(bNum) ? 0 : bNum);
|
|
217
|
+
});
|
|
218
|
+
return items.length > 0 ? parseFloat(items[items.length - 1][1]) : null;
|
|
219
|
+
} else if (Array.isArray(data) && data.length > 0) {
|
|
220
|
+
return parseFloat(data[data.length - 1]);
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Clear the chart container
|
|
227
|
+
*/
|
|
228
|
+
clear() {
|
|
229
|
+
if (this.chartInstance && typeof this.chartInstance.destroy === 'function') {
|
|
230
|
+
this.chartInstance.destroy();
|
|
231
|
+
this.chartInstance = null;
|
|
232
|
+
}
|
|
233
|
+
this.containerElement.innerHTML = '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Set the chart instance (for Chart.js charts)
|
|
238
|
+
*/
|
|
239
|
+
setChartInstance(chart) {
|
|
240
|
+
if (this.chartInstance && typeof this.chartInstance.destroy === 'function') {
|
|
241
|
+
this.chartInstance.destroy();
|
|
242
|
+
}
|
|
243
|
+
this.chartInstance = chart;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ==================== User-Facing Registry ====================
|
|
248
|
+
|
|
249
|
+
const CHART_REGISTRY = new Map();
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Decorator-style function to register a chart
|
|
253
|
+
*
|
|
254
|
+
* Usage:
|
|
255
|
+
* chart('my_chart_name', (probePaths, ctx, listener) => {
|
|
256
|
+
* // Your chart logic here
|
|
257
|
+
* });
|
|
258
|
+
*
|
|
259
|
+
* @param {string} name - Chart name (must match config)
|
|
260
|
+
* @param {Function} renderFunction - Function with signature: (probePaths, ctx, listener) => void
|
|
261
|
+
*/
|
|
262
|
+
export function chart(name, renderFunction) {
|
|
263
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
264
|
+
throw new Error('Chart name must be a non-empty string');
|
|
265
|
+
}
|
|
266
|
+
if (typeof renderFunction !== 'function') {
|
|
267
|
+
throw new Error('Chart render function must be a function');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
CHART_REGISTRY.set(name, renderFunction);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get a registered chart by name
|
|
275
|
+
* @param {string} name
|
|
276
|
+
* @returns {Function|null}
|
|
277
|
+
*/
|
|
278
|
+
export function getChart(name) {
|
|
279
|
+
return CHART_REGISTRY.get(name) || null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* List all registered chart names
|
|
284
|
+
* @returns {string[]}
|
|
285
|
+
*/
|
|
286
|
+
export function listCharts() {
|
|
287
|
+
return Array.from(CHART_REGISTRY.keys());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ==================== Chart Renderer ====================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Render a dynamic chart in a container
|
|
294
|
+
*
|
|
295
|
+
* @param {string} projectId
|
|
296
|
+
* @param {string} runId
|
|
297
|
+
* @param {string} chartName
|
|
298
|
+
* @param {Object} chartConfig - Chart config from backend (includes probe_paths)
|
|
299
|
+
* @param {HTMLElement} containerElement
|
|
300
|
+
* @param {string} apiBase - API base URL
|
|
301
|
+
* @returns {Function} Cleanup function
|
|
302
|
+
*/
|
|
303
|
+
export function renderDynamicChart(projectId, runId, chartName, chartConfig, containerElement, apiBase = '/api') {
|
|
304
|
+
const renderFunction = CHART_REGISTRY.get(chartName);
|
|
305
|
+
if (!renderFunction) {
|
|
306
|
+
containerElement.innerHTML = `<div class="chart-error">Chart '${chartName}' not found. Did you register it?</div>`;
|
|
307
|
+
return () => {};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const ctx = new ChartContext(projectId, runId, chartName, containerElement, apiBase);
|
|
311
|
+
const listener = new MetricsListener(projectId, runId, apiBase);
|
|
312
|
+
|
|
313
|
+
// Start run status monitoring for auto-shutdown
|
|
314
|
+
listener.startRunStatusMonitoring();
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
// Extract probe_paths from config
|
|
318
|
+
const probePaths = chartConfig.probe_paths || {};
|
|
319
|
+
|
|
320
|
+
// Call user's render function
|
|
321
|
+
renderFunction(probePaths, ctx, listener);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(`Error rendering chart '${chartName}':`, error);
|
|
324
|
+
containerElement.innerHTML = `<div class="chart-error">Error: ${error.message}</div>`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Return cleanup function
|
|
328
|
+
return () => {
|
|
329
|
+
ctx.clear();
|
|
330
|
+
listener.stopAll();
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Render a static chart (just display the image)
|
|
336
|
+
*
|
|
337
|
+
* @param {string} imageUrl - URL to the chart image
|
|
338
|
+
* @param {HTMLElement} containerElement
|
|
339
|
+
*/
|
|
340
|
+
export function renderStaticChart(imageUrl, containerElement) {
|
|
341
|
+
containerElement.innerHTML = '';
|
|
342
|
+
const img = document.createElement('img');
|
|
343
|
+
img.src = imageUrl;
|
|
344
|
+
img.alt = 'Chart';
|
|
345
|
+
img.style.maxWidth = '100%';
|
|
346
|
+
img.style.height = 'auto';
|
|
347
|
+
containerElement.appendChild(img);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ==================== Exports ====================
|
|
351
|
+
|
|
352
|
+
export {
|
|
353
|
+
MetricsListener,
|
|
354
|
+
ChartContext,
|
|
355
|
+
CHART_REGISTRY
|
|
356
|
+
};
|
|
357
|
+
|