cubevis 0.5.19__tar.gz → 0.5.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (149) hide show
  1. {cubevis-0.5.19 → cubevis-0.5.20}/PKG-INFO +1 -1
  2. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__init__.py +24 -2
  3. cubevis-0.5.20/cubevis/exe/__init__.py +4 -0
  4. cubevis-0.5.20/cubevis/exe/_context.py +126 -0
  5. cubevis-0.5.20/cubevis/exe/_mode.py +7 -0
  6. cubevis-0.5.20/cubevis/exe/_setting.py +4 -0
  7. cubevis-0.5.20/cubevis/exe/_task.py +230 -0
  8. cubevis-0.5.20/cubevis/private/apps/_interactiveclean.mustache +89 -0
  9. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/apps/_interactiveclean.py +17 -1435
  10. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/toolbox/__init__.py +1 -0
  11. cubevis-0.5.19/cubevis/private/apps/_interactiveclean.mustache → cubevis-0.5.20/cubevis/toolbox/_interactive_clean_ui.mustache +151 -159
  12. cubevis-0.5.20/cubevis/toolbox/_interactive_clean_ui.py +1498 -0
  13. {cubevis-0.5.19 → cubevis-0.5.20}/pyproject.toml +1 -1
  14. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-outlier/run-iclean.py +2 -1
  15. {cubevis-0.5.19 → cubevis-0.5.20}/LICENSE +0 -0
  16. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/LICENSE.rst +0 -0
  17. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/20px/fast-backward.svg +0 -0
  18. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/20px/fast-forward.svg +0 -0
  19. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/20px/step-backward.svg +0 -0
  20. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/20px/step-forward.svg +0 -0
  21. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/add-chan.png +0 -0
  22. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/add-chan.svg +0 -0
  23. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/add-cube.png +0 -0
  24. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/add-cube.svg +0 -0
  25. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/drag.png +0 -0
  26. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/drag.svg +0 -0
  27. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/mask-selected.png +0 -0
  28. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/mask.png +0 -0
  29. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/mask.svg +0 -0
  30. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm-selected.png +0 -0
  31. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm-selected.svg +0 -0
  32. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm.png +0 -0
  33. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm.svg +0 -0
  34. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/reset.png +0 -0
  35. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/reset.svg +0 -0
  36. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/sub-chan.png +0 -0
  37. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/sub-chan.svg +0 -0
  38. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/sub-cube.png +0 -0
  39. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/sub-cube.svg +0 -0
  40. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/zoom-to-fit.png +0 -0
  41. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__icons__/zoom-to-fit.svg +0 -0
  42. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__js__/bokeh-3.6.1.min.js +0 -0
  43. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__js__/bokeh-tables-3.6.1.min.js +0 -0
  44. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__js__/bokeh-widgets-3.6.1.min.js +0 -0
  45. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__js__/casalib.min.js +0 -0
  46. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/__js__/cubevisjs.min.js +0 -0
  47. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/__init__.py +0 -0
  48. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/annotations/__init__.py +0 -0
  49. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/annotations/_ev_poly_annotation.py +0 -0
  50. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/components/__init__.py +0 -0
  51. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/format/__init__.py +0 -0
  52. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/format/_time_ticks.py +0 -0
  53. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/format/_wcs_ticks.py +0 -0
  54. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/models/__init__.py +0 -0
  55. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/models/_edit_span.py +0 -0
  56. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/models/_ev_text_input.py +0 -0
  57. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/models/_tip.py +0 -0
  58. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/models/_tip_button.py +0 -0
  59. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/sources/__init__.py +0 -0
  60. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/sources/_data_pipe.py +0 -0
  61. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/sources/_image_data_source.py +0 -0
  62. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/sources/_image_pipe.py +0 -0
  63. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/sources/_spectra_data_source.py +0 -0
  64. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/sources/_updatable_data_source.py +0 -0
  65. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/__init__.py +0 -0
  66. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/_initialize.py +0 -0
  67. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/_javascript.py +0 -0
  68. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/_palette.py +0 -0
  69. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/_session.py +0 -0
  70. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-2.4.1.min.js +0 -0
  71. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-gl-2.4.1.min.js +0 -0
  72. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-tables-2.4.1.min.js +0 -0
  73. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-widgets-2.4.1.min.js +0 -0
  74. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/casaguijs-v0.0.4.0-b2.4.min.js +0 -0
  75. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/casaguijs-v0.0.5.0-b2.4.min.js +0 -0
  76. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/casaguijs-v0.0.6.0-b2.4.min.js +0 -0
  77. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/state/js/casalib-v0.0.1.min.js +0 -0
  78. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/tools/__init__.py +0 -0
  79. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/tools/_cbreset_tool.py +0 -0
  80. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/tools/_drag_tool.py +0 -0
  81. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/utils/__init__.py +0 -0
  82. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/utils/_axes_labels.py +0 -0
  83. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/bokeh/utils/_svg_icon.py +0 -0
  84. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/data/__init__.py +0 -0
  85. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/data/casaimage/__init__.py +0 -0
  86. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/_gclean.py +0 -0
  87. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/apps/__init__.py +0 -0
  88. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/apps/_createmask.py +0 -0
  89. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/apps/_createregion.py +0 -0
  90. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/apps/_plotants.py +0 -0
  91. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/apps/_plotbandpass.py +0 -0
  92. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/casashell/createmask.py +0 -0
  93. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/casashell/iclean.py +0 -0
  94. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/casatasks/__init__.py +0 -0
  95. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/casatasks/createmask.py +0 -0
  96. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/casatasks/createregion.py +0 -0
  97. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/private/casatasks/iclean.py +0 -0
  98. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/readme.rst +0 -0
  99. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/remote/__init__.py +0 -0
  100. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/remote/_gclean.py +0 -0
  101. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/remote/_local.py +0 -0
  102. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/remote/_remote_kernel.py +0 -0
  103. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/toolbox/_app_context.py +0 -0
  104. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/toolbox/_cube.py +0 -0
  105. {cubevis-0.5.19/cubevis/private/apps → cubevis-0.5.20/cubevis/toolbox}/_interactiveclean_wrappers.py +0 -0
  106. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/toolbox/_region_list.py +0 -0
  107. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_ResourceManager.py +0 -0
  108. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/__init__.py +0 -0
  109. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_contextmgrchain.py +0 -0
  110. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_conversion.py +0 -0
  111. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_copydoc.py +0 -0
  112. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_docenum.py +0 -0
  113. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_import_protected_module.py +0 -0
  114. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_jupyter.py +0 -0
  115. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_logging.py +0 -0
  116. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_pkgs.py +0 -0
  117. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_regions.py +0 -0
  118. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_static.py +0 -0
  119. {cubevis-0.5.19 → cubevis-0.5.20}/cubevis/utils/_tiles.py +0 -0
  120. {cubevis-0.5.19 → cubevis-0.5.20}/readme.rst +0 -0
  121. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/alma-many-chan/alma-many-chan.py +0 -0
  122. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/basic-websockets-demo/client.html +0 -0
  123. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/basic-websockets-demo/client.py +0 -0
  124. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/basic-websockets-demo/server.py +0 -0
  125. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/createmask-demo/run-createmask.py +0 -0
  126. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/createregion-demo/run-createregion.py +0 -0
  127. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider-spectra-done-stats.py +0 -0
  128. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider-spectra-done.py +0 -0
  129. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider-spectra.py +0 -0
  130. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider.py +0 -0
  131. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/cubemask-demo/image.py +0 -0
  132. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-demo/m100_interactive.py +0 -0
  133. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-demo/mask0-iclean.py +0 -0
  134. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-demo/run-gclean.py +0 -0
  135. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-demo/run-iclean-obj.py +0 -0
  136. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-demo/run-iclean.py +0 -0
  137. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-demo/vla-sim-jet-iclean.py +0 -0
  138. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-first-look/run-fl-cont.py +0 -0
  139. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-first-look/run-fl-line.py +0 -0
  140. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-outlier/test_outlier.txt +0 -0
  141. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/iclean-remote/iclean_remote_webserver.py +0 -0
  142. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/large-cube/run-largecube.py +0 -0
  143. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/svg-test.py +0 -0
  144. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/updatable-data-source/direct-plot.py +0 -0
  145. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/updatable-data-source/simple-update.py +0 -0
  146. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/updatable-data-source/updated-plot.py +0 -0
  147. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/uranus-demo/uranus-iclean.py +0 -0
  148. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/websocket-reconnect/client.html +0 -0
  149. {cubevis-0.5.19 → cubevis-0.5.20}/tests/manual/websocket-reconnect/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cubevis
3
- Version: 0.5.19
3
+ Version: 0.5.20
4
4
  Summary: visualization toolkit and apps for casa
5
5
  License: LGPL
6
6
  Author-email: Darrell Schiebel <darrell@schiebel.us>,Pam Harris <pharris@nrao.edu>
@@ -29,11 +29,33 @@
29
29
  used to build GUI applications for astronomy. It also contains some
30
30
  applications turn-key applications'''
31
31
 
32
- import os as __os
32
+ import os as _os
33
+ import logging as _logging
33
34
 
35
+ logger = _logging.getLogger('cubevis')
36
+ _handler = _logging.StreamHandler()
37
+ _formatter = _logging.Formatter('[%(name)s] %(levelname)s: %(message)s')
38
+ _handler.setFormatter(_formatter)
39
+ logger.addHandler(_handler)
40
+
41
+ if _os.getenv('CUBEVIS_DEBUG', '').lower() in ('1', 'true', 'yes', 'on'):
42
+ logger.setLevel(_logging.DEBUG)
43
+ else:
44
+ logger.setLevel(_logging.INFO)
34
45
 
35
46
  from .private.apps import iclean
36
47
 
48
+
49
+ def set_log_level(level):
50
+ """Set the logging level for cubevis.
51
+
52
+ Args:
53
+ level: Logging level (e.g., logging.DEBUG, logging.INFO, 'DEBUG', 'INFO')
54
+ """
55
+ if isinstance(level, str):
56
+ level = getattr(_logging, level.upper())
57
+ logger.setLevel(level)
58
+
37
59
  try:
38
60
  from .__version__ import __version__
39
61
  except ModuleNotFoundError:
@@ -52,7 +74,7 @@ def xml_interface_defs( ):
52
74
  '''
53
75
  return { }
54
76
 
55
- __mustache_interface_templates__ = { 'iclean': __os.path.join( __os.path.dirname(__file__), "private", "casashell", "iclean.mustache" ) }
77
+ __mustache_interface_templates__ = { 'iclean': _os.path.join( _os.path.dirname(__file__), "private", "casashell", "iclean.mustache" ) }
56
78
  def mustache_interface_templates( ):
57
79
  '''This provides a list of mustache files provided by cubevis. It may eventually allow
58
80
  casashell to generate all of its bindings at startup time. This would allow casashell
@@ -0,0 +1,4 @@
1
+ from ._mode import Mode
2
+ from ._setting import Setting
3
+ from ._task import Task
4
+ from ._context import Context
@@ -0,0 +1,126 @@
1
+ import asyncio
2
+ import logging
3
+ import threading
4
+ from typing import Optional, Union, Any, Callable
5
+ from concurrent.futures import ThreadPoolExecutor, Future
6
+
7
+ from . import Mode, Task
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class Context:
12
+ """Unified interface for different execution contexts with lifecycle management."""
13
+
14
+ def __init__(self, mode: Mode, executor: Optional[ThreadPoolExecutor] = None):
15
+ logger.debug( f"\tContext::__init__({mode},{executor} ): {id(self)}" )
16
+ self.mode = mode
17
+ self.executor = executor or ThreadPoolExecutor(max_workers=4)
18
+ self._running_tasks = []
19
+ self._stop_events = {}
20
+
21
+ def create_stop_condition( self, task_id: Optional[str] = None,
22
+ prefer_asyncio: bool = False ):
23
+ """Create appropriate stop condition for the execution mode."""
24
+ logger.debug( f"\tContext::create_stop_condition({task_id}): {id(self)}" )
25
+ if task_id is None:
26
+ task_id = f"task_{len(self._stop_events)}"
27
+
28
+ if prefer_asyncio or self.mode in [ExecutionMode.ASYNC_RUN, ExecutionMode.ASYNC_TASK]:
29
+ # For async contexts, use asyncio primitives
30
+ event = asyncio.Event()
31
+ else:
32
+ # ====>> if self.mode in [Mode.SYNC, Mode.THREAD]: <<====
33
+ # For threading contexts, always use threading primitives
34
+ condition = threading.Condition()
35
+ condition.stop_requested = False
36
+
37
+ def stop_task():
38
+ with condition:
39
+ condition.stop_requested = True
40
+ condition.notify_all()
41
+
42
+ condition.stop_task = stop_task
43
+ event = condition
44
+
45
+ self._stop_events[task_id] = event
46
+ return event, task_id
47
+
48
+ def stop_task(self, task_id: str):
49
+ """Signal a task to stop."""
50
+ if task_id in self._stop_events:
51
+ condition_or_event = self._stop_events[task_id]
52
+ if isinstance(condition_or_event, threading.Condition):
53
+ logger.debug( f"\tContext::stop_task({task_id}): {id(self)} - condition" )
54
+ condition_or_event.stop_task()
55
+ elif hasattr(condition_or_event, 'set'): # Both threading.Event and asyncio.Event have set()
56
+ logger.debug( f"\tContext::stop_task({task_id}): {id(self)} - event" )
57
+ condition_or_event.set()
58
+ else:
59
+ # Fallback for other types
60
+ logger.debug(f"Warning: Unknown stop condition type: {type(condition_or_event)}")
61
+
62
+ def stop_all_tasks(self):
63
+ """Signal all tasks to stop."""
64
+ logger.debug( f"\tContext::stop_all_task( ): {id(self)}" )
65
+ for condition_or_event in self._stop_events.values():
66
+ if isinstance(condition_or_event, threading.Condition):
67
+ condition_or_event.stop_task()
68
+ elif hasattr(condition_or_event, 'set'): # Both threading.Event and asyncio.Event have set()
69
+ condition_or_event.set()
70
+ else:
71
+ logger.debug(f"Warning: Unknown stop condition type: {type(condition_or_event)}")
72
+
73
+ def execute(self, task: Task, task_id: Optional[str] = None) -> Union[Any, Future, asyncio.Task]:
74
+ """Execute Bokeh task according to the configured mode."""
75
+
76
+ if self.mode == Mode.SYNC:
77
+ logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - sync" )
78
+ return task.run_sync()
79
+
80
+ elif self.mode == Mode.ASYNC_RUN:
81
+ logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - async" )
82
+ if asyncio.iscoroutinefunction(task.server_func):
83
+ return asyncio.run(task.run_async())
84
+ else:
85
+ async def async_wrapper():
86
+ return await task.run_async()
87
+ return asyncio.run(async_wrapper())
88
+
89
+ elif self.mode == Mode.ASYNC_TASK:
90
+ logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - task" )
91
+ try:
92
+ loop = asyncio.get_running_loop()
93
+ async_task = loop.create_task(task.run_async())
94
+ if task_id:
95
+ self._running_tasks.append((task_id, async_task))
96
+ return async_task
97
+ except RuntimeError:
98
+ raise RuntimeError("ASYNC_TASK mode requires an active event loop")
99
+
100
+ elif self.mode == Mode.THREAD:
101
+ logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - thread" )
102
+ future = self.executor.submit(task.run_sync)
103
+ if task_id:
104
+ self._running_tasks.append((task_id, future))
105
+ return future
106
+
107
+ async def execute_async(self, task: Task, task_id: Optional[str] = None) -> Any:
108
+ """Async version that always returns the result."""
109
+
110
+ if self.mode == Mode.SYNC:
111
+ logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - sync" )
112
+ loop = asyncio.get_running_loop()
113
+ return await loop.run_in_executor(self.executor, task.run_sync)
114
+
115
+ elif self.mode == Mode.ASYNC_RUN:
116
+ logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - async" )
117
+ return await task.run_async()
118
+
119
+ elif self.mode == Mode.ASYNC_TASK:
120
+ logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - task" )
121
+ return await task.run_async()
122
+
123
+ elif self.mode == Mode.THREAD:
124
+ logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - thread" )
125
+ loop = asyncio.get_running_loop()
126
+ return await loop.run_in_executor(self.executor, task.run_sync)
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+ class Mode(Enum):
4
+ SYNC = "sync"
5
+ ASYNC_RUN = "async_run"
6
+ ASYNC_TASK = "async_task"
7
+ THREAD = "thread"
@@ -0,0 +1,4 @@
1
+ from enum import Enum
2
+
3
+ class Setting(Enum):
4
+ CLI = "python_cli"
@@ -0,0 +1,230 @@
1
+ import logging
2
+ import asyncio
3
+ import threading
4
+ from typing import Callable, Optional, Union, Any
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class Task:
9
+ """A long-running Bokeh backend task that runs until completion or stop signal.
10
+ """
11
+
12
+ def __init__(self,
13
+ server_func: Callable,
14
+ *args,
15
+ stop_condition: Optional[Union[threading.Event, asyncio.Event, threading.Condition, Callable[[], bool]]] = None,
16
+ **kwargs):
17
+ """
18
+ Args:
19
+ server_func: Function that starts the Bokeh server (sync or async)
20
+ stop_condition: Event or callable that signals when to stop
21
+ - threading.Event: for thread-based execution
22
+ - asyncio.Event: for async execution
23
+ - threading.Condition: for complex condition-based stopping (most flexible)
24
+ - Callable[[], bool]: function returning True when should stop
25
+ - None: runs until server_func returns naturally
26
+ """
27
+ logger.debug( f"\tTask::__init__({server_func},{args},{stop_condition},{kwargs}): {id(self)}" )
28
+ self.server_func = server_func
29
+ self.args = args
30
+ self.kwargs = kwargs
31
+ self.stop_condition = stop_condition
32
+ self._result = None
33
+ self._exception = None
34
+
35
+ def _convert_asyncio_event_to_threading(self, asyncio_event):
36
+ """Convert asyncio.Event to threading.Event for sync contexts."""
37
+ threading_event = threading.Event()
38
+ logger.debug( f"\tTask::_convert_asyncio_event_to_threading({asyncio_event}): {id(self)}" )
39
+
40
+ # Create a bridge between asyncio.Event and threading.Event
41
+ # We'll use a background thread with its own event loop to monitor the asyncio event
42
+ def bridge_events():
43
+ # Create a new event loop for this thread
44
+ try:
45
+ loop = asyncio.new_event_loop()
46
+ asyncio.set_event_loop(loop)
47
+
48
+ async def wait_and_set():
49
+ await asyncio_event.wait()
50
+ threading_event.set()
51
+
52
+ loop.run_until_complete(wait_and_set())
53
+ except Exception as e:
54
+ # If bridging fails, fall back to a simple timeout approach
55
+ print(f"Warning: Event bridging failed: {e}")
56
+ time.sleep(0.1) # Small delay to prevent tight loop
57
+ threading_event.set() # Set it anyway to unblock
58
+ finally:
59
+ try:
60
+ loop.close()
61
+ except:
62
+ pass
63
+
64
+ # Start the bridge in a daemon thread
65
+ bridge_thread = threading.Thread(target=bridge_events, daemon=True)
66
+ bridge_thread.start()
67
+
68
+ return threading_event
69
+
70
+ def run_sync(self) -> Any:
71
+ """Run synchronously until completion or stop signal."""
72
+ if self.stop_condition is None:
73
+ # Handle async functions in sync context
74
+ 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))
77
+ else:
78
+ logger.debug( f"\tTask::run_sync( ): {id(self)} - no stop condition {self.server_func}" )
79
+ return self.server_func(*self.args, **self.kwargs)
80
+
81
+ # Handle asyncio.Event in sync context by converting to threading.Event
82
+ stop_condition = self.stop_condition
83
+ if isinstance(self.stop_condition, asyncio.Event):
84
+ logger.debug( f"\tTask::run_sync( ): {id(self)} - asyncio bridge {self.server_func}" )
85
+ # Convert asyncio.Event to threading.Event for sync execution
86
+ stop_condition = self._convert_asyncio_event_to_threading(self.stop_condition)
87
+
88
+ if callable(stop_condition):
89
+ # Poll-based stopping
90
+ logger.debug( f"\tTask::run_sync( ): {id(self)} - polling {self.server_func}" )
91
+ while not stop_condition():
92
+ try:
93
+ if asyncio.iscoroutinefunction(self.server_func):
94
+ result = asyncio.run(self.server_func(*self.args, **self.kwargs))
95
+ else:
96
+ result = self.server_func(*self.args, **self.kwargs)
97
+ if result is not None: # Server function completed
98
+ return result
99
+ except Exception as e:
100
+ self._exception = e
101
+ raise
102
+
103
+ elif isinstance(stop_condition, threading.Event):
104
+ # Event-based stopping for threads
105
+ logger.debug( f"\tTask::run_sync( ): {id(self)} - threading event {self.server_func}" )
106
+ def run_with_check():
107
+ while not stop_condition.is_set():
108
+ try:
109
+ if asyncio.iscoroutinefunction(self.server_func):
110
+ result = asyncio.run(self.server_func(*self.args, **self.kwargs))
111
+ else:
112
+ result = self.server_func(*self.args, **self.kwargs)
113
+ if result is not None:
114
+ return result
115
+ except Exception as e:
116
+ self._exception = e
117
+ raise
118
+ return run_with_check()
119
+
120
+ elif isinstance(stop_condition, threading.Condition):
121
+ logger.debug( f"\tTask::run_sync( ): {id(self)} - threading condition {self.server_func}" )
122
+ # Condition-based stopping - most flexible and efficient
123
+ def run_with_condition():
124
+ with stop_condition:
125
+ 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
+ try:
130
+ # Pass the condition to the server function if it accepts it
131
+ import inspect
132
+ sig = inspect.signature(self.server_func)
133
+ if 'stop_condition' in sig.parameters:
134
+ if asyncio.iscoroutinefunction(self.server_func):
135
+ result = asyncio.run(self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs))
136
+ else:
137
+ result = self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs)
138
+ else:
139
+ if asyncio.iscoroutinefunction(self.server_func):
140
+ result = asyncio.run(self.server_func(*self.args, **self.kwargs))
141
+ else:
142
+ result = self.server_func(*self.args, **self.kwargs)
143
+
144
+ if result is not None:
145
+ return result
146
+
147
+ # If function returns None, wait for condition change
148
+ stop_condition.wait()
149
+
150
+ except Exception as e:
151
+ self._exception = e
152
+ raise
153
+
154
+ return run_with_condition()
155
+
156
+ else:
157
+ raise ValueError(f"Invalid stop_condition ({type(stop_condition)}) for sync execution")
158
+
159
+ async def run_async(self) -> Any:
160
+ """Run asynchronously until completion or stop signal."""
161
+ if asyncio.iscoroutinefunction(self.server_func):
162
+ if self.stop_condition is None:
163
+ logger.debug( f"\tTask::run_async( ): {id(self)} - no stop condition {self.server_func}" )
164
+ return await self.server_func(*self.args, **self.kwargs)
165
+
166
+ if callable(self.stop_condition):
167
+ # Poll-based stopping
168
+ logger.debug( f"\tTask::run_async( ): {id(self)} - polling {self.server_func}" )
169
+ while not self.stop_condition():
170
+ try:
171
+ # Run server function as task so we can check stop condition
172
+ task = asyncio.create_task(self.server_func(*self.args, **self.kwargs))
173
+
174
+ # Check stop condition periodically
175
+ while not task.done() and not self.stop_condition():
176
+ await asyncio.sleep(0.1)
177
+
178
+ if self.stop_condition():
179
+ task.cancel()
180
+ try:
181
+ await task
182
+ except asyncio.CancelledError:
183
+ pass
184
+ return None
185
+
186
+ return await task
187
+ except Exception as e:
188
+ self._exception = e
189
+ raise
190
+
191
+ elif isinstance(self.stop_condition, asyncio.Event):
192
+ logger.debug( f"\tTask::run_async( ): {id(self)} - asyncio event {self.server_func}" )
193
+ # Event-based stopping for async
194
+ server_task = asyncio.create_task(self.server_func(*self.args, **self.kwargs))
195
+ stop_task = asyncio.create_task(self.stop_condition.wait())
196
+
197
+ done, pending = await asyncio.wait(
198
+ [server_task, stop_task],
199
+ return_when=asyncio.FIRST_COMPLETED
200
+ )
201
+
202
+ # Cancel remaining tasks
203
+ for task in pending:
204
+ task.cancel()
205
+ try:
206
+ await task
207
+ except asyncio.CancelledError:
208
+ pass
209
+
210
+ if server_task in done:
211
+ return server_task.result()
212
+ else:
213
+ return None # Stopped by event
214
+
215
+ elif isinstance(self.stop_condition, threading.Condition):
216
+ logger.debug( f"\tTask::run_async( ): {id(self)} - threading condition {self.server_func}" )
217
+ # For async context with threading.Condition, we need to run in executor
218
+ # since threading.Condition is synchronous
219
+ loop = asyncio.get_event_loop()
220
+ return await loop.run_in_executor(None, self.run_sync)
221
+
222
+ else:
223
+ raise ValueError("Invalid stop_condition for async execution")
224
+
225
+ else:
226
+ logger.debug( f"\tTask::run_async( ): {id(self)} - until completion" )
227
+ # Sync function in async context - run in thread pool
228
+ loop = asyncio.get_event_loop()
229
+ return await loop.run_in_executor(None, self.run_sync)
230
+
@@ -0,0 +1,89 @@
1
+ ########################################################################
2
+ #
3
+ #TASK XML> tclean -argfilter=interactive,fullsummary -argfilter:initParams=vis,imagename
4
+ # Copyright (C) 2022,2023,2024,2025
5
+ # Associated Universities, Inc. Washington DC, USA.
6
+ #
7
+ # This script is free software; you can redistribute it and/or modify it
8
+ # under the terms of the GNU Library General Public License as published by
9
+ # the Free Software Foundation; either version 2 of the License, or (at your
10
+ # option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful, but WITHOUT
13
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
15
+ # License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Library General Public License
18
+ # along with this library; if not, write to the Free Software Foundation,
19
+ # Inc., 675 Massachusetts Ave, Cambridge, MA 02139, USA.
20
+ #
21
+ # Correspondence concerning AIPS++ should be adressed as follows:
22
+ # Internet email: casa-feedback@nrao.edu.
23
+ # Postal address: AIPS++ Project Office
24
+ # National Radio Astronomy Observatory
25
+ # 520 Edgemont Road
26
+ # Charlottesville, VA 22903-2475 USA
27
+ #
28
+ ########################################################################
29
+ '''implementation of the ``InteractiveClean`` application for interactive control
30
+ of tclean'''
31
+
32
+ from pprint import pprint
33
+
34
+ import sys
35
+ from os.path import exists
36
+ from casatasks.private.imagerhelpers.input_parameters import ImagerParameters
37
+
38
+ from cubevis.utils import find_pkg, load_pkg
39
+ from cubevis.toolbox import InteractiveCleanUI
40
+ from cubevis import exe
41
+
42
+ class InteractiveClean:
43
+ '''InteractiveClean(...) implements interactive clean using Bokeh
44
+ {{docstring}}
45
+ '''
46
+
47
+ def __init__( self, vis, imagename{{# initParams}}, {{name}}={{default}}{{/ initParams}}, iclean_backend="PROD" ):
48
+
49
+
50
+ ###
51
+ ### iclean_backend can be used to select alternate backends for interactive clean. This could be used
52
+ ### to enable a backend with extended features or it could be used to select a stub backend designed
53
+ ### for testing
54
+ ###
55
+ mod_specs = None
56
+ self._gclean_module = None
57
+ if iclean_backend == 'PROD':
58
+ mod_specs = find_pkg( "casatasks.private.imagerhelpers._gclean" )
59
+ else:
60
+ mod_specs = find_pkg( f"_gclean_{iclean_backend}" )
61
+
62
+ if mod_specs:
63
+ self._gclean_module = load_pkg(mod_specs[0])
64
+ else:
65
+ raise ImportError(f"Could not locate {iclean_backend} kind of iclean backend")
66
+
67
+ self._args = {{forwDict}}
68
+ self._gclean = self._gclean_module.gclean( **self._args )
69
+ self._gclean_paths = self._gclean.image_products( )
70
+ #self._residual_path = self._residual_path(self._clean['gclean'],imid)
71
+ #self._mask_path = self._mask_path(self._clean['gclean'],imid)
72
+
73
+ self._ui = InteractiveCleanUI(self._gclean, self._args)
74
+
75
+
76
+ def __call__( self ):
77
+ '''Display GUI and process events until the user stops the application.
78
+
79
+ Example:
80
+ Create ``iclean`` object and display::
81
+
82
+ print( "Result: %s" %
83
+ iclean( vis='refim_point_withline.ms', imagename='test', imsize=512,
84
+ cell='12.0arcsec', specmode='cube',
85
+ interpolation='nearest', ... )( ) )
86
+ '''
87
+ context = exe.Context( exe.Mode.SYNC )
88
+ exec_task = self._ui( exe.Setting.CLI, context, "interactive-clean" )
89
+ return context.execute( exec_task, "interactive-clean" )