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.
Files changed (86) hide show
  1. expops-0.1.3.dist-info/METADATA +826 -0
  2. expops-0.1.3.dist-info/RECORD +86 -0
  3. expops-0.1.3.dist-info/WHEEL +5 -0
  4. expops-0.1.3.dist-info/entry_points.txt +3 -0
  5. expops-0.1.3.dist-info/licenses/LICENSE +674 -0
  6. expops-0.1.3.dist-info/top_level.txt +1 -0
  7. mlops/__init__.py +0 -0
  8. mlops/__main__.py +11 -0
  9. mlops/_version.py +34 -0
  10. mlops/adapters/__init__.py +12 -0
  11. mlops/adapters/base.py +86 -0
  12. mlops/adapters/config_schema.py +89 -0
  13. mlops/adapters/custom/__init__.py +3 -0
  14. mlops/adapters/custom/custom_adapter.py +447 -0
  15. mlops/adapters/plugin_manager.py +113 -0
  16. mlops/adapters/sklearn/__init__.py +3 -0
  17. mlops/adapters/sklearn/adapter.py +94 -0
  18. mlops/cluster/__init__.py +3 -0
  19. mlops/cluster/controller.py +496 -0
  20. mlops/cluster/process_runner.py +91 -0
  21. mlops/cluster/providers.py +258 -0
  22. mlops/core/__init__.py +95 -0
  23. mlops/core/custom_model_base.py +38 -0
  24. mlops/core/dask_networkx_executor.py +1265 -0
  25. mlops/core/executor_worker.py +1239 -0
  26. mlops/core/experiment_tracker.py +81 -0
  27. mlops/core/graph_types.py +64 -0
  28. mlops/core/networkx_parser.py +135 -0
  29. mlops/core/payload_spill.py +278 -0
  30. mlops/core/pipeline_utils.py +162 -0
  31. mlops/core/process_hashing.py +216 -0
  32. mlops/core/step_state_manager.py +1298 -0
  33. mlops/core/step_system.py +956 -0
  34. mlops/core/workspace.py +99 -0
  35. mlops/environment/__init__.py +10 -0
  36. mlops/environment/base.py +43 -0
  37. mlops/environment/conda_manager.py +307 -0
  38. mlops/environment/factory.py +70 -0
  39. mlops/environment/pyenv_manager.py +146 -0
  40. mlops/environment/setup_env.py +31 -0
  41. mlops/environment/system_manager.py +66 -0
  42. mlops/environment/utils.py +105 -0
  43. mlops/environment/venv_manager.py +134 -0
  44. mlops/main.py +527 -0
  45. mlops/managers/project_manager.py +400 -0
  46. mlops/managers/reproducibility_manager.py +575 -0
  47. mlops/platform.py +996 -0
  48. mlops/reporting/__init__.py +16 -0
  49. mlops/reporting/context.py +187 -0
  50. mlops/reporting/entrypoint.py +292 -0
  51. mlops/reporting/kv_utils.py +77 -0
  52. mlops/reporting/registry.py +50 -0
  53. mlops/runtime/__init__.py +9 -0
  54. mlops/runtime/context.py +34 -0
  55. mlops/runtime/env_export.py +113 -0
  56. mlops/storage/__init__.py +12 -0
  57. mlops/storage/adapters/__init__.py +9 -0
  58. mlops/storage/adapters/gcp_kv_store.py +778 -0
  59. mlops/storage/adapters/gcs_object_store.py +96 -0
  60. mlops/storage/adapters/memory_store.py +240 -0
  61. mlops/storage/adapters/redis_store.py +438 -0
  62. mlops/storage/factory.py +199 -0
  63. mlops/storage/interfaces/__init__.py +6 -0
  64. mlops/storage/interfaces/kv_store.py +118 -0
  65. mlops/storage/path_utils.py +38 -0
  66. mlops/templates/premier-league/charts/plot_metrics.js +70 -0
  67. mlops/templates/premier-league/charts/plot_metrics.py +145 -0
  68. mlops/templates/premier-league/charts/requirements.txt +6 -0
  69. mlops/templates/premier-league/configs/cluster_config.yaml +13 -0
  70. mlops/templates/premier-league/configs/project_config.yaml +207 -0
  71. mlops/templates/premier-league/data/England CSV.csv +12154 -0
  72. mlops/templates/premier-league/models/premier_league_model.py +638 -0
  73. mlops/templates/premier-league/requirements.txt +8 -0
  74. mlops/templates/sklearn-basic/README.md +22 -0
  75. mlops/templates/sklearn-basic/charts/plot_metrics.py +85 -0
  76. mlops/templates/sklearn-basic/charts/requirements.txt +3 -0
  77. mlops/templates/sklearn-basic/configs/project_config.yaml +64 -0
  78. mlops/templates/sklearn-basic/data/train.csv +14 -0
  79. mlops/templates/sklearn-basic/models/model.py +62 -0
  80. mlops/templates/sklearn-basic/requirements.txt +10 -0
  81. mlops/web/__init__.py +3 -0
  82. mlops/web/server.py +585 -0
  83. mlops/web/ui/index.html +52 -0
  84. mlops/web/ui/mlops-charts.js +357 -0
  85. mlops/web/ui/script.js +1244 -0
  86. 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
+