cubevis 1.0.5__py3-none-any.whl → 1.0.21__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.
@@ -0,0 +1,110 @@
1
+ import logging
2
+ from bokeh.core.properties import String, Dict, Any, Nullable, Instance
3
+ from bokeh.models.layouts import LayoutDOM
4
+ from bokeh.models.ui import UIElement
5
+ from bokeh.resources import CDN
6
+ from tempfile import TemporaryDirectory
7
+ from uuid import uuid4
8
+ import unicodedata
9
+ import webbrowser
10
+ import os
11
+ import re
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class BokehAppContext(LayoutDOM):
16
+ """
17
+ Custom Bokeh model that bridges Python AppContext with JavaScript.
18
+ Initializes session-level data structure and app-specific state.
19
+ """
20
+ ui = Nullable(Instance(UIElement), help="""
21
+ A UI element, which can be plots, layouts, widgets, or any other UIElement.
22
+ """)
23
+
24
+ app_id = String(default="")
25
+ session_id = String(default="")
26
+ app_state = Dict(String, Any, default={})
27
+
28
+ # Class-level session ID shared across all apps in the same Python session
29
+ _session_id = None
30
+
31
+ @classmethod
32
+ def get_session_id(cls):
33
+ """Get or create a session ID for this Python session"""
34
+ if cls._session_id is None:
35
+ cls._session_id = str(uuid4())
36
+ return cls._session_id
37
+
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 ):
56
+ logger.debug(f"\tBokehAppContext::__init__(ui={type(ui).__name__ if ui else None}, {kwargs}): {id(self)}")
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
+
66
+ if ui is not None and 'ui' in kwargs:
67
+ raise RuntimeError( "'ui' supplied as both a positional parameter and a keyword parameter" )
68
+
69
+ kwargs['session_id'] = self.get_session_id( )
70
+
71
+ if 'ui' not in kwargs:
72
+ kwargs['ui'] = ui
73
+ if 'app_id' not in kwargs:
74
+ kwargs['app_id'] = str(uuid4())
75
+
76
+ super().__init__(**kwargs)
77
+
78
+ def _sphinx_height_hint(self):
79
+ """Delegate height hint to the wrapped UI element"""
80
+ logger.debug(f"\tShowable::_sphinx_height_hint(): {id(self)}")
81
+ if self.ui and hasattr(self.ui, '_sphinx_height_hint'):
82
+ return self.ui._sphinx_height_hint()
83
+ return None
84
+
85
+ def update_app_state(self, state_updates):
86
+ """
87
+ Update the application state (will be in the generated HTML/JS)
88
+
89
+ Args:
90
+ state_updates: dict of state key-value pairs to update
91
+ """
92
+ current_state = dict(self.app_state)
93
+ current_state.update(state_updates)
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,7 +1,8 @@
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
7
8
 
@@ -9,12 +10,20 @@ logger = logging.getLogger(__name__)
9
10
 
10
11
  class Showable(LayoutDOM,BokehInit):
11
12
  """Wrap a UIElement to make any Bokeh UI component showable with show()
12
-
13
+
13
14
  This class works by acting as a simple container that delegates to its UI element.
14
15
  For Jupyter notebook display, use show(showable) - automatic display via _repr_mimebundle_
15
16
  is not reliably supported by Bokeh's architecture.
16
17
  """
17
18
 
19
+ ### _usage_mode is needed to prevent mixing "bokeh.plotting.show(showable)" with
20
+ ### "showable.show( )" or just evaluating "showable". This is required because the
21
+ ### latter use "bokeh.embed.components" to create the HTML that is rendered while
22
+ ### "bokeh.plotting.show(showable)" uses internal Bokeh rendering that is
23
+ ### incompatable with "bokeh.embed.components" usage. For this reason, the user
24
+ ### can use either one, but not both.
25
+ _usage_mode = None
26
+
18
27
  @property
19
28
  def document(self):
20
29
  """Get the document this model is attached to."""
@@ -26,58 +35,52 @@ class Showable(LayoutDOM,BokehInit):
26
35
  Intercept when Bokeh tries to attach us to a document.
27
36
  This is called by bokeh.plotting.show() when it adds us to a document.
28
37
  """
29
- from bokeh.io.state import curstate
30
- import traceback
31
38
 
32
- # Allow None (detaching from document)
39
+ def get_caller_class_name(frame):
40
+ """Attempt to find the name of the class the calling method belongs to."""
41
+ # Check if 'self' is in the caller's local variables (conventional for instance methods)
42
+ if 'self' in frame.f_locals:
43
+ return frame.f_locals['self'].__class__.__name__
44
+ # Check for 'cls' (conventional for class methods)
45
+ elif 'cls' in frame.f_locals:
46
+ return frame.f_locals['cls'].__name__
47
+ else:
48
+ # It might be a regular function or static method without explicit 'self'/'cls'
49
+ return None
50
+
51
+ # Allow None (detaching from document) without any further checking
33
52
  if doc is None:
34
53
  self._document = None
35
54
  return
36
55
 
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
- )
56
+ from bokeh.io.state import curstate
57
+ state = curstate( )
54
58
 
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
- )
59
+ # Validate environment (only one OUTPUT mode)
60
+ active_modes = []
61
+ if state.file: active_modes.append('file')
62
+ if state.notebook: active_modes.append('notebook')
66
63
 
67
- # Validate environment
68
- if not state.notebook:
64
+ # only allow a single GUI to be displayed since there is a backend
65
+ # this could be relaxed if the backend can manage events from two
66
+ # different GUIs
67
+ if len(active_modes) > 1:
69
68
  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()"
69
+ f"{self.__class__.__name__} can only be displayed in a single Bokeh\n"
70
+ f"display mode. Either file or notebook, but not both."
74
71
  )
75
72
 
76
- # Apply notebook sizing before attaching to document
77
- if self._notebook_sizing == 'fixed':
73
+ # For notebook display, fixed sizing is required. This selects between the
74
+ # fixed, notebook dimensions and the default browser dimensions based on
75
+ # the Bokeh output that has been selected.
76
+ if 'notebook' in active_modes and self._display_config['notebook']['mode'] == 'fixed':
78
77
  self.sizing_mode = None
79
- self.width = self._notebook_width
80
- self.height = self._notebook_height
78
+ self.width = self._display_config['notebook']['width']
79
+ self.height = self._display_config['notebook']['height']
80
+ else:
81
+ self.sizing_mode = self._display_config['browser']['mode']
82
+ self.width = self._display_config['browser']['width']
83
+ self.height = self._display_config['browser']['height']
81
84
 
82
85
  # Now set the document
83
86
  self._document = doc
@@ -87,12 +90,23 @@ class Showable(LayoutDOM,BokehInit):
87
90
  self._start_backend()
88
91
  self._backend_started = True
89
92
 
93
+ def to_serializable(self, *args, **kwargs):
94
+ if self._display_context:
95
+ self._display_context.on_to_serializable( )
96
+
97
+ # Call parent's to_serializable
98
+ return super().to_serializable(*args, **kwargs)
99
+
90
100
  def __init__( self, ui_element=None, backend_func=None,
91
101
  result_retrieval=None,
92
102
  notebook_width=1200, notebook_height=800,
93
- notebook_sizing='fixed', **kwargs):
103
+ notebook_sizing='fixed',
104
+ display_context=None,
105
+ **kwargs ):
94
106
  logger.debug(f"\tShowable::__init__(ui_element={type(ui_element).__name__ if ui_element else None}, {kwargs}): {id(self)}")
95
-
107
+
108
+ self._display_context = display_context
109
+
96
110
  # Set default sizing if not provided
97
111
  sizing_params = {'sizing_mode', 'width', 'height'}
98
112
  provided_sizing_params = set(kwargs.keys()) & sizing_params
@@ -102,11 +116,20 @@ class Showable(LayoutDOM,BokehInit):
102
116
  # CRITICAL FIX: Don't call _ensure_in_document during __init__
103
117
  # Let Bokeh handle document management through the normal flow
104
118
  super().__init__(**kwargs)
105
-
119
+
106
120
  # Set the UI element
107
121
  if ui_element is not None:
108
122
  self.ui = ui_element
109
123
 
124
+ # Keep track of defaults based on display mode
125
+ ### self._notebook_width = notebook_width
126
+ ### self._notebook_height = notebook_height
127
+ ### self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
128
+ self._display_config = {
129
+ 'notebook': { 'mode': notebook_sizing, 'width': notebook_width, 'height': notebook_height },
130
+ 'browser': { 'mode': self.sizing_mode, 'width': self.width, 'height': self.height }
131
+ }
132
+
110
133
  # Set the function to be called upon display
111
134
  if backend_func is not None:
112
135
  self._backend_startup_callback = backend_func
@@ -114,14 +137,35 @@ class Showable(LayoutDOM,BokehInit):
114
137
  # result (if one is/will be available)...
115
138
  self._result_retrieval = result_retrieval
116
139
 
117
- self._notebook_width = notebook_width
118
- self._notebook_height = notebook_height
119
- self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
120
140
  self._notebook_rendering = None
121
141
 
122
142
  ui = Instance(UIElement, help="""
123
143
  A UI element, which can be plots, layouts, widgets, or any other UIElement.
124
144
  """)
145
+ ###
146
+ ### when 'disabled' is set to true, this message should be displayed over
147
+ ### a grey-obscured GUI...
148
+ ###
149
+ ### May need to adjust message formatting to allow for more flexiblity,
150
+ ### i.e. all of the message does not need to be in big green print. May
151
+ ### need to allow something like:
152
+ ###
153
+ ### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
154
+ ### Interaction Complete ✓
155
+ ### </div>
156
+ ### <div style="font-size: 14px; color: #666;">
157
+ ### You can now close this GUI or continue working in your notebook
158
+ ### </div>
159
+ ###
160
+ ### currently the message is displayed in showable.ts like:
161
+ ###
162
+ ### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
163
+ ### ${this.model.disabled_message}
164
+ ### </div>
165
+ ###
166
+ disabled_message = String(default="Interaction Complete ✓", help="""
167
+ Message to show when disabled
168
+ """)
125
169
 
126
170
  # FIXED: Remove the children property override
127
171
  # Let LayoutDOM handle its own children management
@@ -138,7 +182,7 @@ class Showable(LayoutDOM,BokehInit):
138
182
  """Ensure this Showable is in the current document"""
139
183
  from bokeh.io import curdoc
140
184
  current_doc = curdoc()
141
-
185
+
142
186
  # FIXED: More careful document management
143
187
  # Only add to document if we're not already in the right one
144
188
  if self.document is None:
@@ -239,6 +283,9 @@ class Showable(LayoutDOM,BokehInit):
239
283
  if self.ui is None:
240
284
  return '<div style="color: red; padding: 10px; border: 1px solid red;">Showable object with no UI set</div>'
241
285
 
286
+ if self._display_context:
287
+ self._display_context.on_show( )
288
+
242
289
  if self._notebook_rendering:
243
290
  # Return a lightweight reference instead of re-rendering the full GUI
244
291
  return f'''
@@ -251,10 +298,10 @@ class Showable(LayoutDOM,BokehInit):
251
298
  '''
252
299
 
253
300
  # 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
301
+ ###if self._notebook_sizing == 'fixed':
302
+ ### self.sizing_mode = None
303
+ ### self.width = self._notebook_width
304
+ ### self.height = self._notebook_height
258
305
 
259
306
  script, div = components(self)
260
307
  if start_backend:
@@ -263,32 +310,6 @@ class Showable(LayoutDOM,BokehInit):
263
310
  self._notebook_rendering = f'{script}\n{div}'
264
311
  return self._notebook_rendering
265
312
 
266
- def _repr_html_(self, start_backend=True):
267
- """
268
- HTML representation for Jupyter display.
269
-
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:
276
- 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")
291
-
292
313
  def _repr_mimebundle_(self, include=None, exclude=None):
293
314
  """
294
315
  MIME bundle representation for Jupyter display.
@@ -301,6 +322,8 @@ output_notebook()</pre>
301
322
  - Bokeh show(showable)
302
323
  - showable.show()
303
324
  """
325
+ if self.__class__._usage_mode is None:
326
+ self.__class__._usage_mode = "custom"
304
327
  from bokeh.io.state import curstate
305
328
 
306
329
  state = curstate()
@@ -311,13 +334,16 @@ output_notebook()</pre>
311
334
  return {
312
335
  'text/html': html
313
336
  }
314
-
337
+
315
338
  # Fall back to default Bokeh behavior for non-notebook environments
316
339
  # Return None to let Bokeh handle it
317
340
  return None
318
341
 
319
342
  def show(self, start_backend=True):
320
343
  """Explicitly show this Showable using inline display in Jupyter"""
344
+ if self.__class__._usage_mode is None:
345
+ self.__class__._usage_mode = "custom"
346
+
321
347
  from bokeh.io.state import curstate
322
348
 
323
349
  self._ensure_in_document()
@@ -327,7 +353,7 @@ output_notebook()</pre>
327
353
  if state.notebook:
328
354
  # In Jupyter, display directly using IPython.display
329
355
  from IPython.display import display, HTML
330
-
356
+
331
357
  html = self._get_notebook_html(start_backend)
332
358
  if html:
333
359
  display(HTML(html))
@@ -36,13 +36,14 @@ import asyncio
36
36
  import traceback
37
37
  import time
38
38
  import json
39
+ from uuid import uuid4
39
40
 
40
41
  from bokeh.models.sources import DataSource
41
42
  from bokeh.util.compiler import TypeScript
42
- from bokeh.core.properties import Tuple, String, Int, Instance, Nullable
43
+ from bokeh.core.properties import Tuple, String, Int, Instance, Nullable, Bool
43
44
  from bokeh.models.callbacks import Callback
44
45
 
45
- from ...utils import serialize, deserialize
46
+ from ...utils import serialize, deserialize, is_interactive_jupyter
46
47
  from ..state import casalib_url, cubevisjs_url
47
48
  from .. import BokehInit
48
49
 
@@ -72,6 +73,10 @@ class DataPipe(DataSource,BokehInit):
72
73
 
73
74
  address = Tuple( String, Int, help="two integer sequence representing the address and port to use for the websocket" )
74
75
 
76
+ instance_id = String( help="Unique ID for each DataPipe object" )
77
+
78
+ conflict_check = Bool( default=True, help="Perform check to avoid reuse of URL for GUI. Not needed in the Jupyter context" )
79
+
75
80
  # Class-level session tracking to prevent multiple connections
76
81
  _active_sessions = {} # session_id -> {'websocket': ws, 'timestamp': time, 'datapipe': instance}
77
82
  _session_lock = threading.Lock()
@@ -82,7 +87,14 @@ class DataPipe(DataSource,BokehInit):
82
87
  #__javascript__ = [ casalib_url( ), cubevisjs_url( ) ]
83
88
 
84
89
  def __init__( self, *args, abort=None, **kwargs ):
90
+
91
+ if 'conflict_check' not in kwargs:
92
+ kwargs['conflict_check'] = not is_interactive_jupyter( )
93
+ if 'instance_id' not in kwargs:
94
+ kwargs['instance_id'] = str(uuid4( ))
95
+
85
96
  super( ).__init__( *args, **kwargs )
97
+
86
98
  self.__send_queue = { }
87
99
  self.__pending = { }
88
100
  self.__incoming_callbacks = { }
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
-