experimaestro 2.0.0a8__py3-none-any.whl → 2.0.0b8__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 (122) hide show
  1. experimaestro/__init__.py +10 -11
  2. experimaestro/annotations.py +167 -206
  3. experimaestro/cli/__init__.py +278 -7
  4. experimaestro/cli/filter.py +42 -74
  5. experimaestro/cli/jobs.py +157 -106
  6. experimaestro/cli/refactor.py +249 -0
  7. experimaestro/click.py +0 -1
  8. experimaestro/commandline.py +19 -3
  9. experimaestro/connectors/__init__.py +20 -1
  10. experimaestro/connectors/local.py +12 -0
  11. experimaestro/core/arguments.py +182 -46
  12. experimaestro/core/identifier.py +107 -6
  13. experimaestro/core/objects/__init__.py +6 -0
  14. experimaestro/core/objects/config.py +542 -25
  15. experimaestro/core/objects/config_walk.py +20 -0
  16. experimaestro/core/serialization.py +91 -34
  17. experimaestro/core/subparameters.py +164 -0
  18. experimaestro/core/types.py +175 -38
  19. experimaestro/exceptions.py +26 -0
  20. experimaestro/experiments/cli.py +111 -25
  21. experimaestro/generators.py +50 -9
  22. experimaestro/huggingface.py +3 -1
  23. experimaestro/launcherfinder/parser.py +29 -0
  24. experimaestro/launchers/__init__.py +26 -1
  25. experimaestro/launchers/direct.py +12 -0
  26. experimaestro/launchers/slurm/base.py +154 -2
  27. experimaestro/mkdocs/metaloader.py +0 -1
  28. experimaestro/mypy.py +452 -7
  29. experimaestro/notifications.py +63 -13
  30. experimaestro/progress.py +0 -2
  31. experimaestro/rpyc.py +0 -1
  32. experimaestro/run.py +19 -6
  33. experimaestro/scheduler/base.py +510 -125
  34. experimaestro/scheduler/dependencies.py +43 -28
  35. experimaestro/scheduler/dynamic_outputs.py +259 -130
  36. experimaestro/scheduler/experiment.py +256 -31
  37. experimaestro/scheduler/interfaces.py +501 -0
  38. experimaestro/scheduler/jobs.py +216 -206
  39. experimaestro/scheduler/remote/__init__.py +31 -0
  40. experimaestro/scheduler/remote/client.py +874 -0
  41. experimaestro/scheduler/remote/protocol.py +467 -0
  42. experimaestro/scheduler/remote/server.py +423 -0
  43. experimaestro/scheduler/remote/sync.py +144 -0
  44. experimaestro/scheduler/services.py +323 -23
  45. experimaestro/scheduler/state_db.py +437 -0
  46. experimaestro/scheduler/state_provider.py +2766 -0
  47. experimaestro/scheduler/state_sync.py +891 -0
  48. experimaestro/scheduler/workspace.py +52 -10
  49. experimaestro/scriptbuilder.py +7 -0
  50. experimaestro/server/__init__.py +147 -57
  51. experimaestro/server/data/index.css +0 -125
  52. experimaestro/server/data/index.css.map +1 -1
  53. experimaestro/server/data/index.js +194 -58
  54. experimaestro/server/data/index.js.map +1 -1
  55. experimaestro/settings.py +44 -5
  56. experimaestro/sphinx/__init__.py +3 -3
  57. experimaestro/taskglobals.py +20 -0
  58. experimaestro/tests/conftest.py +80 -0
  59. experimaestro/tests/core/test_generics.py +2 -2
  60. experimaestro/tests/identifier_stability.json +45 -0
  61. experimaestro/tests/launchers/bin/sacct +6 -2
  62. experimaestro/tests/launchers/bin/sbatch +4 -2
  63. experimaestro/tests/launchers/test_slurm.py +80 -0
  64. experimaestro/tests/tasks/test_dynamic.py +231 -0
  65. experimaestro/tests/test_cli_jobs.py +615 -0
  66. experimaestro/tests/test_deprecated.py +630 -0
  67. experimaestro/tests/test_environment.py +200 -0
  68. experimaestro/tests/test_file_progress_integration.py +1 -1
  69. experimaestro/tests/test_forward.py +3 -3
  70. experimaestro/tests/test_identifier.py +372 -41
  71. experimaestro/tests/test_identifier_stability.py +458 -0
  72. experimaestro/tests/test_instance.py +3 -3
  73. experimaestro/tests/test_multitoken.py +442 -0
  74. experimaestro/tests/test_mypy.py +433 -0
  75. experimaestro/tests/test_objects.py +312 -5
  76. experimaestro/tests/test_outputs.py +2 -2
  77. experimaestro/tests/test_param.py +8 -12
  78. experimaestro/tests/test_partial_paths.py +231 -0
  79. experimaestro/tests/test_progress.py +0 -48
  80. experimaestro/tests/test_remote_state.py +671 -0
  81. experimaestro/tests/test_resumable_task.py +480 -0
  82. experimaestro/tests/test_serializers.py +141 -1
  83. experimaestro/tests/test_state_db.py +434 -0
  84. experimaestro/tests/test_subparameters.py +160 -0
  85. experimaestro/tests/test_tags.py +136 -0
  86. experimaestro/tests/test_tasks.py +107 -121
  87. experimaestro/tests/test_token_locking.py +252 -0
  88. experimaestro/tests/test_tokens.py +17 -13
  89. experimaestro/tests/test_types.py +123 -1
  90. experimaestro/tests/test_workspace_triggers.py +158 -0
  91. experimaestro/tests/token_reschedule.py +4 -2
  92. experimaestro/tests/utils.py +2 -2
  93. experimaestro/tokens.py +154 -57
  94. experimaestro/tools/diff.py +1 -1
  95. experimaestro/tui/__init__.py +8 -0
  96. experimaestro/tui/app.py +2395 -0
  97. experimaestro/tui/app.tcss +353 -0
  98. experimaestro/tui/log_viewer.py +228 -0
  99. experimaestro/utils/__init__.py +23 -0
  100. experimaestro/utils/environment.py +148 -0
  101. experimaestro/utils/git.py +129 -0
  102. experimaestro/utils/resources.py +1 -1
  103. experimaestro/version.py +34 -0
  104. {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b8.dist-info}/METADATA +68 -38
  105. experimaestro-2.0.0b8.dist-info/RECORD +187 -0
  106. {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b8.dist-info}/WHEEL +1 -1
  107. experimaestro-2.0.0b8.dist-info/entry_points.txt +16 -0
  108. experimaestro/compat.py +0 -6
  109. experimaestro/core/objects.pyi +0 -221
  110. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  111. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  112. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  113. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  114. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  115. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  116. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  117. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  118. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  119. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  120. experimaestro-2.0.0a8.dist-info/RECORD +0 -166
  121. experimaestro-2.0.0a8.dist-info/entry_points.txt +0 -17
  122. {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b8.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"""
@@ -8,22 +8,9 @@ from experimaestro.scheduler.base import Job
8
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
12
 
13
- if sys.version_info >= (3, 9):
14
- from importlib.resources import files
15
-
16
- pkg_resources = None
17
- else:
18
- try:
19
- from importlib_resources import files
20
-
21
- pkg_resources = None
22
- except ImportError:
23
- # Fallback to pkg_resources if importlib_resources not available
24
- import pkg_resources
25
-
26
- files = None
13
+ from importlib.resources import files
27
14
  from experimaestro.scheduler import Scheduler, Listener as BaseListener
28
15
  from experimaestro.scheduler.services import Service, ServiceListener
29
16
  from experimaestro.settings import ServerSettings
@@ -61,6 +48,9 @@ def job_details(job):
61
48
 
62
49
 
63
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
+
64
54
  return {
65
55
  "jobId": job.identifier,
66
56
  "taskId": job.name,
@@ -68,28 +58,50 @@ def job_create(job: Job):
68
58
  "status": job.state.name.lower(),
69
59
  "tags": list(job.tags.items()),
70
60
  "progress": progress_state(job),
61
+ "experimentIds": experiment_ids, # Add experiment IDs
71
62
  }
72
63
 
73
64
 
74
65
  class Listener(BaseListener, ServiceListener):
75
- def __init__(self, scheduler: Scheduler, socketio):
76
- self.scheduler = scheduler
66
+ def __init__(self, socketio, state_provider):
77
67
  self.socketio = socketio
78
- self.scheduler.addlistener(self)
79
- self.services = {}
80
- for service in self.scheduler.xp.services.values():
81
- 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 = {}
82
92
 
83
93
  def job_submitted(self, job):
84
94
  self.socketio.emit("job.add", job_create(job))
85
95
 
86
96
  def job_state(self, job):
97
+ experiment_ids = [xp.workdir.name for xp in job.experiments]
87
98
  self.socketio.emit(
88
99
  "job.update",
89
100
  {
90
101
  "jobId": job.identifier,
91
102
  "status": job.state.name.lower(),
92
103
  "progress": progress_state(job),
104
+ "experimentIds": experiment_ids,
93
105
  },
94
106
  )
95
107
 
@@ -165,7 +177,7 @@ def start_app(server: "Server"):
165
177
 
166
178
  logging.debug("Starting Flask server (SocketIO)...")
167
179
  socketio = SocketIO(app, path="/api", async_mode="gevent")
168
- listener = Listener(server.scheduler, socketio)
180
+ listener = Listener(socketio, server.state_provider)
169
181
 
170
182
  logging.debug("Starting Flask server (setting up socketio)...")
171
183
 
@@ -175,13 +187,48 @@ def start_app(server: "Server"):
175
187
  raise ConnectionRefusedError("invalid token")
176
188
 
177
189
  @socketio.on("refresh")
178
- def handle_refresh():
179
- for job in listener.scheduler.jobs.values():
180
- 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)
181
217
 
182
218
  @socketio.on("job.details")
183
- def handle_details(jobid):
184
- 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)
185
232
 
186
233
  @socketio.on("services")
187
234
  def handle_services_list():
@@ -196,14 +243,26 @@ def start_app(server: "Server"):
196
243
  )
197
244
 
198
245
  @socketio.on("job.kill")
199
- def handle_job_kill(jobid: str):
200
- job = server.scheduler.jobs[jobid]
201
- future = asyncio.run_coroutine_threadsafe(
202
- job.aio_process(), server.scheduler.loop
203
- )
204
- process = future.result()
205
- if process is not None:
206
- 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")
207
266
 
208
267
  logging.debug("Starting Flask server (setting up routes)...")
209
268
 
@@ -213,7 +272,15 @@ def start_app(server: "Server"):
213
272
  if not path:
214
273
  return redirect(f"/services/{service}/", http.HTTPStatus.PERMANENT_REDIRECT)
215
274
 
216
- 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
217
284
  if service is None:
218
285
  return Response(f"Service {service} not found", http.HTTPStatus.NOT_FOUND)
219
286
 
@@ -226,7 +293,8 @@ def start_app(server: "Server"):
226
293
  progress = float(request.args.get("progress", 0.0))
227
294
 
228
295
  try:
229
- server.scheduler.jobs[jobid].set_progress(
296
+ scheduler = Scheduler.instance()
297
+ scheduler.jobs[jobid].set_progress(
230
298
  level,
231
299
  progress,
232
300
  request.args.get("desc", None),
@@ -273,24 +341,16 @@ def start_app(server: "Server"):
273
341
  datapath = "data/%s" % path
274
342
  logging.debug("Looking for %s", datapath)
275
343
 
276
- if files is not None:
277
- try:
278
- package_files = files("experimaestro.server")
279
- resource_file = package_files / datapath
280
- if resource_file.is_file():
281
- mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
282
- content = resource_file.read_bytes()
283
- return Response(content, mimetype=mimetype)
284
- except (FileNotFoundError, KeyError):
285
- pass
286
- elif pkg_resources is not None:
287
- # Fallback to pkg_resources
288
- if pkg_resources.resource_exists("experimaestro.server", datapath):
344
+ try:
345
+ package_files = files("experimaestro.server")
346
+ resource_file = package_files / datapath
347
+ if resource_file.is_file():
289
348
  mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
290
- content = pkg_resources.resource_string(
291
- "experimaestro.server", datapath
292
- )
349
+ content = resource_file.read_bytes()
293
350
  return Response(content, mimetype=mimetype)
351
+ except (FileNotFoundError, KeyError):
352
+ pass
353
+
294
354
  return Response("Page not found", status=404)
295
355
 
296
356
  # Start the app
@@ -323,7 +383,36 @@ def start_app(server: "Server"):
323
383
 
324
384
 
325
385
  class Server:
326
- 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):
327
416
  if settings.autohost == "fqdn":
328
417
  settings.host = socket.getfqdn()
329
418
  logging.info("Auto host name (fqdn): %s", settings.host)
@@ -338,8 +427,8 @@ class Server:
338
427
 
339
428
  self.host = settings.host or "127.0.0.1"
340
429
  self.port = settings.port
341
- self.scheduler = scheduler
342
430
  self.token = settings.token or uuid.uuid4().hex
431
+ self.state_provider = state_provider
343
432
  self.instance = None
344
433
  self.running = False
345
434
  self.cv_running = threading.Condition()
@@ -362,13 +451,14 @@ class Server:
362
451
  pass
363
452
 
364
453
  def start(self):
365
- """Start the websocket server in a new process process"""
454
+ """Start the websocket server in a daemon thread"""
366
455
  logging.info("Starting the web server")
367
456
 
368
457
  # Avoids clutering
369
458
  logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
370
459
 
371
- 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()
372
462
 
373
463
  # Wait until we really started
374
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
  }