syd 1.0.2__tar.gz → 1.2.0__tar.gz

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 (30) hide show
  1. {syd-1.0.2 → syd-1.2.0}/PKG-INFO +24 -10
  2. {syd-1.0.2 → syd-1.2.0}/README.md +23 -9
  3. syd-1.2.0/syd/__init__.py +4 -0
  4. {syd-1.0.2 → syd-1.2.0}/syd/flask_deployment/deployer.py +119 -26
  5. {syd-1.0.2 → syd-1.2.0}/syd/flask_deployment/static/css/styles.css +100 -67
  6. syd-1.2.0/syd/flask_deployment/static/css/viewer.css +48 -0
  7. syd-1.2.0/syd/flask_deployment/static/js/modules/api.js +89 -0
  8. syd-1.2.0/syd/flask_deployment/static/js/modules/config.js +22 -0
  9. syd-1.2.0/syd/flask_deployment/static/js/modules/plot.js +75 -0
  10. syd-1.2.0/syd/flask_deployment/static/js/modules/state.js +89 -0
  11. syd-1.2.0/syd/flask_deployment/static/js/modules/system_controls.js +191 -0
  12. syd-1.2.0/syd/flask_deployment/static/js/modules/ui_controls.js +812 -0
  13. syd-1.2.0/syd/flask_deployment/static/js/modules/utils.js +49 -0
  14. syd-1.0.2/syd/flask_deployment/static/js/viewer.js → syd-1.2.0/syd/flask_deployment/static/js/old_viewer.js +394 -30
  15. syd-1.2.0/syd/flask_deployment/static/js/viewer.js +58 -0
  16. {syd-1.0.2 → syd-1.2.0}/syd/flask_deployment/templates/index.html +1 -1
  17. {syd-1.0.2 → syd-1.2.0}/syd/notebook_deployment/deployer.py +1 -3
  18. {syd-1.0.2 → syd-1.2.0}/syd/notebook_deployment/widgets.py +45 -27
  19. {syd-1.0.2 → syd-1.2.0}/syd/support.py +25 -0
  20. {syd-1.0.2 → syd-1.2.0}/syd/viewer.py +35 -4
  21. syd-1.0.2/syd/__init__.py +0 -11
  22. {syd-1.0.2 → syd-1.2.0}/.gitignore +0 -0
  23. {syd-1.0.2 → syd-1.2.0}/LICENSE +0 -0
  24. {syd-1.0.2 → syd-1.2.0}/pyproject.toml +0 -0
  25. {syd-1.0.2 → syd-1.2.0}/syd/flask_deployment/__init__.py +0 -0
  26. {syd-1.0.2 → syd-1.2.0}/syd/flask_deployment/static/__init__.py +0 -0
  27. {syd-1.0.2 → syd-1.2.0}/syd/flask_deployment/templates/__init__.py +0 -0
  28. {syd-1.0.2 → syd-1.2.0}/syd/flask_deployment/testing_principles.md +0 -0
  29. {syd-1.0.2 → syd-1.2.0}/syd/notebook_deployment/__init__.py +0 -0
  30. {syd-1.0.2 → syd-1.2.0}/syd/parameters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syd
3
- Version: 1.0.2
3
+ Version: 1.2.0
4
4
  Summary: A Python package for making GUIs for data science easy.
5
5
  Project-URL: Homepage, https://github.com/landoskape/syd
6
6
  Author-email: Andrew Landau <andrew+tyler+landau+getridofthisanddtheplusses@gmail.com>
@@ -47,7 +47,7 @@ Have you ever wanted to look through all your data really quickly interactively?
47
47
 
48
48
  Syd is a system for creating a data viewing GUI that you can view in a jupyter notebook or in a web browser. And guess what? Since it can open in a web browser, you can even open it on any other computer on your local network! For example, your PI's computer. Gone are the days of single random examples that they make infinitely stubborn conclusions about. Now, you can look at all the examples, quickly and easily, on their computer. And that's why Syd stands for share your data!
49
49
 
50
- Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about what you want to _**plot**_ and which _**parameters**_ you want to be interactive. Syd handles all the behind-the-scenes action required to make an interface. And guess what? That means you get to spend your time _**thinking**_ about your data, rather than writing code to look at it. And that's why Syd stands for Science, Yes! Dayummmm!
50
+ Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about _**what you want to plot**_ and _**which parameters**_ you want to be interactive. Syd handles all the behind-the-scenes action required to make an interface. And guess what? That means you get to _**spend your time thinking**_ about your data, rather than writing code to look at it. And that's why Syd stands for Science, Yes! Dayummmm!
51
51
 
52
52
  ## Installation
53
53
  It's easy, just use pip install. The dependencies are light so it should work in most environments.
@@ -56,7 +56,7 @@ pip install syd
56
56
  ```
57
57
 
58
58
  ## Documentation
59
- The full documentation is available at [shareyourdata.readthedocs.io](https://shareyourdata.readthedocs.io/). It includes a quick start guide, a comprehensive tutorial, and an API reference for the different elements of Syd. If you have any questions or want to suggest improvements to the docs, please let us know on the [github issues page](https://github.com/landoskape/syd/issues)!
59
+ The full documentation is available [here](https://shareyourdata.readthedocs.io/). It includes a quick start guide, a comprehensive tutorial, and an API reference for the different elements of Syd. If you have any questions or want to suggest improvements to the docs, please let us know on the [github issues page](https://github.com/landoskape/syd/issues)!
60
60
 
61
61
  ## Quick Start
62
62
  This is an example of a sine wave viewer which is about as simple as it gets. You can choose which env to use - if you use ``env="notebook"`` then the GUI will deploy as the output of a jupyter cell (this only works in jupyter!). If you use ``env="browser"`` then the GUI will open a page in your default web browser and you can interact with the data there (works in jupyter notebooks and also from python scripts!).
@@ -78,9 +78,8 @@ viewer.add_float("amplitude", value=1.0, min=0.1, max=2.0)
78
78
  viewer.add_float("frequency", value=1.0, min=0.1, max=5.0)
79
79
  viewer.add_selection("color", value="red", options=["red", "blue", "green", "black"])
80
80
 
81
- # env = "browser" # for viewing in a web browser
82
- env = "notebook" # for viewing within a jupyter notebook
83
- viewer.show()
81
+ viewer.show() # for viewing in a jupyter notebook
82
+ # viewer.share() # for viewing in a web browser
84
83
  ```
85
84
 
86
85
  ![Quick Start Viewer](./docs/assets/viewer_screenshots/readme_example_gif.gif)
@@ -202,7 +201,21 @@ This project is licensed under the GNU General Public License v3.0 - see the [LI
202
201
 
203
202
  ## Contributing
204
203
 
205
- Contributions are welcome! Here's how you can help:
204
+ Contributions are welcome! If you have an idea for an improvement or a bug report, please let us know by opening an
205
+ issue on the [github issues page](https://github.com/landoskape/syd/issues). You can also contribute code by submitting
206
+ a pull request. Here are some guidelines for contributing:
207
+
208
+ ### 1. Reporting Bugs
209
+ If you find a bug, (e.g. any error or strange behavior that is not expected), please let us know by opening an issue on the [github issues page](https://github.com/landoskape/syd/issues).
210
+
211
+ ### 2. Suggesting Features
212
+ If you have an idea for a feature or improvement, please let tell us. Opening an issue on the [github issues page](https://github.com/landoskape/syd/issues).
213
+
214
+ ### 3. Improvements to the Documentation
215
+ A package is only as good as its documentation. If you think the documentation is missing something, confusing, or could be improved in any way, please let us know by opening an issue on the [github issues page](https://github.com/landoskape/syd/issues).
216
+
217
+ ### 4. Contributing Code
218
+ If you want to contribute code (good for you!), here's how you can do it:
206
219
 
207
220
  1. Fork the repository
208
221
  2. Create a new branch (`git checkout -b feature/amazing-feature`)
@@ -212,7 +225,7 @@ Contributions are welcome! Here's how you can help:
212
225
  6. Push to the branch (`git push origin feature/amazing-feature`)
213
226
  7. Open a Pull Request online
214
227
 
215
- Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines). Try to make the code coverage report improve or stay the same rather than decrease (right now the deployment system isn't covered by tests). I don't have any precommit hooks or anything so you're responsible for checking this yourself. You can process the code with black as follows:
228
+ Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines). Try to make the code coverage report improve or stay the same rather than decrease (right now the deployment system isn't covered by tests). There aren't any precommit hooks or anything so you're responsible for checking this yourself. You can process the code with black as follows:
216
229
  ```bash
217
230
  pip install black
218
231
  black . # from the root directory of the repo
@@ -220,7 +233,6 @@ black . # from the root directory of the repo
220
233
 
221
234
  ## To-Do List
222
235
  - Layout controls
223
- - [ ] Improve the display and make it look better
224
236
  - [ ] Add a "save" button that saves the current state of the viewer to a json file
225
237
  - [ ] Add a "load" button that loads the viewer state from a json file
226
238
  - [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
@@ -228,4 +240,6 @@ black . # from the root directory of the repo
228
240
  - [ ] Consider "app_deployed" context for each deployer...
229
241
  - Export options:
230
242
  - [ ] Export lite: export the viewer as a HTML/Java package that contains an incomplete set of renderings of figures -- using a certain set of parameters.
231
- - [ ] Export full: export the viewer in a way that contains the data to give full functionality.
243
+ - [ ] Export full: export the viewer in a way that contains the data to give full functionality.
244
+ - [ ] Idea for sharing: https://github.com/analyticalmonk/awesome-neuroscience, https://github.com/fasouto/awesome-dataviz
245
+ - [ ] The handling of value in Selection parameters is kind of weird.... I think we need to think more about what to do for fails!!!!
@@ -17,7 +17,7 @@ Have you ever wanted to look through all your data really quickly interactively?
17
17
 
18
18
  Syd is a system for creating a data viewing GUI that you can view in a jupyter notebook or in a web browser. And guess what? Since it can open in a web browser, you can even open it on any other computer on your local network! For example, your PI's computer. Gone are the days of single random examples that they make infinitely stubborn conclusions about. Now, you can look at all the examples, quickly and easily, on their computer. And that's why Syd stands for share your data!
19
19
 
20
- Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about what you want to _**plot**_ and which _**parameters**_ you want to be interactive. Syd handles all the behind-the-scenes action required to make an interface. And guess what? That means you get to spend your time _**thinking**_ about your data, rather than writing code to look at it. And that's why Syd stands for Science, Yes! Dayummmm!
20
+ Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about _**what you want to plot**_ and _**which parameters**_ you want to be interactive. Syd handles all the behind-the-scenes action required to make an interface. And guess what? That means you get to _**spend your time thinking**_ about your data, rather than writing code to look at it. And that's why Syd stands for Science, Yes! Dayummmm!
21
21
 
22
22
  ## Installation
23
23
  It's easy, just use pip install. The dependencies are light so it should work in most environments.
@@ -26,7 +26,7 @@ pip install syd
26
26
  ```
27
27
 
28
28
  ## Documentation
29
- The full documentation is available at [shareyourdata.readthedocs.io](https://shareyourdata.readthedocs.io/). It includes a quick start guide, a comprehensive tutorial, and an API reference for the different elements of Syd. If you have any questions or want to suggest improvements to the docs, please let us know on the [github issues page](https://github.com/landoskape/syd/issues)!
29
+ The full documentation is available [here](https://shareyourdata.readthedocs.io/). It includes a quick start guide, a comprehensive tutorial, and an API reference for the different elements of Syd. If you have any questions or want to suggest improvements to the docs, please let us know on the [github issues page](https://github.com/landoskape/syd/issues)!
30
30
 
31
31
  ## Quick Start
32
32
  This is an example of a sine wave viewer which is about as simple as it gets. You can choose which env to use - if you use ``env="notebook"`` then the GUI will deploy as the output of a jupyter cell (this only works in jupyter!). If you use ``env="browser"`` then the GUI will open a page in your default web browser and you can interact with the data there (works in jupyter notebooks and also from python scripts!).
@@ -48,9 +48,8 @@ viewer.add_float("amplitude", value=1.0, min=0.1, max=2.0)
48
48
  viewer.add_float("frequency", value=1.0, min=0.1, max=5.0)
49
49
  viewer.add_selection("color", value="red", options=["red", "blue", "green", "black"])
50
50
 
51
- # env = "browser" # for viewing in a web browser
52
- env = "notebook" # for viewing within a jupyter notebook
53
- viewer.show()
51
+ viewer.show() # for viewing in a jupyter notebook
52
+ # viewer.share() # for viewing in a web browser
54
53
  ```
55
54
 
56
55
  ![Quick Start Viewer](./docs/assets/viewer_screenshots/readme_example_gif.gif)
@@ -172,7 +171,21 @@ This project is licensed under the GNU General Public License v3.0 - see the [LI
172
171
 
173
172
  ## Contributing
174
173
 
175
- Contributions are welcome! Here's how you can help:
174
+ Contributions are welcome! If you have an idea for an improvement or a bug report, please let us know by opening an
175
+ issue on the [github issues page](https://github.com/landoskape/syd/issues). You can also contribute code by submitting
176
+ a pull request. Here are some guidelines for contributing:
177
+
178
+ ### 1. Reporting Bugs
179
+ If you find a bug, (e.g. any error or strange behavior that is not expected), please let us know by opening an issue on the [github issues page](https://github.com/landoskape/syd/issues).
180
+
181
+ ### 2. Suggesting Features
182
+ If you have an idea for a feature or improvement, please let tell us. Opening an issue on the [github issues page](https://github.com/landoskape/syd/issues).
183
+
184
+ ### 3. Improvements to the Documentation
185
+ A package is only as good as its documentation. If you think the documentation is missing something, confusing, or could be improved in any way, please let us know by opening an issue on the [github issues page](https://github.com/landoskape/syd/issues).
186
+
187
+ ### 4. Contributing Code
188
+ If you want to contribute code (good for you!), here's how you can do it:
176
189
 
177
190
  1. Fork the repository
178
191
  2. Create a new branch (`git checkout -b feature/amazing-feature`)
@@ -182,7 +195,7 @@ Contributions are welcome! Here's how you can help:
182
195
  6. Push to the branch (`git push origin feature/amazing-feature`)
183
196
  7. Open a Pull Request online
184
197
 
185
- Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines). Try to make the code coverage report improve or stay the same rather than decrease (right now the deployment system isn't covered by tests). I don't have any precommit hooks or anything so you're responsible for checking this yourself. You can process the code with black as follows:
198
+ Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines). Try to make the code coverage report improve or stay the same rather than decrease (right now the deployment system isn't covered by tests). There aren't any precommit hooks or anything so you're responsible for checking this yourself. You can process the code with black as follows:
186
199
  ```bash
187
200
  pip install black
188
201
  black . # from the root directory of the repo
@@ -190,7 +203,6 @@ black . # from the root directory of the repo
190
203
 
191
204
  ## To-Do List
192
205
  - Layout controls
193
- - [ ] Improve the display and make it look better
194
206
  - [ ] Add a "save" button that saves the current state of the viewer to a json file
195
207
  - [ ] Add a "load" button that loads the viewer state from a json file
196
208
  - [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
@@ -198,4 +210,6 @@ black . # from the root directory of the repo
198
210
  - [ ] Consider "app_deployed" context for each deployer...
199
211
  - Export options:
200
212
  - [ ] Export lite: export the viewer as a HTML/Java package that contains an incomplete set of renderings of figures -- using a certain set of parameters.
201
- - [ ] Export full: export the viewer in a way that contains the data to give full functionality.
213
+ - [ ] Export full: export the viewer in a way that contains the data to give full functionality.
214
+ - [ ] Idea for sharing: https://github.com/analyticalmonk/awesome-neuroscience, https://github.com/fasouto/awesome-dataviz
215
+ - [ ] The handling of value in Selection parameters is kind of weird.... I think we need to think more about what to do for fails!!!!
@@ -0,0 +1,4 @@
1
+ __version__ = "1.2.0"
2
+
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,12 +42,61 @@ 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."""
49
97
 
50
98
  controls_position: str = "left" # Options are: 'left', 'top', 'right', 'bottom'
51
- controls_width_percent: int = 30
99
+ controls_width_percent: int = 15
52
100
 
53
101
  def __post_init__(self):
54
102
  valid_positions = ["left", "top", "right", "bottom"]
@@ -73,12 +121,14 @@ class FlaskDeployer:
73
121
  viewer: Viewer,
74
122
  controls_position: str = "left",
75
123
  fig_dpi: int = 300,
76
- controls_width_percent: int = 20,
124
+ controls_width_percent: int = 15,
77
125
  suppress_warnings: bool = True,
78
126
  debug: bool = False,
79
127
  host: str = "127.0.0.1",
80
128
  port: Optional[int] = None,
81
129
  open_browser: bool = True,
130
+ update_threshold: float = 1.0,
131
+ timeout_threshold: float = 10.0,
82
132
  ):
83
133
  """
84
134
  Initialize the Flask deployer.
@@ -107,10 +157,16 @@ class FlaskDeployer:
107
157
  Port for the server. If None, finds an available port (default: None).
108
158
  open_browser : bool, optional
109
159
  Whether to open the web application in a browser tab (default: True).
160
+ update_threshold : float, optional
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).
110
164
  """
111
165
  self.viewer = viewer
112
166
  self.suppress_warnings = suppress_warnings
113
167
  self._updating = False # Flag to check circular updates
168
+ self.update_threshold = update_threshold # Store update threshold
169
+ self.timeout_threshold = timeout_threshold # Store timeout threshold
114
170
 
115
171
  # Flask specific configurations
116
172
  self.config = FlaskLayoutConfig(
@@ -167,12 +223,17 @@ class FlaskDeployer:
167
223
  }
168
224
  # Get the order of parameters
169
225
  param_order = list(self.viewer.parameters.keys())
170
- # Also include the initial state
226
+ # Also include the initial state and configuration
171
227
  return jsonify(
172
228
  {
173
229
  "params": param_info,
174
230
  "param_order": param_order,
175
231
  "state": self.viewer.state,
232
+ "config": {
233
+ "controls_position": self.config.controls_position,
234
+ "controls_width_percent": self.config.controls_width_percent,
235
+ "update_threshold": self.update_threshold,
236
+ },
176
237
  }
177
238
  )
178
239
 
@@ -315,7 +376,6 @@ class FlaskDeployer:
315
376
  host: str = "127.0.0.1",
316
377
  port: Optional[int] = None,
317
378
  open_browser: bool = True,
318
- **kwargs,
319
379
  ) -> None:
320
380
  """Starts the Flask development server."""
321
381
  if not self.app:
@@ -329,26 +389,59 @@ class FlaskDeployer:
329
389
  self.url = f"http://{self.host}:{self.port}"
330
390
  print(f" * Syd Flask server running on {self.url}")
331
391
 
332
- if open_browser:
333
-
334
- def open_browser_tab():
335
- time.sleep(1.0)
336
- webbrowser.open(self.url)
337
-
338
- threading.Thread(target=open_browser_tab, daemon=True).start()
339
-
340
- # Run the Flask server using Werkzeug's run_simple
341
- # Pass debug status to run_simple for auto-reloading
342
- run_simple(
343
- self.host,
344
- self.port,
345
- self.app,
346
- use_reloader=self.debug,
347
- use_debugger=self.debug,
348
- **kwargs,
349
- )
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)
350
442
 
351
- # --- Overridden Methods ---
443
+ # 4) Keep the thread handle around so you can call srv_thread.shutdown()
444
+ self._server_thread = srv_thread
352
445
 
353
446
  def deploy(self) -> None:
354
447
  """
@@ -598,7 +691,7 @@ class FlaskDeployer:
598
691
  )
599
692
 
600
693
 
601
- def _find_available_port(start_port=5000, max_attempts=100):
694
+ def _find_available_port(start_port=5000, max_attempts=1000):
602
695
  """
603
696
  Find an available port starting from start_port.
604
697
  (Identical to original)
@@ -60,14 +60,14 @@ body {
60
60
  #controls-container {
61
61
  display: grid;
62
62
  grid-template-columns: 1fr;
63
- gap: 10px;
63
+ gap: 5px;
64
64
  }
65
65
 
66
66
  /* Control groups */
67
67
  .control-group {
68
68
  display: flex;
69
69
  flex-direction: column;
70
- padding: 10px;
70
+ padding: 7px;
71
71
  border: 1px solid #eee;
72
72
  border-radius: 4px;
73
73
  background-color: white;
@@ -76,7 +76,7 @@ body {
76
76
 
77
77
  .control-label {
78
78
  font-weight: 600;
79
- margin-bottom: 10px;
79
+ margin-bottom: 0px;
80
80
  color: #333;
81
81
  text-transform: capitalize;
82
82
  }
@@ -87,38 +87,13 @@ input[type="number"] {
87
87
  padding: 8px 12px;
88
88
  border: 1px solid #ddd;
89
89
  border-radius: 4px;
90
- font-size: 14px;
90
+ font-size: 12px;
91
91
  width: 100%;
92
92
  box-sizing: border-box;
93
93
  }
94
94
 
95
- /* Range inputs */
96
95
  input[type="range"] {
97
96
  width: 100%;
98
- height: 6px;
99
- background: #ddd;
100
- border-radius: 3px;
101
- outline: none;
102
- margin: 10px 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;
@@ -194,7 +212,7 @@ button.active {
194
212
  .range-inputs {
195
213
  display: flex;
196
214
  justify-content: space-between;
197
- margin-bottom: 10px;
215
+ margin-bottom: 5px;
198
216
  }
199
217
 
200
218
  .range-input {
@@ -204,77 +222,92 @@ button.active {
204
222
 
205
223
  .range-slider-container {
206
224
  position: relative;
207
- margin: 10px 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;
295
+ }
296
+
297
+ #status-display {
298
+ margin-top: 10px;
299
+ margin-bottom: 3px;
300
+ padding: 8px;
301
+ border-radius: 4px;
302
+ background-color: #ffffff;
303
+ border: 1px solid #e5e7eb;
304
+ }
305
+
306
+ .status-message {
307
+ background-color: #e0e0e0;
308
+ color: #000;
309
+ padding: 2px 6px;
310
+ border-radius: 4px;
311
+ font-size: 90%;
312
+ margin-left: 8px;
280
313
  }