cubevis 1.0.14__py3-none-any.whl → 1.0.26__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.
cubevis/bokeh/__init__.py CHANGED
@@ -30,6 +30,7 @@ provided by Bokeh'''
30
30
 
31
31
  from .state import order_bokeh_js as _order_bokeh_js
32
32
  from .state import register_model as _register_model
33
+ from .state._initialize import get_bokeh_js_paths
33
34
  from .state import set_cubevis_lib
34
35
 
35
36
  class BokehInit:
@@ -2,7 +2,13 @@ import logging
2
2
  from bokeh.core.properties import String, Dict, Any, Nullable, Instance
3
3
  from bokeh.models.layouts import LayoutDOM
4
4
  from bokeh.models.ui import UIElement
5
+ from bokeh.resources import CDN
6
+ from tempfile import TemporaryDirectory
5
7
  from uuid import uuid4
8
+ import unicodedata
9
+ import webbrowser
10
+ import os
11
+ import re
6
12
 
7
13
  logger = logging.getLogger(__name__)
8
14
 
@@ -29,9 +35,34 @@ class BokehAppContext(LayoutDOM):
29
35
  cls._session_id = str(uuid4())
30
36
  return cls._session_id
31
37
 
32
- def __init__( self, ui=None, **kwargs ):
38
+ def _slugify(self, value, allow_unicode=False):
39
+ """
40
+ Taken from https://github.com/django/django/blob/master/django/utils/text.py
41
+ Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
42
+ dashes to single dashes. Remove characters that aren't alphanumerics,
43
+ underscores, or hyphens. Convert to lowercase. Also strip leading and
44
+ trailing whitespace, dashes, and underscores.
45
+ https://stackoverflow.com/a/295466/2903943
46
+ """
47
+ value = str(value)
48
+ if allow_unicode:
49
+ value = unicodedata.normalize('NFKC', value)
50
+ else:
51
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
52
+ value = re.sub(r'[^\w\s-]', '', value.lower())
53
+ return re.sub(r'[-\s]+', '-', value).strip('-_')
54
+
55
+ def __init__( self, ui=None, title=str(uuid4( )), prefix=None, **kwargs ):
33
56
  logger.debug(f"\tBokehAppContext::__init__(ui={type(ui).__name__ if ui else None}, {kwargs}): {id(self)}")
34
57
 
58
+ if prefix is None:
59
+ ## create a prefix from the title
60
+ prefix = self._slugify(title)[:10]
61
+
62
+ self.__title = title
63
+ self.__workdir = TemporaryDirectory(prefix=prefix)
64
+ self.__htmlpath = os.path.join( self.__workdir.name, f'''{self._slugify(self.__title)}.html''' )
65
+
35
66
  if ui is not None and 'ui' in kwargs:
36
67
  raise RuntimeError( "'ui' supplied as both a positional parameter and a keyword parameter" )
37
68
 
@@ -61,3 +92,19 @@ class BokehAppContext(LayoutDOM):
61
92
  current_state = dict(self.app_state)
62
93
  current_state.update(state_updates)
63
94
  self.app_state = current_state
95
+
96
+ def show( self ):
97
+ """Always show plot in a new browser tab without changing output settings.
98
+ Jupyter display is handled by the Showable class. However, at some
99
+ point this function might need to support more than just independent
100
+ browser tab display.
101
+ """
102
+ logger.debug(f"\tBokehAppContext::show( ): {id(self)}")
103
+
104
+ from bokeh.plotting import save
105
+
106
+ # Save the plot
107
+ save( self, filename=self.__htmlpath, resources=CDN, title=self.__title)
108
+
109
+ # Open in browser
110
+ webbrowser.open('file://' + os.path.abspath(self.__htmlpath))
@@ -1,20 +1,30 @@
1
1
  import logging
2
2
  from bokeh.models.layouts import LayoutDOM
3
3
  from bokeh.models.ui import UIElement
4
- from bokeh.core.properties import Instance
4
+ from bokeh.core.properties import Instance, String
5
+
5
6
  from bokeh.io import curdoc
6
7
  from .. import BokehInit
8
+ from ...utils import is_colab
7
9
 
8
10
  logger = logging.getLogger(__name__)
9
11
 
10
12
  class Showable(LayoutDOM,BokehInit):
11
13
  """Wrap a UIElement to make any Bokeh UI component showable with show()
12
-
14
+
13
15
  This class works by acting as a simple container that delegates to its UI element.
14
16
  For Jupyter notebook display, use show(showable) - automatic display via _repr_mimebundle_
15
17
  is not reliably supported by Bokeh's architecture.
16
18
  """
17
19
 
20
+ ### _usage_mode is needed to prevent mixing "bokeh.plotting.show(showable)" with
21
+ ### "showable.show( )" or just evaluating "showable". This is required because the
22
+ ### latter use "bokeh.embed.components" to create the HTML that is rendered while
23
+ ### "bokeh.plotting.show(showable)" uses internal Bokeh rendering that is
24
+ ### incompatable with "bokeh.embed.components" usage. For this reason, the user
25
+ ### can use either one, but not both.
26
+ _usage_mode = None
27
+
18
28
  @property
19
29
  def document(self):
20
30
  """Get the document this model is attached to."""
@@ -26,58 +36,52 @@ class Showable(LayoutDOM,BokehInit):
26
36
  Intercept when Bokeh tries to attach us to a document.
27
37
  This is called by bokeh.plotting.show() when it adds us to a document.
28
38
  """
29
- from bokeh.io.state import curstate
30
- import traceback
31
39
 
32
- # Allow None (detaching from document)
40
+ def get_caller_class_name(frame):
41
+ """Attempt to find the name of the class the calling method belongs to."""
42
+ # Check if 'self' is in the caller's local variables (conventional for instance methods)
43
+ if 'self' in frame.f_locals:
44
+ return frame.f_locals['self'].__class__.__name__
45
+ # Check for 'cls' (conventional for class methods)
46
+ elif 'cls' in frame.f_locals:
47
+ return frame.f_locals['cls'].__name__
48
+ else:
49
+ # It might be a regular function or static method without explicit 'self'/'cls'
50
+ return None
51
+
52
+ # Allow None (detaching from document) without any further checking
33
53
  if doc is None:
34
54
  self._document = None
35
55
  return
36
56
 
37
- state = curstate()
38
-
39
- # Check calling context
40
- stack = traceback.extract_stack()
41
-
42
- # Detect if called from bokeh.plotting.show or bokeh.io.show
43
- called_from_bokeh_show = any(
44
- ('bokeh/io/' in frame.filename or 'bokeh\\io\\' in frame.filename or
45
- 'bokeh/plotting/' in frame.filename or 'bokeh\\plotting\\' in frame.filename)
46
- for frame in stack[:-2] # Exclude the last 2 frames (this setter and __setattr__)
47
- )
48
-
49
- # Check if called from our own methods
50
- called_from_our_methods = any(
51
- 'Showable' in str(frame.line) or frame.filename.endswith('showable.py')
52
- for frame in stack[-5:-2] # Check recent frames
53
- )
57
+ from bokeh.io.state import curstate
58
+ state = curstate( )
54
59
 
55
- if state.file and called_from_bokeh_show and not called_from_our_methods:
56
- raise RuntimeError(
57
- f"\n{'='*70}\n"
58
- f"❌ Cannot use bokeh.plotting.show() with {self.__class__.__name__}\n\n"
59
- f"Please use one of these methods instead:\n"
60
- f" • my_showable.show() # Custom show method\n"
61
- f" • my_showable # Automatic display (evaluate in cell)\n\n"
62
- f"Reason: bokeh.plotting.show() doesn't properly handle the custom\n"
63
- f"sizing and backend requirements of Showable objects.\n"
64
- f"{'='*70}\n"
65
- )
60
+ # Validate environment (only one OUTPUT mode)
61
+ active_modes = []
62
+ if state.file: active_modes.append('file')
63
+ if state.notebook: active_modes.append('notebook')
66
64
 
67
- # Validate environment
68
- if not state.notebook:
65
+ # only allow a single GUI to be displayed since there is a backend
66
+ # this could be relaxed if the backend can manage events from two
67
+ # different GUIs
68
+ if len(active_modes) > 1:
69
69
  raise RuntimeError(
70
- f"{self.__class__.__name__} can only be displayed in Jupyter notebooks.\n"
71
- f"Please run:\n"
72
- f" from bokeh.io import output_notebook\n"
73
- f" output_notebook()"
70
+ f"{self.__class__.__name__} can only be displayed in a single Bokeh\n"
71
+ f"display mode. Either file or notebook, but not both."
74
72
  )
75
73
 
76
- # Apply notebook sizing before attaching to document
77
- if self._notebook_sizing == 'fixed':
74
+ # For notebook display, fixed sizing is required. This selects between the
75
+ # fixed, notebook dimensions and the default browser dimensions based on
76
+ # the Bokeh output that has been selected.
77
+ if 'notebook' in active_modes and self._display_config['notebook']['mode'] == 'fixed':
78
78
  self.sizing_mode = None
79
- self.width = self._notebook_width
80
- self.height = self._notebook_height
79
+ self.width = self._display_config['notebook']['width']
80
+ self.height = self._display_config['notebook']['height']
81
+ else:
82
+ self.sizing_mode = self._display_config['browser']['mode']
83
+ self.width = self._display_config['browser']['width']
84
+ self.height = self._display_config['browser']['height']
81
85
 
82
86
  # Now set the document
83
87
  self._document = doc
@@ -87,12 +91,23 @@ class Showable(LayoutDOM,BokehInit):
87
91
  self._start_backend()
88
92
  self._backend_started = True
89
93
 
94
+ def to_serializable(self, *args, **kwargs):
95
+ if self._display_context:
96
+ self._display_context.on_to_serializable( )
97
+
98
+ # Call parent's to_serializable
99
+ return super().to_serializable(*args, **kwargs)
100
+
90
101
  def __init__( self, ui_element=None, backend_func=None,
91
102
  result_retrieval=None,
92
103
  notebook_width=1200, notebook_height=800,
93
- notebook_sizing='fixed', **kwargs):
104
+ notebook_sizing='fixed',
105
+ display_context=None,
106
+ **kwargs ):
94
107
  logger.debug(f"\tShowable::__init__(ui_element={type(ui_element).__name__ if ui_element else None}, {kwargs}): {id(self)}")
95
-
108
+
109
+ self._display_context = display_context
110
+
96
111
  # Set default sizing if not provided
97
112
  sizing_params = {'sizing_mode', 'width', 'height'}
98
113
  provided_sizing_params = set(kwargs.keys()) & sizing_params
@@ -102,11 +117,20 @@ class Showable(LayoutDOM,BokehInit):
102
117
  # CRITICAL FIX: Don't call _ensure_in_document during __init__
103
118
  # Let Bokeh handle document management through the normal flow
104
119
  super().__init__(**kwargs)
105
-
120
+
106
121
  # Set the UI element
107
122
  if ui_element is not None:
108
123
  self.ui = ui_element
109
124
 
125
+ # Keep track of defaults based on display mode
126
+ ### self._notebook_width = notebook_width
127
+ ### self._notebook_height = notebook_height
128
+ ### self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
129
+ self._display_config = {
130
+ 'notebook': { 'mode': notebook_sizing, 'width': notebook_width, 'height': notebook_height },
131
+ 'browser': { 'mode': self.sizing_mode, 'width': self.width, 'height': self.height }
132
+ }
133
+
110
134
  # Set the function to be called upon display
111
135
  if backend_func is not None:
112
136
  self._backend_startup_callback = backend_func
@@ -114,14 +138,35 @@ class Showable(LayoutDOM,BokehInit):
114
138
  # result (if one is/will be available)...
115
139
  self._result_retrieval = result_retrieval
116
140
 
117
- self._notebook_width = notebook_width
118
- self._notebook_height = notebook_height
119
- self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
120
141
  self._notebook_rendering = None
121
142
 
122
143
  ui = Instance(UIElement, help="""
123
144
  A UI element, which can be plots, layouts, widgets, or any other UIElement.
124
145
  """)
146
+ ###
147
+ ### when 'disabled' is set to true, this message should be displayed over
148
+ ### a grey-obscured GUI...
149
+ ###
150
+ ### May need to adjust message formatting to allow for more flexiblity,
151
+ ### i.e. all of the message does not need to be in big green print. May
152
+ ### need to allow something like:
153
+ ###
154
+ ### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
155
+ ### Interaction Complete ✓
156
+ ### </div>
157
+ ### <div style="font-size: 14px; color: #666;">
158
+ ### You can now close this GUI or continue working in your notebook
159
+ ### </div>
160
+ ###
161
+ ### currently the message is displayed in showable.ts like:
162
+ ###
163
+ ### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
164
+ ### ${this.model.disabled_message}
165
+ ### </div>
166
+ ###
167
+ disabled_message = String(default="Interaction Complete ✓", help="""
168
+ Message to show when disabled
169
+ """)
125
170
 
126
171
  # FIXED: Remove the children property override
127
172
  # Let LayoutDOM handle its own children management
@@ -138,7 +183,7 @@ class Showable(LayoutDOM,BokehInit):
138
183
  """Ensure this Showable is in the current document"""
139
184
  from bokeh.io import curdoc
140
185
  current_doc = curdoc()
141
-
186
+
142
187
  # FIXED: More careful document management
143
188
  # Only add to document if we're not already in the right one
144
189
  if self.document is None:
@@ -228,8 +273,11 @@ class Showable(LayoutDOM,BokehInit):
228
273
  Common logic for generating HTML in notebook environments.
229
274
  Returns the HTML string to display, or None if not in a notebook.
230
275
  """
231
- from bokeh.embed import components
276
+ from bokeh.embed import components, json_item
232
277
  from bokeh.io.state import curstate
278
+ from bokeh.resources import CDN
279
+ import sys
280
+ import json as json_lib
233
281
 
234
282
  state = curstate()
235
283
 
@@ -239,55 +287,93 @@ class Showable(LayoutDOM,BokehInit):
239
287
  if self.ui is None:
240
288
  return '<div style="color: red; padding: 10px; border: 1px solid red;">Showable object with no UI set</div>'
241
289
 
290
+ if self._display_context:
291
+ self._display_context.on_show()
292
+
242
293
  if self._notebook_rendering:
243
294
  # Return a lightweight reference instead of re-rendering the full GUI
244
295
  return f'''
245
296
  <div style="padding: 10px; background: #f0f8f0; border-left: 4px solid #4CAF50; margin: 5px 0;">
246
- <strong>↑ iclean GUI active above</strong>
297
+ <strong>→ iclean GUI active above</strong>
247
298
  <small style="color: #666; display: block; margin-top: 5px;">
248
299
  Showable ID: {self.id[-8:]} | Backend: Running
249
300
  </small>
250
301
  </div>
251
302
  '''
252
303
 
253
- # Apply notebook sizing for Jupyter context
254
- if self._notebook_sizing == 'fixed':
255
- self.sizing_mode = None
256
- self.width = self._notebook_width
257
- self.height = self._notebook_height
258
-
259
- script, div = components(self)
260
- if start_backend:
261
- self._start_backend()
262
-
263
- self._notebook_rendering = f'{script}\n{div}'
264
- return self._notebook_rendering
304
+ if is_colab( ):
305
+ # Get all JS paths from the existing function
306
+ # This returns paths in the correct order:
307
+ # [casalib, bokeh-core, bokeh-widgets, bokeh-tables, cubevisjs]
308
+ from cubevis.bokeh import get_bokeh_js_paths
309
+ js_paths = get_bokeh_js_paths( )
310
+
311
+ # Build script tags for all libraries in order
312
+ all_scripts = '\n'.join([
313
+ f'<script type="text/javascript" src="{url}"></script>'
314
+ for url in js_paths
315
+ ])
316
+
317
+ # Use json_item approach which is more reliable in iframes
318
+ item = json_item(self, target=f"bokeh-{self.id}")
319
+ item_json = json_lib.dumps(item)
320
+
321
+ # Build complete HTML with proper loading sequence
322
+ # get_bokeh_js_paths() already returns libs in the correct order:
323
+ # 1. casalib (third-party libs for CustomJS)
324
+ # 2. bokeh-core
325
+ # 3. bokeh-widgets
326
+ # 4. bokeh-tables
327
+ # 5. cubevisjs (custom Bokeh models)
328
+ html = f'''
329
+ {f'<link href="{CDN.css_files[0]}" rel="stylesheet" type="text/css">' if CDN.css_files else ""}
330
+ <div id="bokeh-{self.id}" class="bk-root"></div>
331
+ {all_scripts}
332
+ <script type="text/javascript">
333
+ (function() {{
334
+ var item = {item_json};
335
+
336
+ function embedWhenReady() {{
337
+ // Check if all required libraries are loaded
338
+ if (typeof Bokeh !== 'undefined' && Bokeh.embed) {{
339
+ var target = document.getElementById("bokeh-{self.id}");
340
+ if (target) {{
341
+ try {{
342
+ Bokeh.embed.embed_item(item);
343
+ console.log("Bokeh plot embedded successfully");
344
+ }} catch(e) {{
345
+ console.error("Error embedding Bokeh plot:", e);
346
+ }}
347
+ }} else {{
348
+ console.error("Target element not found");
349
+ setTimeout(embedWhenReady, 50);
350
+ }}
351
+ }} else {{
352
+ setTimeout(embedWhenReady, 50);
353
+ }}
354
+ }}
355
+
356
+ if (document.readyState === 'loading') {{
357
+ document.addEventListener('DOMContentLoaded', embedWhenReady);
358
+ }} else {{
359
+ embedWhenReady();
360
+ }}
361
+ }})();
362
+ </script>
363
+ '''
265
364
 
266
- def _repr_html_(self, start_backend=True):
267
- """
268
- HTML representation for Jupyter display.
365
+ if start_backend:
366
+ self._start_backend()
269
367
 
270
- Note: Bokeh doesn't reliably support automatic display via _repr_mimebundle_.
271
- This provides a helpful message directing users to use show().
272
- """
273
- html = self._get_notebook_html(start_backend)
274
-
275
- if html is not None:
368
+ self._notebook_rendering = html
276
369
  return html
277
-
278
- # Not in notebook environment
279
- return f"<!-- error: non-notebook environment{' in ' + self.name if self.name else ''} -->" + '''
280
- <div style="padding: 15px; border: 2px solid #4CAF50; border-radius: 5px; background: #f9fff9; margin: 10px 0;">
281
- <strong>📊 Showable Widget Ready</strong><br>
282
- <em>Notebook display is not enabled, run:</em>
283
- <p><pre>
284
- from bokeh.io import output_notebook
285
- output_notebook()</pre>
286
- <p><em>and try again.</em>
287
- <hr>
288
- <small>Contains: {}</small>
289
- </div>
290
- '''.format(type(self.ui).__name__ if self.ui else "None")
370
+ else:
371
+ # In Jupyter Lab/Classic, use components() as before
372
+ script, div = components(self)
373
+ if start_backend:
374
+ self._start_backend()
375
+ self._notebook_rendering = f'{script}\n{div}'
376
+ return self._notebook_rendering
291
377
 
292
378
  def _repr_mimebundle_(self, include=None, exclude=None):
293
379
  """
@@ -301,6 +387,8 @@ output_notebook()</pre>
301
387
  - Bokeh show(showable)
302
388
  - showable.show()
303
389
  """
390
+ if self.__class__._usage_mode is None:
391
+ self.__class__._usage_mode = "custom"
304
392
  from bokeh.io.state import curstate
305
393
 
306
394
  state = curstate()
@@ -311,13 +399,16 @@ output_notebook()</pre>
311
399
  return {
312
400
  'text/html': html
313
401
  }
314
-
402
+
315
403
  # Fall back to default Bokeh behavior for non-notebook environments
316
404
  # Return None to let Bokeh handle it
317
405
  return None
318
406
 
319
407
  def show(self, start_backend=True):
320
408
  """Explicitly show this Showable using inline display in Jupyter"""
409
+ if self.__class__._usage_mode is None:
410
+ self.__class__._usage_mode = "custom"
411
+
321
412
  from bokeh.io.state import curstate
322
413
 
323
414
  self._ensure_in_document()
@@ -327,7 +418,7 @@ output_notebook()</pre>
327
418
  if state.notebook:
328
419
  # In Jupyter, display directly using IPython.display
329
420
  from IPython.display import display, HTML
330
-
421
+
331
422
  html = self._get_notebook_html(start_backend)
332
423
  if html:
333
424
  display(HTML(html))
@@ -321,6 +321,13 @@ def order_bokeh_js():
321
321
  resources.Resources.js_files = property(js_files)
322
322
  return
323
323
 
324
+ #def get_bokeh_js_paths( ):
325
+ # modes = ['cdn','inline','server','server-dev','relative','relative-dev','absolute','absolute-dev']
326
+ # return { 'new': { mode: resources.Resources(mode=mode).js_files for mode in modes },
327
+ # 'old': { mode: resources.Resources(mode=mode)._old_js_files for mode in modes } }
328
+ def get_bokeh_js_paths( ):
329
+ return resources.Resources(mode='cdn').js_files
330
+
324
331
  def get_jupyter_state( ):
325
332
  """Get the package-level Jupyter state"""
326
333
  return _JUPYTER_STATE
cubevis/exe/_task.py CHANGED
@@ -67,31 +67,53 @@ class Task:
67
67
 
68
68
  return threading_event
69
69
 
70
+ def _run_coroutine_sync(self, coro):
71
+ """
72
+ Helper to run a coroutine synchronously, handling both CLI and Jupyter contexts.
73
+ """
74
+ try:
75
+ # Check if there's already a running event loop (Jupyter/IPython)
76
+ loop = asyncio.get_running_loop()
77
+ except RuntimeError:
78
+ # No running loop - safe to use asyncio.run() (CLI context)
79
+ return asyncio.run(coro)
80
+ else:
81
+ # Running loop exists (Jupyter context)
82
+ try:
83
+ import nest_asyncio
84
+ nest_asyncio.apply()
85
+ return asyncio.run(coro)
86
+ except ImportError:
87
+ # Fallback: run in a new thread with its own event loop
88
+ import concurrent.futures
89
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
90
+ return pool.submit(asyncio.run, coro).result()
91
+
70
92
  def run_sync(self) -> Any:
71
93
  """Run synchronously until completion or stop signal."""
72
94
  if self.stop_condition is None:
73
95
  # Handle async functions in sync context
74
96
  if asyncio.iscoroutinefunction(self.server_func):
75
- logger.debug( f"\tTask::run_sync( ): {id(self)} - asyncio no stop condition {self.server_func}" )
76
- return asyncio.run(self.server_func(*self.args, **self.kwargs))
97
+ logger.debug(f"\tTask::run_sync( ): {id(self)} - asyncio no stop condition {self.server_func}")
98
+ return self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
77
99
  else:
78
- logger.debug( f"\tTask::run_sync( ): {id(self)} - no stop condition {self.server_func}" )
100
+ logger.debug(f"\tTask::run_sync( ): {id(self)} - no stop condition {self.server_func}")
79
101
  return self.server_func(*self.args, **self.kwargs)
80
102
 
81
103
  # Handle asyncio.Event in sync context by converting to threading.Event
82
104
  stop_condition = self.stop_condition
83
105
  if isinstance(self.stop_condition, asyncio.Event):
84
- logger.debug( f"\tTask::run_sync( ): {id(self)} - asyncio bridge {self.server_func}" )
106
+ logger.debug(f"\tTask::run_sync( ): {id(self)} - asyncio bridge {self.server_func}")
85
107
  # Convert asyncio.Event to threading.Event for sync execution
86
108
  stop_condition = self._convert_asyncio_event_to_threading(self.stop_condition)
87
-
109
+
88
110
  if callable(stop_condition):
89
111
  # Poll-based stopping
90
- logger.debug( f"\tTask::run_sync( ): {id(self)} - polling {self.server_func}" )
112
+ logger.debug(f"\tTask::run_sync( ): {id(self)} - polling {self.server_func}")
91
113
  while not stop_condition():
92
114
  try:
93
115
  if asyncio.iscoroutinefunction(self.server_func):
94
- result = asyncio.run(self.server_func(*self.args, **self.kwargs))
116
+ result = self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
95
117
  else:
96
118
  result = self.server_func(*self.args, **self.kwargs)
97
119
  if result is not None: # Server function completed
@@ -102,12 +124,12 @@ class Task:
102
124
 
103
125
  elif isinstance(stop_condition, threading.Event):
104
126
  # Event-based stopping for threads
105
- logger.debug( f"\tTask::run_sync( ): {id(self)} - threading event {self.server_func}" )
127
+ logger.debug(f"\tTask::run_sync( ): {id(self)} - threading event {self.server_func}")
106
128
  def run_with_check():
107
129
  while not stop_condition.is_set():
108
130
  try:
109
131
  if asyncio.iscoroutinefunction(self.server_func):
110
- result = asyncio.run(self.server_func(*self.args, **self.kwargs))
132
+ result = self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
111
133
  else:
112
134
  result = self.server_func(*self.args, **self.kwargs)
113
135
  if result is not None:
@@ -118,26 +140,25 @@ class Task:
118
140
  return run_with_check()
119
141
 
120
142
  elif isinstance(stop_condition, threading.Condition):
121
- logger.debug( f"\tTask::run_sync( ): {id(self)} - threading condition {self.server_func}" )
143
+ logger.debug(f"\tTask::run_sync( ): {id(self)} - threading condition {self.server_func}")
122
144
  # Condition-based stopping - most flexible and efficient
123
145
  def run_with_condition():
124
146
  with stop_condition:
125
147
  while True:
126
- # Check if we should stop (condition should have a flag or predicate)
127
- # The condition object should be used by external code to signal stopping
128
- # We'll pass the condition to the server function so it can wait efficiently
129
148
  try:
130
149
  # Pass the condition to the server function if it accepts it
131
150
  import inspect
132
151
  sig = inspect.signature(self.server_func)
133
152
  if 'stop_condition' in sig.parameters:
134
153
  if asyncio.iscoroutinefunction(self.server_func):
135
- result = asyncio.run(self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs))
154
+ result = self._run_coroutine_sync(
155
+ self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs)
156
+ )
136
157
  else:
137
158
  result = self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs)
138
159
  else:
139
160
  if asyncio.iscoroutinefunction(self.server_func):
140
- result = asyncio.run(self.server_func(*self.args, **self.kwargs))
161
+ result = self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
141
162
  else:
142
163
  result = self.server_func(*self.args, **self.kwargs)
143
164
 
@@ -227,4 +248,3 @@ class Task:
227
248
  # Sync function in async context - run in thread pool
228
249
  loop = asyncio.get_event_loop()
229
250
  return await loop.run_in_executor(None, self.run_sync)
230
-