experimaestro 1.11.1__py3-none-any.whl → 2.0.0b4__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.

Potentially problematic release.


This version of experimaestro might be problematic. Click here for more details.

Files changed (133) hide show
  1. experimaestro/__init__.py +10 -11
  2. experimaestro/annotations.py +167 -206
  3. experimaestro/cli/__init__.py +140 -16
  4. experimaestro/cli/filter.py +42 -74
  5. experimaestro/cli/jobs.py +157 -106
  6. experimaestro/cli/progress.py +269 -0
  7. experimaestro/cli/refactor.py +249 -0
  8. experimaestro/click.py +0 -1
  9. experimaestro/commandline.py +19 -3
  10. experimaestro/connectors/__init__.py +22 -3
  11. experimaestro/connectors/local.py +12 -0
  12. experimaestro/core/arguments.py +192 -37
  13. experimaestro/core/identifier.py +127 -12
  14. experimaestro/core/objects/__init__.py +6 -0
  15. experimaestro/core/objects/config.py +702 -285
  16. experimaestro/core/objects/config_walk.py +24 -6
  17. experimaestro/core/serialization.py +91 -34
  18. experimaestro/core/serializers.py +1 -8
  19. experimaestro/core/subparameters.py +164 -0
  20. experimaestro/core/types.py +198 -83
  21. experimaestro/exceptions.py +26 -0
  22. experimaestro/experiments/cli.py +107 -25
  23. experimaestro/generators.py +50 -9
  24. experimaestro/huggingface.py +3 -1
  25. experimaestro/launcherfinder/parser.py +29 -0
  26. experimaestro/launcherfinder/registry.py +3 -3
  27. experimaestro/launchers/__init__.py +26 -1
  28. experimaestro/launchers/direct.py +12 -0
  29. experimaestro/launchers/slurm/base.py +154 -2
  30. experimaestro/mkdocs/base.py +6 -8
  31. experimaestro/mkdocs/metaloader.py +0 -1
  32. experimaestro/mypy.py +452 -7
  33. experimaestro/notifications.py +75 -16
  34. experimaestro/progress.py +404 -0
  35. experimaestro/rpyc.py +0 -1
  36. experimaestro/run.py +19 -6
  37. experimaestro/scheduler/__init__.py +18 -1
  38. experimaestro/scheduler/base.py +504 -959
  39. experimaestro/scheduler/dependencies.py +43 -28
  40. experimaestro/scheduler/dynamic_outputs.py +259 -130
  41. experimaestro/scheduler/experiment.py +582 -0
  42. experimaestro/scheduler/interfaces.py +474 -0
  43. experimaestro/scheduler/jobs.py +485 -0
  44. experimaestro/scheduler/services.py +186 -12
  45. experimaestro/scheduler/signal_handler.py +32 -0
  46. experimaestro/scheduler/state.py +1 -1
  47. experimaestro/scheduler/state_db.py +388 -0
  48. experimaestro/scheduler/state_provider.py +2345 -0
  49. experimaestro/scheduler/state_sync.py +834 -0
  50. experimaestro/scheduler/workspace.py +52 -10
  51. experimaestro/scriptbuilder.py +7 -0
  52. experimaestro/server/__init__.py +153 -32
  53. experimaestro/server/data/index.css +0 -125
  54. experimaestro/server/data/index.css.map +1 -1
  55. experimaestro/server/data/index.js +194 -58
  56. experimaestro/server/data/index.js.map +1 -1
  57. experimaestro/settings.py +47 -6
  58. experimaestro/sphinx/__init__.py +3 -3
  59. experimaestro/taskglobals.py +20 -0
  60. experimaestro/tests/conftest.py +80 -0
  61. experimaestro/tests/core/test_generics.py +2 -2
  62. experimaestro/tests/identifier_stability.json +45 -0
  63. experimaestro/tests/launchers/bin/sacct +6 -2
  64. experimaestro/tests/launchers/bin/sbatch +4 -2
  65. experimaestro/tests/launchers/common.py +2 -2
  66. experimaestro/tests/launchers/test_slurm.py +80 -0
  67. experimaestro/tests/restart.py +1 -1
  68. experimaestro/tests/tasks/all.py +7 -0
  69. experimaestro/tests/tasks/test_dynamic.py +231 -0
  70. experimaestro/tests/test_checkers.py +2 -2
  71. experimaestro/tests/test_cli_jobs.py +615 -0
  72. experimaestro/tests/test_dependencies.py +11 -17
  73. experimaestro/tests/test_deprecated.py +630 -0
  74. experimaestro/tests/test_environment.py +200 -0
  75. experimaestro/tests/test_experiment.py +3 -3
  76. experimaestro/tests/test_file_progress.py +425 -0
  77. experimaestro/tests/test_file_progress_integration.py +477 -0
  78. experimaestro/tests/test_forward.py +3 -3
  79. experimaestro/tests/test_generators.py +93 -0
  80. experimaestro/tests/test_identifier.py +520 -169
  81. experimaestro/tests/test_identifier_stability.py +458 -0
  82. experimaestro/tests/test_instance.py +16 -21
  83. experimaestro/tests/test_multitoken.py +442 -0
  84. experimaestro/tests/test_mypy.py +433 -0
  85. experimaestro/tests/test_objects.py +314 -30
  86. experimaestro/tests/test_outputs.py +8 -8
  87. experimaestro/tests/test_param.py +22 -26
  88. experimaestro/tests/test_partial_paths.py +231 -0
  89. experimaestro/tests/test_progress.py +2 -50
  90. experimaestro/tests/test_resumable_task.py +480 -0
  91. experimaestro/tests/test_serializers.py +141 -60
  92. experimaestro/tests/test_state_db.py +434 -0
  93. experimaestro/tests/test_subparameters.py +160 -0
  94. experimaestro/tests/test_tags.py +151 -15
  95. experimaestro/tests/test_tasks.py +137 -160
  96. experimaestro/tests/test_token_locking.py +252 -0
  97. experimaestro/tests/test_tokens.py +25 -19
  98. experimaestro/tests/test_types.py +133 -11
  99. experimaestro/tests/test_validation.py +19 -19
  100. experimaestro/tests/test_workspace_triggers.py +158 -0
  101. experimaestro/tests/token_reschedule.py +5 -3
  102. experimaestro/tests/utils.py +2 -2
  103. experimaestro/tokens.py +154 -57
  104. experimaestro/tools/diff.py +8 -1
  105. experimaestro/tui/__init__.py +8 -0
  106. experimaestro/tui/app.py +2303 -0
  107. experimaestro/tui/app.tcss +353 -0
  108. experimaestro/tui/log_viewer.py +228 -0
  109. experimaestro/typingutils.py +11 -2
  110. experimaestro/utils/__init__.py +23 -0
  111. experimaestro/utils/environment.py +148 -0
  112. experimaestro/utils/git.py +129 -0
  113. experimaestro/utils/resources.py +1 -1
  114. experimaestro/version.py +34 -0
  115. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
  116. experimaestro-2.0.0b4.dist-info/RECORD +181 -0
  117. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
  118. experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
  119. experimaestro/compat.py +0 -6
  120. experimaestro/core/objects.pyi +0 -225
  121. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  122. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  123. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  124. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  125. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  126. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  127. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  128. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  129. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  130. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  131. experimaestro-1.11.1.dist-info/RECORD +0 -158
  132. experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
  133. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
@@ -2,10 +2,14 @@ from collections import ChainMap
2
2
  from enum import Enum
3
3
  from functools import cached_property
4
4
  from pathlib import Path
5
- from typing import Iterator, Optional
5
+ from typing import Optional
6
6
  from experimaestro.settings import WorkspaceSettings, Settings
7
7
 
8
8
 
9
+ # Current workspace version
10
+ WORKSPACE_VERSION = 0
11
+
12
+
9
13
  class RunMode(str, Enum):
10
14
  NORMAL = "normal"
11
15
  """Normal run"""
@@ -18,17 +22,16 @@ class RunMode(str, Enum):
18
22
 
19
23
 
20
24
  class Workspace:
21
- """An experimental workspace
25
+ """Workspace environment for experiments
22
26
 
23
- This workspace is created by an experiment object and is used by launchers
24
- to set up jobs
27
+ This is a simple container for workspace settings, environment, and configuration.
28
+ Multiple Workspace instances can exist for the same path - the singleton pattern
29
+ is handled by WorkspaceStateProvider which manages the database per workspace path.
25
30
  """
26
31
 
27
32
  CURRENT = None
28
33
  settings: "Settings"
29
- worspace: "WorkspaceSettings"
30
-
31
- """Creates a workspace for experiments"""
34
+ workspace_settings: "WorkspaceSettings"
32
35
 
33
36
  def __init__(
34
37
  self,
@@ -37,6 +40,14 @@ class Workspace:
37
40
  launcher=None,
38
41
  run_mode: RunMode = None,
39
42
  ):
43
+ """Initialize workspace environment
44
+
45
+ Args:
46
+ settings: Global settings
47
+ workspace_settings: Workspace-specific settings
48
+ launcher: Default launcher for this workspace
49
+ run_mode: Run mode for experiments in this workspace
50
+ """
40
51
  self.settings = settings
41
52
  self.workspace_settings = workspace_settings
42
53
 
@@ -44,21 +55,47 @@ class Workspace:
44
55
  self.notificationURL: Optional[str] = None
45
56
  if isinstance(path, Path):
46
57
  path = path.absolute()
58
+ else:
59
+ path = Path(path).absolute()
47
60
  self.path = path
48
61
  self.run_mode = run_mode
49
62
  self.python_path = []
63
+
50
64
  from ..launchers import Launcher
51
65
 
52
66
  self.launcher = launcher or Launcher.get(path)
53
67
 
54
68
  self.env = ChainMap({}, workspace_settings.env, settings.env)
55
69
 
70
+ # Reference counting for nested context managers
71
+ self._ref_count = 0
72
+
56
73
  def __enter__(self):
57
- self.old_workspace = Workspace.CURRENT
58
- Workspace.CURRENT = self
74
+ # Increment reference count
75
+ self._ref_count += 1
76
+
77
+ # Only initialize on first entry
78
+ if self._ref_count == 1:
79
+ # Check if a different workspace is already active
80
+ if Workspace.CURRENT is not None and Workspace.CURRENT.path != self.path:
81
+ raise RuntimeError(
82
+ f"Cannot activate workspace at {self.path} - "
83
+ f"workspace at {Workspace.CURRENT.path} is already active. "
84
+ "Multiple workspaces are not yet supported."
85
+ )
86
+
87
+ self.old_workspace = Workspace.CURRENT
88
+ Workspace.CURRENT = self
89
+
90
+ return self
59
91
 
60
92
  def __exit__(self, *args):
61
- Workspace.CURRENT = self.old_workspace
93
+ # Decrement reference count
94
+ self._ref_count -= 1
95
+
96
+ # Only cleanup on last exit
97
+ if self._ref_count == 0:
98
+ Workspace.CURRENT = self.old_workspace
62
99
 
63
100
  @cached_property
64
101
  def alt_workspaces(self):
@@ -79,6 +116,11 @@ class Workspace:
79
116
  """Folder for jobs"""
80
117
  return self.path / "jobs"
81
118
 
119
+ @property
120
+ def partialspath(self):
121
+ """Folder for partial job directories (shared checkpoints, etc.)"""
122
+ return self.path / "partials"
123
+
82
124
  @property
83
125
  def experimentspath(self):
84
126
  """Folder for experiments"""
@@ -126,6 +126,13 @@ class PythonScriptBuilder:
126
126
  for path in job.python_path:
127
127
  out.write(f""" sys.path.insert(0, "{shquote(str(path))}")\n""")
128
128
 
129
+ # Write launcher info code (for remaining_time support)
130
+ launcher_info_code = job.launcher.launcher_info_code()
131
+ if launcher_info_code:
132
+ out.write("\n")
133
+ out.write(launcher_info_code)
134
+ out.write("\n")
135
+
129
136
  out.write(
130
137
  f""" TaskRunner("{shquote(connector.resolve(scriptpath))}","""
131
138
  """ lockfiles).run()\n"""
@@ -5,10 +5,12 @@ import platform
5
5
  import socket
6
6
  import uuid
7
7
  from experimaestro.scheduler.base import Job
8
- import pkg_resources
8
+ import sys
9
9
  import http
10
10
  import threading
11
- from typing import Optional, Tuple
11
+ from typing import Optional, Tuple, ClassVar
12
+
13
+ from importlib.resources import files
12
14
  from experimaestro.scheduler import Scheduler, Listener as BaseListener
13
15
  from experimaestro.scheduler.services import Service, ServiceListener
14
16
  from experimaestro.settings import ServerSettings
@@ -46,6 +48,9 @@ def job_details(job):
46
48
 
47
49
 
48
50
  def job_create(job: Job):
51
+ # Get experiment IDs from job.experiments list
52
+ experiment_ids = [xp.workdir.name for xp in job.experiments]
53
+
49
54
  return {
50
55
  "jobId": job.identifier,
51
56
  "taskId": job.name,
@@ -53,28 +58,50 @@ def job_create(job: Job):
53
58
  "status": job.state.name.lower(),
54
59
  "tags": list(job.tags.items()),
55
60
  "progress": progress_state(job),
61
+ "experimentIds": experiment_ids, # Add experiment IDs
56
62
  }
57
63
 
58
64
 
59
65
  class Listener(BaseListener, ServiceListener):
60
- def __init__(self, scheduler: Scheduler, socketio):
61
- self.scheduler = scheduler
66
+ def __init__(self, socketio, state_provider):
62
67
  self.socketio = socketio
63
- self.scheduler.addlistener(self)
64
- self.services = {}
65
- for service in self.scheduler.xp.services.values():
66
- self.service_add(service)
68
+ self.state_provider = state_provider
69
+
70
+ # Try to get the scheduler (if one is running for active experiments)
71
+ # Otherwise we're in monitoring mode and don't need scheduler events
72
+ try:
73
+ from experimaestro.scheduler import Scheduler
74
+
75
+ # Check if a scheduler instance exists (would be created if experiments are running)
76
+ if Scheduler._instance is not None:
77
+ self.scheduler = Scheduler._instance
78
+ self.scheduler.addlistener(self)
79
+ self.services = {}
80
+ # Initialize services from all registered experiments
81
+ for xp in self.scheduler.experiments.values():
82
+ for service in xp.services.values():
83
+ self.service_add(service)
84
+ else:
85
+ # No scheduler running - monitoring mode
86
+ self.scheduler = None
87
+ self.services = {}
88
+ except Exception:
89
+ # Scheduler not available - monitoring mode
90
+ self.scheduler = None
91
+ self.services = {}
67
92
 
68
93
  def job_submitted(self, job):
69
94
  self.socketio.emit("job.add", job_create(job))
70
95
 
71
96
  def job_state(self, job):
97
+ experiment_ids = [xp.workdir.name for xp in job.experiments]
72
98
  self.socketio.emit(
73
99
  "job.update",
74
100
  {
75
101
  "jobId": job.identifier,
76
102
  "status": job.state.name.lower(),
77
103
  "progress": progress_state(job),
104
+ "experimentIds": experiment_ids,
78
105
  },
79
106
  )
80
107
 
@@ -143,13 +170,14 @@ def proxy_response(base_url: str, request: Request, path: str):
143
170
  return flask_response
144
171
 
145
172
 
173
+ # flake8: noqa: C901
146
174
  def start_app(server: "Server"):
147
175
  logging.debug("Starting Flask server...")
148
176
  app = Flask("experimaestro")
149
177
 
150
178
  logging.debug("Starting Flask server (SocketIO)...")
151
179
  socketio = SocketIO(app, path="/api", async_mode="gevent")
152
- listener = Listener(server.scheduler, socketio)
180
+ listener = Listener(socketio, server.state_provider)
153
181
 
154
182
  logging.debug("Starting Flask server (setting up socketio)...")
155
183
 
@@ -159,13 +187,48 @@ def start_app(server: "Server"):
159
187
  raise ConnectionRefusedError("invalid token")
160
188
 
161
189
  @socketio.on("refresh")
162
- def handle_refresh():
163
- for job in listener.scheduler.jobs.values():
164
- emit("job.add", job_create(job))
190
+ def handle_refresh(experiment_id=None):
191
+ """Refresh jobs for an experiment (or all experiments if None)"""
192
+ if experiment_id:
193
+ # Refresh specific experiment
194
+ jobs = listener.state_provider.get_jobs(experiment_id)
195
+ for job_data in jobs:
196
+ emit("job.add", job_data)
197
+ else:
198
+ # Refresh all experiments
199
+ if listener.scheduler:
200
+ # Active experiments: get jobs from scheduler
201
+ for job in listener.scheduler.jobs.values():
202
+ emit("job.add", job_create(job))
203
+ else:
204
+ # Monitoring mode: get jobs from WorkspaceStateProvider
205
+ for exp in listener.state_provider.get_experiments():
206
+ exp_id = exp["experiment_id"]
207
+ jobs = listener.state_provider.get_jobs(exp_id)
208
+ for job_data in jobs:
209
+ emit("job.add", job_data)
210
+
211
+ @socketio.on("experiments")
212
+ def handle_experiments():
213
+ """List all experiments"""
214
+ experiments = listener.state_provider.get_experiments()
215
+ for exp in experiments:
216
+ emit("experiment.add", exp)
165
217
 
166
218
  @socketio.on("job.details")
167
- def handle_details(jobid):
168
- emit("job.update", job_details(listener.scheduler.jobs[jobid]))
219
+ def handle_details(data):
220
+ """Get job details - expects {experimentId, jobId} or just jobId (backward compat)"""
221
+ # Backward compatibility: if data is a string, treat it as jobId
222
+ if isinstance(data, str):
223
+ jobid = data
224
+ if listener.scheduler:
225
+ emit("job.update", job_details(listener.scheduler.jobs[jobid]))
226
+ else:
227
+ experiment_id = data.get("experimentId")
228
+ job_id = data.get("jobId")
229
+ job_data = listener.state_provider.get_job(experiment_id, job_id)
230
+ if job_data:
231
+ emit("job.update", job_data)
169
232
 
170
233
  @socketio.on("services")
171
234
  def handle_services_list():
@@ -180,14 +243,26 @@ def start_app(server: "Server"):
180
243
  )
181
244
 
182
245
  @socketio.on("job.kill")
183
- def handle_job_kill(jobid: str):
184
- job = server.scheduler.jobs[jobid]
185
- future = asyncio.run_coroutine_threadsafe(
186
- job.aio_process(), server.scheduler.loop
187
- )
188
- process = future.result()
189
- if process is not None:
190
- process.kill()
246
+ def handle_job_kill(data):
247
+ """Kill a job - expects {experimentId, jobId} or just jobId (backward compat)"""
248
+ # Backward compatibility: if data is a string, treat it as jobId
249
+ if isinstance(data, str):
250
+ jobid = data
251
+ if listener.scheduler:
252
+ job = listener.scheduler.jobs[jobid]
253
+ future = asyncio.run_coroutine_threadsafe(
254
+ job.aio_process(), listener.scheduler.loop
255
+ )
256
+ process = future.result()
257
+ if process is not None:
258
+ process.kill()
259
+ else:
260
+ experiment_id = data.get("experimentId")
261
+ job_id = data.get("jobId")
262
+ try:
263
+ listener.state_provider.kill_job(experiment_id, job_id)
264
+ except NotImplementedError:
265
+ logging.warning("kill_job not supported for this state provider")
191
266
 
192
267
  logging.debug("Starting Flask server (setting up routes)...")
193
268
 
@@ -197,7 +272,15 @@ def start_app(server: "Server"):
197
272
  if not path:
198
273
  return redirect(f"/services/{service}/", http.HTTPStatus.PERMANENT_REDIRECT)
199
274
 
200
- service = server.scheduler.xp.services.get(service, None)
275
+ # Get service from all registered experiments
276
+ scheduler = Scheduler.instance()
277
+ service_obj = None
278
+ for xp in scheduler.experiments.values():
279
+ service_obj = xp.services.get(service, None)
280
+ if service_obj:
281
+ break
282
+
283
+ service = service_obj
201
284
  if service is None:
202
285
  return Response(f"Service {service} not found", http.HTTPStatus.NOT_FOUND)
203
286
 
@@ -210,7 +293,8 @@ def start_app(server: "Server"):
210
293
  progress = float(request.args.get("progress", 0.0))
211
294
 
212
295
  try:
213
- server.scheduler.jobs[jobid].set_progress(
296
+ scheduler = Scheduler.instance()
297
+ scheduler.jobs[jobid].set_progress(
214
298
  level,
215
299
  progress,
216
300
  request.args.get("desc", None),
@@ -256,10 +340,17 @@ def start_app(server: "Server"):
256
340
 
257
341
  datapath = "data/%s" % path
258
342
  logging.debug("Looking for %s", datapath)
259
- if pkg_resources.resource_exists("experimaestro.server", datapath):
260
- mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
261
- content = pkg_resources.resource_string("experimaestro.server", datapath)
262
- return Response(content, mimetype=mimetype)
343
+
344
+ try:
345
+ package_files = files("experimaestro.server")
346
+ resource_file = package_files / datapath
347
+ if resource_file.is_file():
348
+ mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
349
+ content = resource_file.read_bytes()
350
+ return Response(content, mimetype=mimetype)
351
+ except (FileNotFoundError, KeyError):
352
+ pass
353
+
263
354
  return Response("Page not found", status=404)
264
355
 
265
356
  # Start the app
@@ -292,7 +383,36 @@ def start_app(server: "Server"):
292
383
 
293
384
 
294
385
  class Server:
295
- def __init__(self, scheduler: Scheduler, settings: ServerSettings):
386
+ _instance: ClassVar[Optional["Server"]] = None
387
+ _lock: ClassVar[threading.Lock] = threading.Lock()
388
+
389
+ @staticmethod
390
+ def instance(settings: ServerSettings = None, state_provider=None) -> "Server":
391
+ """Get or create the global server instance
392
+
393
+ Args:
394
+ settings: Server settings (optional)
395
+ state_provider: WorkspaceStateProvider instance (required)
396
+ """
397
+ if Server._instance is None:
398
+ with Server._lock:
399
+ if Server._instance is None:
400
+ if settings is None:
401
+ from experimaestro.settings import get_settings
402
+
403
+ settings = get_settings().server
404
+
405
+ # State provider is required - it should be passed explicitly
406
+ if state_provider is None:
407
+ raise ValueError(
408
+ "state_provider parameter is required. "
409
+ "Get it via WorkspaceStateProvider.get_instance(workspace.path)"
410
+ )
411
+
412
+ Server._instance = Server(settings, state_provider)
413
+ return Server._instance
414
+
415
+ def __init__(self, settings: ServerSettings, state_provider):
296
416
  if settings.autohost == "fqdn":
297
417
  settings.host = socket.getfqdn()
298
418
  logging.info("Auto host name (fqdn): %s", settings.host)
@@ -307,8 +427,8 @@ class Server:
307
427
 
308
428
  self.host = settings.host or "127.0.0.1"
309
429
  self.port = settings.port
310
- self.scheduler = scheduler
311
430
  self.token = settings.token or uuid.uuid4().hex
431
+ self.state_provider = state_provider
312
432
  self.instance = None
313
433
  self.running = False
314
434
  self.cv_running = threading.Condition()
@@ -331,13 +451,14 @@ class Server:
331
451
  pass
332
452
 
333
453
  def start(self):
334
- """Start the websocket server in a new process process"""
454
+ """Start the websocket server in a daemon thread"""
335
455
  logging.info("Starting the web server")
336
456
 
337
457
  # Avoids clutering
338
458
  logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
339
459
 
340
- self.thread = threading.Thread(target=start_app, args=(self,)).start()
460
+ self.thread = threading.Thread(target=start_app, args=(self,), daemon=True)
461
+ self.thread.start()
341
462
 
342
463
  # Wait until we really started
343
464
  while True:
@@ -22753,131 +22753,6 @@ readers do not read off random characters that represent icons */
22753
22753
  font-weight: bold;
22754
22754
  }
22755
22755
 
22756
- @font-face {
22757
- font-family: "Material Icons";
22758
- font-style: normal;
22759
- font-weight: 400;
22760
- font-display: block;
22761
- src: url(/0c35d18bf06992036b69.woff2) format("woff2"), url(/4d73cb90e394b34b7670.woff) format("woff");
22762
- }
22763
- .material-icons {
22764
- font-family: "Material Icons";
22765
- font-weight: normal;
22766
- font-style: normal;
22767
- font-size: 24px;
22768
- line-height: 1;
22769
- letter-spacing: normal;
22770
- text-transform: none;
22771
- display: inline-block;
22772
- white-space: nowrap;
22773
- word-wrap: normal;
22774
- direction: ltr;
22775
- -webkit-font-smoothing: antialiased;
22776
- -moz-osx-font-smoothing: grayscale;
22777
- text-rendering: optimizeLegibility;
22778
- font-feature-settings: "liga";
22779
- }
22780
-
22781
- @font-face {
22782
- font-family: "Material Icons Outlined";
22783
- font-style: normal;
22784
- font-weight: 400;
22785
- font-display: block;
22786
- src: url(/6f420cf17cc0d7676fad.woff2) format("woff2"), url(/f882956fd323fd322f31.woff) format("woff");
22787
- }
22788
- .material-icons-outlined {
22789
- font-family: "Material Icons Outlined";
22790
- font-weight: normal;
22791
- font-style: normal;
22792
- font-size: 24px;
22793
- line-height: 1;
22794
- letter-spacing: normal;
22795
- text-transform: none;
22796
- display: inline-block;
22797
- white-space: nowrap;
22798
- word-wrap: normal;
22799
- direction: ltr;
22800
- -webkit-font-smoothing: antialiased;
22801
- -moz-osx-font-smoothing: grayscale;
22802
- text-rendering: optimizeLegibility;
22803
- font-feature-settings: "liga";
22804
- }
22805
-
22806
- @font-face {
22807
- font-family: "Material Icons Round";
22808
- font-style: normal;
22809
- font-weight: 400;
22810
- font-display: block;
22811
- src: url(/c380809fd3677d7d6903.woff2) format("woff2"), url(/5d681e2edae8c60630db.woff) format("woff");
22812
- }
22813
- .material-icons-round {
22814
- font-family: "Material Icons Round";
22815
- font-weight: normal;
22816
- font-style: normal;
22817
- font-size: 24px;
22818
- line-height: 1;
22819
- letter-spacing: normal;
22820
- text-transform: none;
22821
- display: inline-block;
22822
- white-space: nowrap;
22823
- word-wrap: normal;
22824
- direction: ltr;
22825
- -webkit-font-smoothing: antialiased;
22826
- -moz-osx-font-smoothing: grayscale;
22827
- text-rendering: optimizeLegibility;
22828
- font-feature-settings: "liga";
22829
- }
22830
-
22831
- @font-face {
22832
- font-family: "Material Icons Sharp";
22833
- font-style: normal;
22834
- font-weight: 400;
22835
- font-display: block;
22836
- src: url(/219aa9140e099e6c72ed.woff2) format("woff2"), url(/3a4004a46a653d4b2166.woff) format("woff");
22837
- }
22838
- .material-icons-sharp {
22839
- font-family: "Material Icons Sharp";
22840
- font-weight: normal;
22841
- font-style: normal;
22842
- font-size: 24px;
22843
- line-height: 1;
22844
- letter-spacing: normal;
22845
- text-transform: none;
22846
- display: inline-block;
22847
- white-space: nowrap;
22848
- word-wrap: normal;
22849
- direction: ltr;
22850
- -webkit-font-smoothing: antialiased;
22851
- -moz-osx-font-smoothing: grayscale;
22852
- text-rendering: optimizeLegibility;
22853
- font-feature-settings: "liga";
22854
- }
22855
-
22856
- @font-face {
22857
- font-family: "Material Icons Two Tone";
22858
- font-style: normal;
22859
- font-weight: 400;
22860
- font-display: block;
22861
- src: url(/4ef4218c522f1eb6b5b1.woff2) format("woff2"), url(/3baa5b8f3469222b822d.woff) format("woff");
22862
- }
22863
- .material-icons-two-tone {
22864
- font-family: "Material Icons Two Tone";
22865
- font-weight: normal;
22866
- font-style: normal;
22867
- font-size: 24px;
22868
- line-height: 1;
22869
- letter-spacing: normal;
22870
- text-transform: none;
22871
- display: inline-block;
22872
- white-space: nowrap;
22873
- word-wrap: normal;
22874
- direction: ltr;
22875
- -webkit-font-smoothing: antialiased;
22876
- -moz-osx-font-smoothing: grayscale;
22877
- text-rendering: optimizeLegibility;
22878
- font-feature-settings: "liga";
22879
- }
22880
-
22881
22756
  body {
22882
22757
  font-size: 1rem;
22883
22758
  }