syd 1.1.0__py3-none-any.whl → 1.2.1__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.
syd/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
- __version__ = "1.1.0"
1
+ __version__ = "1.2.1"
2
2
 
3
3
  from .viewer import make_viewer, Viewer
4
+ from .support import show_open_servers, close_servers
@@ -6,7 +6,6 @@ from dataclasses import dataclass
6
6
  import matplotlib as mpl
7
7
  import matplotlib.pyplot as plt
8
8
  import io
9
- import time
10
9
  import webbrowser
11
10
  import threading
12
11
  import socket
@@ -20,7 +19,7 @@ from flask import (
20
19
  jsonify,
21
20
  render_template,
22
21
  )
23
- from werkzeug.serving import run_simple
22
+ from werkzeug.serving import make_server
24
23
 
25
24
  # Use Deployer base class
26
25
  from ..viewer import Viewer
@@ -43,6 +42,55 @@ from ..support import ParameterUpdateWarning, plot_context
43
42
  mpl.use("Agg")
44
43
 
45
44
 
45
+ class ServerManager:
46
+ def __init__(self):
47
+ self.servers: dict[int, "ServerThread"] = {}
48
+
49
+ def register_server(self, server: "ServerThread", port: int):
50
+ self.servers[port] = server
51
+
52
+ def close_app(self, port: int | None = None):
53
+ if port is None:
54
+ for server in self.servers.values():
55
+ server.shutdown()
56
+ self.servers.clear()
57
+ else:
58
+ if port in self.servers:
59
+ self.servers[port].shutdown()
60
+ del self.servers[port]
61
+
62
+
63
+ server_manager = ServerManager()
64
+
65
+
66
+ class ServerThread(threading.Thread):
67
+ def __init__(self, host: str, port: int, app, debug: bool):
68
+ super().__init__(daemon=True)
69
+ self.server = make_server(host, port, app, threaded=True)
70
+ self.port = port
71
+ self.debug = debug
72
+ self.ready = threading.Event()
73
+
74
+ num_open_servers = len(server_manager.servers)
75
+ if num_open_servers >= 10:
76
+ open_servers = "\n".join([f"{port}" for port in server_manager.servers])
77
+ print(
78
+ f"\nYou have {num_open_servers} open servers!\n"
79
+ f"Open servers:\n{open_servers}\n"
80
+ "You can close them with syd.close_servers() or a particular one with syd.close_servers(port).\n"
81
+ "To see a list, use: syd.show_open_servers()."
82
+ )
83
+
84
+ def run(self):
85
+ server_manager.register_server(self, self.port)
86
+ self.ready.set()
87
+ self.server.serve_forever()
88
+
89
+ def shutdown(self):
90
+ # Call this to stop the server cleanly
91
+ self.server.shutdown()
92
+
93
+
46
94
  @dataclass
47
95
  class FlaskLayoutConfig:
48
96
  """Configuration for the Flask viewer layout."""
@@ -80,6 +128,7 @@ class FlaskDeployer:
80
128
  port: Optional[int] = None,
81
129
  open_browser: bool = True,
82
130
  update_threshold: float = 1.0,
131
+ timeout_threshold: float = 10.0,
83
132
  ):
84
133
  """
85
134
  Initialize the Flask deployer.
@@ -110,11 +159,14 @@ class FlaskDeployer:
110
159
  Whether to open the web application in a browser tab (default: True).
111
160
  update_threshold : float, optional
112
161
  Time in seconds to wait before showing the loading indicator (default: 1.0)
162
+ timeout_threshold : float, optional
163
+ Time in seconds to wait for the browser to open (default: 10.0).
113
164
  """
114
165
  self.viewer = viewer
115
166
  self.suppress_warnings = suppress_warnings
116
167
  self._updating = False # Flag to check circular updates
117
168
  self.update_threshold = update_threshold # Store update threshold
169
+ self.timeout_threshold = timeout_threshold # Store timeout threshold
118
170
 
119
171
  # Flask specific configurations
120
172
  self.config = FlaskLayoutConfig(
@@ -324,7 +376,6 @@ class FlaskDeployer:
324
376
  host: str = "127.0.0.1",
325
377
  port: Optional[int] = None,
326
378
  open_browser: bool = True,
327
- **kwargs,
328
379
  ) -> None:
329
380
  """Starts the Flask development server."""
330
381
  if not self.app:
@@ -338,26 +389,59 @@ class FlaskDeployer:
338
389
  self.url = f"http://{self.host}:{self.port}"
339
390
  print(f" * Syd Flask server running on {self.url}")
340
391
 
341
- if open_browser:
342
-
343
- def open_browser_tab():
344
- time.sleep(1.0)
345
- webbrowser.open(self.url)
346
-
347
- threading.Thread(target=open_browser_tab, daemon=True).start()
348
-
349
- # Run the Flask server using Werkzeug's run_simple
350
- # Pass debug status to run_simple for auto-reloading
351
- run_simple(
352
- self.host,
353
- self.port,
354
- self.app,
355
- use_reloader=self.debug,
356
- use_debugger=self.debug,
357
- **kwargs,
358
- )
392
+ # if open_browser:
393
+
394
+ # def wait_until_responsive(url, timeout=self.timeout_threshold):
395
+ # start_time = time.time()
396
+ # while time.time() - start_time < timeout:
397
+ # try:
398
+ # r = requests.get(url, timeout=0.5)
399
+ # if r.status_code == 200:
400
+ # return True
401
+ # except requests.exceptions.RequestException:
402
+ # pass
403
+ # time.sleep(0.1)
404
+ # return False
405
+
406
+ # def open_browser_tab_when_ready():
407
+ # if wait_until_responsive(self.url):
408
+ # out = webbrowser.open(self.url, new=1, autoraise=True)
409
+ # else:
410
+ # print(
411
+ # f"Could not open browser: server at {self.url} not responding."
412
+ # f"Increase the timeout_threshold to fix this! It's set to {self.timeout_threshold} seconds."
413
+ # "You can do this from viewer.show(timeout_threshold=...) or in the FlaskDeployer constructor."
414
+ # "Also, this is unexpected so please report this issue on GitHub."
415
+ # )
416
+
417
+ # threading.Thread(target=open_browser_tab_when_ready, daemon=True).start()
418
+
419
+ # # Run the Flask server using Werkzeug's run_simple
420
+ # # Pass debug status to run_simple for auto-reloading
421
+ # run_simple(
422
+ # self.host,
423
+ # self.port,
424
+ # self.app,
425
+ # use_reloader=False,
426
+ # use_debugger=self.debug,
427
+ # )
428
+
429
+ # 1) Spin up the server thread
430
+ srv_thread = ServerThread(self.host, self.port, self.app, debug=self.debug)
431
+ srv_thread.start()
432
+
433
+ # 2) Wait for the socket‐bind event (not for an HTTP 200)
434
+ if not srv_thread.ready.wait(timeout=self.timeout_threshold):
435
+ print(
436
+ f"[!] Server did not bind within {self.timeout_threshold:.1f}s; it may already be in use."
437
+ )
438
+ else:
439
+ # 3) Now we know the app is truly listening; open a focused window
440
+ if open_browser:
441
+ webbrowser.open(self.url, new=1, autoraise=True)
359
442
 
360
- # --- Overridden Methods ---
443
+ # 4) Keep the thread handle around so you can call srv_thread.shutdown()
444
+ self._server_thread = srv_thread
361
445
 
362
446
  def deploy(self) -> None:
363
447
  """
@@ -607,7 +691,7 @@ class FlaskDeployer:
607
691
  )
608
692
 
609
693
 
610
- def _find_available_port(start_port=5000, max_attempts=100):
694
+ def _find_available_port(start_port=5000, max_attempts=1000):
611
695
  """
612
696
  Find an available port starting from start_port.
613
697
  (Identical to original)
@@ -92,33 +92,8 @@ input[type="number"] {
92
92
  box-sizing: border-box;
93
93
  }
94
94
 
95
- /* Range inputs */
96
95
  input[type="range"] {
97
96
  width: 100%;
98
- height: 12px;
99
- background: #ddd;
100
- border-radius: 3px;
101
- outline: none;
102
- margin: 5px 0;
103
- }
104
-
105
- input[type="range"]::-webkit-slider-thumb {
106
- -webkit-appearance: none;
107
- width: 18px;
108
- height: 18px;
109
- border-radius: 50%;
110
- background: #3f51b5;
111
- cursor: pointer;
112
- border: 1px solid #2c3e90;
113
- }
114
-
115
- input[type="range"]::-moz-range-thumb {
116
- width: 18px;
117
- height: 18px;
118
- border-radius: 50%;
119
- background: #3f51b5;
120
- cursor: pointer;
121
- border: 1px solid #2c3e90;
122
97
  }
123
98
 
124
99
  /* Checkbox styling */
@@ -184,6 +159,49 @@ button.active {
184
159
  font-style: italic;
185
160
  }
186
161
 
162
+ /* Style all numeric controls consistently */
163
+ .numeric-control {
164
+ display: flex;
165
+ align-items: center;
166
+ }
167
+
168
+ .numeric-control input[type="range"] {
169
+ flex: 1;
170
+ -webkit-appearance: none;
171
+ appearance: none;
172
+ height: 6px;
173
+ background: #ddd;
174
+ outline: none;
175
+ border-radius: 3px;
176
+ }
177
+
178
+ .numeric-control input[type="range"]::-webkit-slider-thumb {
179
+ -webkit-appearance: none;
180
+ appearance: none;
181
+ width: 16px;
182
+ height: 16px;
183
+ background: #4a90e2;
184
+ cursor: pointer;
185
+ border-radius: 50%;
186
+ }
187
+
188
+ .numeric-control input[type="range"]::-moz-range-thumb {
189
+ width: 16px;
190
+ height: 16px;
191
+ background: #4a90e2;
192
+ cursor: pointer;
193
+ border-radius: 50%;
194
+ border: none;
195
+ }
196
+
197
+ .numeric-control input[type="number"] {
198
+ width: 60px;
199
+ padding: 4px 1px;
200
+ border: 1px solid #ddd;
201
+ border-radius: 1px;
202
+ margin-left: 6px;
203
+ }
204
+
187
205
  /* Range slider styles */
188
206
  .range-container {
189
207
  display: flex;
@@ -204,79 +222,76 @@ button.active {
204
222
 
205
223
  .range-slider-container {
206
224
  position: relative;
207
- margin: 5px 0;
208
- background: linear-gradient(to right,
225
+ margin: 10px 0 15px 0;
226
+ background: linear-gradient(
227
+ to right,
209
228
  #ddd 0%,
210
229
  #ddd var(--min-pos, 0%),
211
- #3f51b5 var(--min-pos, 0%),
212
- #3f51b5 var(--max-pos, 100%),
230
+ #4a90e2 var(--min-pos, 0%),
231
+ #4a90e2 var(--max-pos, 100%),
213
232
  #ddd var(--max-pos, 100%),
214
233
  #ddd 100%);
215
- border-radius: 3px;
216
- height: 18px;
234
+ border-radius: 4px;
235
+ height: 6px;
236
+ width: 100%;
217
237
  }
218
238
 
219
239
  .range-slider {
220
240
  position: absolute;
221
- top: 50%;
222
- transform: translateY(-50%);
241
+ top: 0;
223
242
  left: 0;
224
243
  width: 100%;
225
- pointer-events: none;
226
- -webkit-appearance: none;
244
+ height: 100%;
227
245
  appearance: none;
228
- background: transparent;
246
+ -webkit-appearance: none;
229
247
  cursor: pointer;
248
+ background: none;
230
249
  margin: 0;
231
- height: 18px;
250
+ padding: 0;
251
+ pointer-events: none;
232
252
  }
233
253
 
234
254
  /* Transparent Track for Webkit */
235
- .range-slider::-webkit-slider-runnable-track {
255
+ .range-slider-container .range-slider::-webkit-slider-runnable-track {
236
256
  background: transparent;
237
- border: none;
238
- border-radius: 3px;
257
+ border-radius: 2px;
258
+ height: 8px;
239
259
  }
240
260
 
241
261
  /* Transparent Track for Firefox */
242
- .range-slider::-moz-range-track {
262
+ .range-slider-container .range-slider::-moz-range-track {
243
263
  background: transparent;
244
- border: none;
245
- border-radius: 3px;
264
+ border-radius: 2px;
265
+ height: 8px;
246
266
  }
247
267
 
248
- .range-slider.active {
249
- z-index: 2;
250
- }
251
-
252
- .range-slider::-webkit-slider-thumb {
268
+ .range-slider-container .range-slider::-webkit-slider-thumb {
253
269
  pointer-events: auto;
254
270
  -webkit-appearance: none;
255
- appearance: none;
256
- width: 18px;
257
- height: 18px;
258
- border-radius: 50%;
259
- background: #3f51b5;
271
+ width: 16px;
272
+ height: 16px;
273
+ background: #4a90e2;
260
274
  cursor: pointer;
261
- border: 1px solid #2c3e90;
275
+ border-radius: 50%;
276
+ margin-top: -4px; /* center on track */
262
277
  }
263
278
 
264
- .range-slider::-moz-range-thumb {
279
+ .range-slider-container .range-slider::-moz-range-thumb {
265
280
  pointer-events: auto;
266
- width: 18px;
267
- height: 18px;
268
- border-radius: 50%;
269
- background: #3f51b5;
281
+ width: 16px;
282
+ height: 16px;
283
+ background: #4a90e2;
270
284
  cursor: pointer;
271
- border: 1px solid #2c3e90;
285
+ border-radius: 50%;
286
+ margin-top: -4px; /* center on track */
272
287
  }
273
288
 
274
289
  .min-slider {
275
- z-index: 1;
290
+ z-index: 5;
276
291
  }
277
292
 
278
293
  .max-slider {
279
- z-index: 2;
294
+ z-index: 5;
280
295
  }
281
296
 
282
297
  #status-display {
@@ -45,46 +45,4 @@
45
45
  .section-header {
46
46
  margin-bottom: 15px;
47
47
  font-size: 16px;
48
- }
49
-
50
- /* Style all numeric controls consistently */
51
- .numeric-control {
52
- display: flex;
53
- align-items: center;
54
- }
55
-
56
- .numeric-control input[type="range"] {
57
- flex: 1;
58
- -webkit-appearance: none;
59
- appearance: none;
60
- height: 6px;
61
- background: #ddd;
62
- outline: none;
63
- border-radius: 3px;
64
- }
65
-
66
- .numeric-control input[type="range"]::-webkit-slider-thumb {
67
- -webkit-appearance: none;
68
- appearance: none;
69
- width: 16px;
70
- height: 16px;
71
- background: #4a90e2;
72
- cursor: pointer;
73
- border-radius: 50%;
74
- }
75
-
76
- .numeric-control input[type="range"]::-moz-range-thumb {
77
- width: 16px;
78
- height: 16px;
79
- background: #4a90e2;
80
- cursor: pointer;
81
- border-radius: 50%;
82
- border: none;
83
- }
84
-
85
- .numeric-control input[type="number"] {
86
- width: 60px;
87
- padding: 1px 1px;
88
- border: 1px solid #ddd;
89
- border-radius: 1px;
90
48
  }
@@ -0,0 +1,89 @@
1
+ import { updateStatus } from './utils.js';
2
+ import { initializeState, updateStateFromServer } from './state.js';
3
+ import { updatePlot } from './plot.js';
4
+ import { setUpdateThreshold } from './config.js';
5
+
6
+ /**
7
+ * Fetch initial parameter information from the server.
8
+ * Initializes the state and gets configuration.
9
+ */
10
+ export async function fetchInitialData() {
11
+ try {
12
+ const response = await fetch('/init-data');
13
+ if (!response.ok) {
14
+ throw new Error(`HTTP error! status: ${response.status}`);
15
+ }
16
+ const data = await response.json();
17
+
18
+ setUpdateThreshold(data.config.update_threshold); // Set initial threshold
19
+ initializeState(data); // Initialize state
20
+
21
+ return data; // Return data in case the caller needs it
22
+ } catch (error) {
23
+ console.error('Error initializing viewer:', error);
24
+ updateStatus('Error initializing viewer');
25
+ throw error; // Re-throw the error to signal failure
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Send parameter update to the server.
31
+ * @param {string} name - The name of the parameter.
32
+ * @param {*} value - The new value of the parameter.
33
+ * @param {boolean} [action=false] - Whether this is a button action.
34
+ */
35
+ export async function updateParameterOnServer(name, value, action = false) {
36
+ try {
37
+ const response = await fetch('/update-param', {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ body: JSON.stringify({
43
+ name: name,
44
+ value: value,
45
+ action: action
46
+ }),
47
+ });
48
+ if (!response.ok) {
49
+ throw new Error(`HTTP error! status: ${response.status}`);
50
+ }
51
+ return await response.json(); // Return the server response (likely includes updated state)
52
+ } catch (error) {
53
+ console.error('Error updating parameter:', error);
54
+ updateStatus('Error updating parameter');
55
+ throw error; // Re-throw error
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Handles button click actions by sending to the server.
61
+ * @param {string} name - The name of the button parameter.
62
+ */
63
+ export function handleButtonClick(name) {
64
+ const button = document.getElementById(`${name}-button`);
65
+ if (!button) return;
66
+
67
+ button.classList.add('active'); // Show button as active
68
+ updateStatus(`Processing ${name}...`);
69
+
70
+ updateParameterOnServer(name, null, true) // Send action=true
71
+ .then(data => {
72
+ button.classList.remove('active');
73
+ if (data.error) {
74
+ console.error('Error:', data.error);
75
+ updateStatus(`Error processing ${name}`);
76
+ } else {
77
+ // Update state with any changes from callbacks
78
+ updateStateFromServer(data.state, data.params);
79
+ // Update plot if needed (plot.js handles this now)
80
+ updatePlot();
81
+ updateStatus('Ready!');
82
+ }
83
+ })
84
+ .catch(error => {
85
+ button.classList.remove('active');
86
+ console.error('Error during button action:', error);
87
+ updateStatus(`Error processing ${name}`);
88
+ });
89
+ }
@@ -0,0 +1,22 @@
1
+ export let updateThreshold = 1.0; // Default update threshold
2
+
3
+ // Config object parsed from HTML data attributes
4
+ export const config = {
5
+ controlsPosition: document.getElementById('viewer-config')?.dataset.controlsPosition || 'left',
6
+ controlsWidthPercent: parseInt(document.getElementById('viewer-config')?.dataset.controlsWidthPercent || 20),
7
+ plotMarginPercent: parseInt(document.getElementById('viewer-config')?.dataset.plotMarginPercent || 15)
8
+ };
9
+
10
+ // Function to update threshold, needed by system controls
11
+ export function setUpdateThreshold(value) {
12
+ updateThreshold = value;
13
+ }
14
+
15
+ // Function to update config values, needed by system controls
16
+ export function setConfigValue(key, value) {
17
+ if (key in config) {
18
+ config[key] = value;
19
+ } else {
20
+ console.warn(`Attempted to set unknown config key: ${key}`);
21
+ }
22
+ }
@@ -0,0 +1,75 @@
1
+ import { state } from './state.js';
2
+ import { updateThreshold } from './config.js';
3
+ import { updateStatus, createSlowLoadingImage } from './utils.js';
4
+
5
+ let loadingTimeout = null; // Timeout for showing loading state
6
+
7
+ /**
8
+ * Update the plot with current state
9
+ */
10
+ export function updatePlot() {
11
+ const plotImage = document.getElementById('plot-image');
12
+ if (!plotImage) {
13
+ console.warn("Plot image element not found");
14
+ return;
15
+ }
16
+
17
+ // Clear any existing loading timeout
18
+ if (loadingTimeout) {
19
+ clearTimeout(loadingTimeout);
20
+ loadingTimeout = null; // Reset timeout variable
21
+ }
22
+
23
+ // Show loading state after threshold
24
+ loadingTimeout = setTimeout(() => {
25
+ const slowLoadingDataURL = createSlowLoadingImage(); // Get cached or create new
26
+ plotImage.src = slowLoadingDataURL;
27
+ plotImage.style.opacity = '0.5';
28
+ updateStatus('Generating plot...'); // Update status during loading
29
+ }, updateThreshold * 1000);
30
+
31
+ // Build query string from state
32
+ const queryParams = new URLSearchParams();
33
+ for (const [name, value] of Object.entries(state)) {
34
+ if (Array.isArray(value) || typeof value === 'object') {
35
+ // Ensure complex objects/arrays are properly stringified for URL
36
+ queryParams.append(name, JSON.stringify(value));
37
+ } else {
38
+ queryParams.append(name, value);
39
+ }
40
+ }
41
+
42
+ // Set the image source to the plot endpoint with parameters
43
+ const url = `/plot?${queryParams.toString()}`;
44
+
45
+ // Use an Image object to preload and handle load/error events
46
+ const newImage = new Image();
47
+
48
+ newImage.onload = function() {
49
+ // Clear loading timeout if it hasn't fired yet
50
+ if (loadingTimeout) {
51
+ clearTimeout(loadingTimeout);
52
+ loadingTimeout = null;
53
+ }
54
+ // Update the actual image source and reset opacity
55
+ plotImage.src = url;
56
+ plotImage.style.opacity = 1;
57
+ // Don't necessarily set status to Ready here, as state updates might happen
58
+ // Let the calling function (updateParameter or initial load) handle final status
59
+ };
60
+
61
+ newImage.onerror = function() {
62
+ // Clear loading timeout
63
+ if (loadingTimeout) {
64
+ clearTimeout(loadingTimeout);
65
+ loadingTimeout = null;
66
+ }
67
+ updateStatus('Error loading plot');
68
+ plotImage.style.opacity = 1; // Reset opacity even on error
69
+ // Optionally display an error image/message
70
+ // plotImage.src = 'path/to/error/image.png';
71
+ };
72
+
73
+ // Start loading the new image
74
+ newImage.src = url;
75
+ }