cubevis 1.0.0__py3-none-any.whl → 1.0.3__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.
Files changed (47) hide show
  1. cubevis/__init__.py +13 -24
  2. cubevis/__js__/bokeh-3.6/cubevisjs.min.js +64 -0
  3. cubevis/__js__/{cubevisjs.min.js → bokeh-3.7/cubevisjs.min.js} +3 -3
  4. cubevis/__js__/bokeh-3.8/cubevisjs.min.js +64 -0
  5. cubevis/__js__/casalib.min.js +1 -1
  6. cubevis/__version__.py +1 -1
  7. cubevis/bokeh/__init__.py +16 -0
  8. cubevis/bokeh/annotations/_ev_poly_annotation.py +2 -1
  9. cubevis/bokeh/format/_wcs_ticks.py +6 -2
  10. cubevis/bokeh/models/__init__.py +1 -0
  11. cubevis/bokeh/models/_showable.py +352 -0
  12. cubevis/bokeh/models/_tip.py +2 -1
  13. cubevis/bokeh/models/_tip_button.py +2 -3
  14. cubevis/bokeh/sources/_data_pipe.py +6 -2
  15. cubevis/bokeh/sources/_image_data_source.py +6 -2
  16. cubevis/bokeh/sources/_image_pipe.py +4 -1
  17. cubevis/bokeh/sources/_spectra_data_source.py +6 -3
  18. cubevis/bokeh/sources/_updatable_data_source.py +6 -2
  19. cubevis/bokeh/state/__init__.py +4 -3
  20. cubevis/bokeh/state/_current.py +34 -0
  21. cubevis/bokeh/state/_initialize.py +282 -116
  22. cubevis/bokeh/state/_javascript.py +95 -21
  23. cubevis/bokeh/tools/_cbreset_tool.py +2 -1
  24. cubevis/bokeh/tools/_drag_tool.py +2 -1
  25. cubevis/bokeh/utils/__init__.py +0 -1
  26. cubevis/exe/_setting.py +1 -0
  27. cubevis/private/apps/__init__.py +4 -2
  28. cubevis/private/apps/_interactiveclean.mustache +6 -2
  29. cubevis/private/apps/_interactiveclean.py +6 -2
  30. cubevis/private/apps/_interactivecleanjpy.mustache +112 -0
  31. cubevis/private/apps/_interactivecleanjpy.py +1874 -0
  32. cubevis/private/casatasks/__init__.py +1 -0
  33. cubevis/private/casatasks/icleanjpy.py +1831 -0
  34. cubevis/toolbox/_app_context.py +5 -9
  35. cubevis/toolbox/_cube.py +6 -2
  36. cubevis/toolbox/_interactive_clean_ui.mustache +20 -31
  37. cubevis/toolbox/_interactive_clean_ui.py +20 -31
  38. cubevis/utils/__init__.py +120 -18
  39. cubevis/utils/_git.py +36 -0
  40. cubevis/utils/_jupyter.py +12 -0
  41. {cubevis-1.0.0.dist-info → cubevis-1.0.3.dist-info}/METADATA +3 -3
  42. {cubevis-1.0.0.dist-info → cubevis-1.0.3.dist-info}/RECORD +44 -39
  43. cubevis/__js__/bokeh-3.6.1.min.js +0 -728
  44. cubevis/__js__/bokeh-tables-3.6.1.min.js +0 -119
  45. cubevis/__js__/bokeh-widgets-3.6.1.min.js +0 -141
  46. {cubevis-1.0.0.dist-info → cubevis-1.0.3.dist-info}/WHEEL +0 -0
  47. {cubevis-1.0.0.dist-info → cubevis-1.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  ########################################################################
2
2
  #
3
- # Copyright (C) 2024
3
+ # Copyright (C) 2024,2025
4
4
  # Associated Universities, Inc. Washington DC, USA.
5
5
  #
6
6
  # This script is free software; you can redistribute it and/or modify it
@@ -25,13 +25,14 @@
25
25
  # Charlottesville, VA 22903-2475 USA
26
26
  #
27
27
  ########################################################################
28
- from cubevis.bokeh.state import initialize_bokeh
29
28
  from tempfile import TemporaryDirectory
30
29
  from bokeh.io import output_file
31
30
  from os.path import join
32
31
  import unicodedata
33
32
  import re
34
33
 
34
+ from ..utils import is_interactive_jupyter
35
+
35
36
  class AppContext:
36
37
 
37
38
  def _slugify(self, value, allow_unicode=False):
@@ -53,18 +54,13 @@ class AppContext:
53
54
 
54
55
  def __init__( self, title, prefix=None, init_bokeh=True ):
55
56
 
56
- ###
57
- ### Setup up Bokeh paths, inject cubevis libraries into Bokeh HTML output
58
- ###
59
- if init_bokeh:
60
- initialize_bokeh( )
61
-
62
57
  if prefix is None:
63
58
  ## create a prefix from the title
64
59
  prefix = self._slugify(title)[:10]
65
60
  self.__workdir = TemporaryDirectory(prefix=prefix)
66
61
  self.__htmlpath = join( self.__workdir.name, f'''{self._slugify(title)}.html''' )
67
- output_file( self.__htmlpath, title=title )
62
+ if not is_interactive_jupyter( ):
63
+ output_file( self.__htmlpath, title=title )
68
64
 
69
65
  def __del__( self ):
70
66
  ### remove work directory and its contents
cubevis/toolbox/_cube.py CHANGED
@@ -100,7 +100,7 @@ class CubeMask:
100
100
  self.COUNT = 1
101
101
  self.CCOUNT = 1
102
102
 
103
- self._is_notebook = is_interactive_jupyter()
103
+ ##self._is_notebook = is_interactive_jupyter()
104
104
  #self._color = '#00FF00' # anti-green user feedback (issue #40 2024-05-02 13:08:32)
105
105
  self._region_style=dict( fill_alpha=0, hover_fill_alpha=0.3,
106
106
  fill_color='white', hover_fill_color='white',
@@ -1505,7 +1505,11 @@ class CubeMask:
1505
1505
  function done_close_window( msg ) {
1506
1506
  if ( msg.result === 'stopped' ) {""" +
1507
1507
  # Don't close tab if running in a jupyter notebook
1508
- ("""console.log("Running in jupyter notebook. Not closing window.")""" if self._is_notebook else
1508
+ ##("""console.log("Running in jupyter notebook. Not closing window.")""" if self._is_notebook else
1509
+ ## """console.log("Running from script/terminal. Closing window.")
1510
+ ## window.close()"""
1511
+ ##) +
1512
+ ("""console.log("Running in jupyter notebook. Not closing window.")""" if is_interactive_jupyter( ) else
1509
1513
  """console.log("Running from script/terminal. Closing window.")
1510
1514
  window.close()"""
1511
1515
  ) +
@@ -29,15 +29,6 @@
29
29
  '''implementation of the ``InteractiveCleanUI`` application for interactive control
30
30
  of tclean'''
31
31
 
32
- ###
33
- ### Useful for debugging
34
- ###
35
- ###from cubevis.bokeh.state import initialize_bokeh
36
- ###initialize_bokeh( bokehjs_subst=".../bokeh-3.2.2.js" )
37
- ###
38
-
39
- from pprint import pprint
40
-
41
32
  import os
42
33
  import sys
43
34
  import copy
@@ -137,8 +128,8 @@ class InteractiveCleanUI:
137
128
  ports.append( imdetails['gui']['cube']._pipe['control'].address[1] )
138
129
 
139
130
  # Also forward http port if serving webpage
140
- if not self._is_notebook:
141
- ports.append(self._http_port)
131
+ #if not self._is_notebook:
132
+ # ports.append(self._http_port)
142
133
 
143
134
  cmd = 'ssh'
144
135
  for port in ports:
@@ -187,7 +178,7 @@ class InteractiveCleanUI:
187
178
  ###
188
179
  ### whether or not the session is being run from a jupyter notebook or script
189
180
  ###
190
- self._is_notebook = is_interactive_jupyter()
181
+ #self._is_notebook = is_interactive_jupyter()
191
182
 
192
183
  ##
193
184
  ## the http port for serving GUI in webpage if not running in script
@@ -520,7 +511,7 @@ class InteractiveCleanUI:
520
511
  'flux_axis': flux_axis
521
512
  }
522
513
 
523
- def _launch_gui( self ):
514
+ def _build_bokeh( self ):
524
515
  '''create and show GUI
525
516
  '''
526
517
  ###
@@ -625,7 +616,7 @@ class InteractiveCleanUI:
625
616
  icw = SharedWidgets( )
626
617
  toolbars = [ ]
627
618
  for imid, imdetails in self._clean_targets.items( ):
628
- imdetails['gui']['stats'] = imdetails['gui']['cube'].statistics( )
619
+ imdetails['gui']['stats'] = imdetails['gui']['cube'].statistics( name=f"{imid} stats" )
629
620
  imdetails['image-channels'] = imdetails['gui']['cube'].shape( )[3]
630
621
 
631
622
  status_line = imdetails['gui']['stopcode'] = imdetails['gui']['cube'].status_text( "<p>initial residual image</p>" if imdetails['image-channels'] > 1 else "<p>initial <b>single-channel</b> residual image</p>", width=230, reuse=status_line )
@@ -969,15 +960,15 @@ class InteractiveCleanUI:
969
960
  itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
970
961
 
971
962
  # Change display type depending on runtime environment
972
- if self._is_notebook:
973
- output_notebook()
974
- else:
975
- ### Directory is created when an HTTP server is running
976
- ### (MAX)
977
- ### output_file(self._imagename+'_webpage/index.html')
978
- pass
963
+ #if self._is_notebook:
964
+ # output_notebook()
965
+ #else:
966
+ # ### Directory is created when an HTTP server is running
967
+ # ### (MAX)
968
+ ### # output_file(self._imagename+'_webpage/index.html')
969
+ # pass
979
970
 
980
- show(self._fig['layout'])
971
+ return self._fig['layout']
981
972
 
982
973
  def _create_colormap_adjust( self, imdetails ):
983
974
  palette = imdetails['gui']['cube'].palette( reuse=self._cube_palette )
@@ -1015,7 +1006,7 @@ class InteractiveCleanUI:
1015
1006
  title='Colormap' ),
1016
1007
  TabPanel( child=column( *imdetails['gui']['stats'] ),
1017
1008
  title='Statistics' ) ] + imdetails['gui']['auto-masking-panel'],
1018
- width=500, sizing_mode='stretch_height', tabs_location='below' )
1009
+ sizing_mode='stretch_height', tabs_location='below' )
1019
1010
 
1020
1011
  if not hasattr(self,'_image_control_tab_groups'):
1021
1012
  self._image_control_tab_groups = { }
@@ -1046,7 +1037,7 @@ class InteractiveCleanUI:
1046
1037
  height_policy='max', width_policy='max' ),
1047
1038
  height_policy='max', width_policy='max' ), title=imid )
1048
1039
 
1049
- def __call__( self, setting, exec_context, id=None ):
1040
+ def __call__( self, exec_context, task_id=None ):
1050
1041
  '''Display GUI and process events until the user stops the application.
1051
1042
 
1052
1043
  Example:
@@ -1075,13 +1066,12 @@ class InteractiveCleanUI:
1075
1066
 
1076
1067
  ###
1077
1068
  ### cubevis.exe subpkg supports adding a stop condition to allow for interrupt,
1078
- ### but it is not needed for synchronous execution
1069
+ ### but it is not needed for synchronous execution, e.g.
1070
+ ### self._exec['stop-condition'], self._exec['id'] = exec_context.create_stop_condition(task_id)
1079
1071
  ###
1080
1072
  self._exec = { 'stop-condition': None }
1081
- #self._exec['stop-condition'], self._exec['id'] = exec_context.create_stop_condition(id)
1082
1073
 
1083
- return exe.Task( self._task_server )
1084
- # , stop_condition=self._exec['stop-condition'] )
1074
+ return self._build_bokeh( ), exe.Task( self._task_server )
1085
1075
 
1086
1076
  async def _task_server( self ):
1087
1077
  """Wrapper for your serve() context manager"""
@@ -1131,8 +1121,6 @@ class InteractiveCleanUI:
1131
1121
 
1132
1122
  httpd.serve_forever()
1133
1123
 
1134
- self._launch_gui( )
1135
-
1136
1124
  async with CMC( *( [ ctx for img in self._clean_targets.keys( ) for ctx in
1137
1125
  [
1138
1126
  self._clean_targets[img]['gui']['cube'].serve(self.__stop),
@@ -1296,6 +1284,7 @@ class InteractiveCleanUI:
1296
1284
  ### -- document is used storing state --
1297
1285
  ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1298
1286
  'initialize': '''if ( ! document._casa_initialized ) {
1287
+ console.log(`casalib version: ${casalib.version}`)
1299
1288
  document._casa_image_name = initial_image
1300
1289
  document._casa_initialized = true
1301
1290
  document._casa_window_closed = false
@@ -1303,7 +1292,7 @@ class InteractiveCleanUI:
1303
1292
  function (e) {
1304
1293
  // if the window is already closed this message is never
1305
1294
  // delivered (unless interactive clean is called again then
1306
- // the event shows up in the newly created control pipe
1295
+ // the event shows up in the newly created control pipe)
1307
1296
  if ( document._casa_window_closed == false ) {
1308
1297
  ctrl_pipe.send( ids['stop'],
1309
1298
  { action: 'stop', value: { } },
@@ -28,15 +28,6 @@
28
28
  '''implementation of the ``InteractiveCleanUI`` application for interactive control
29
29
  of tclean'''
30
30
 
31
- ###
32
- ### Useful for debugging
33
- ###
34
- ###from cubevis.bokeh.state import initialize_bokeh
35
- ###initialize_bokeh( bokehjs_subst=".../bokeh-3.2.2.js" )
36
- ###
37
-
38
- from pprint import pprint
39
-
40
31
  import os
41
32
  import sys
42
33
  import copy
@@ -136,8 +127,8 @@ class InteractiveCleanUI:
136
127
  ports.append( imdetails['gui']['cube']._pipe['control'].address[1] )
137
128
 
138
129
  # Also forward http port if serving webpage
139
- if not self._is_notebook:
140
- ports.append(self._http_port)
130
+ #if not self._is_notebook:
131
+ # ports.append(self._http_port)
141
132
 
142
133
  cmd = 'ssh'
143
134
  for port in ports:
@@ -186,7 +177,7 @@ class InteractiveCleanUI:
186
177
  ###
187
178
  ### whether or not the session is being run from a jupyter notebook or script
188
179
  ###
189
- self._is_notebook = is_interactive_jupyter()
180
+ #self._is_notebook = is_interactive_jupyter()
190
181
 
191
182
  ##
192
183
  ## the http port for serving GUI in webpage if not running in script
@@ -519,7 +510,7 @@ class InteractiveCleanUI:
519
510
  'flux_axis': flux_axis
520
511
  }
521
512
 
522
- def _launch_gui( self ):
513
+ def _build_bokeh( self ):
523
514
  '''create and show GUI
524
515
  '''
525
516
  ###
@@ -624,7 +615,7 @@ class InteractiveCleanUI:
624
615
  icw = SharedWidgets( )
625
616
  toolbars = [ ]
626
617
  for imid, imdetails in self._clean_targets.items( ):
627
- imdetails['gui']['stats'] = imdetails['gui']['cube'].statistics( )
618
+ imdetails['gui']['stats'] = imdetails['gui']['cube'].statistics( name=f"{imid} stats" )
628
619
  imdetails['image-channels'] = imdetails['gui']['cube'].shape( )[3]
629
620
 
630
621
  status_line = imdetails['gui']['stopcode'] = imdetails['gui']['cube'].status_text( "<p>initial residual image</p>" if imdetails['image-channels'] > 1 else "<p>initial <b>single-channel</b> residual image</p>", width=230, reuse=status_line )
@@ -968,15 +959,15 @@ class InteractiveCleanUI:
968
959
  itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
969
960
 
970
961
  # Change display type depending on runtime environment
971
- if self._is_notebook:
972
- output_notebook()
973
- else:
974
- ### Directory is created when an HTTP server is running
975
- ### (MAX)
976
- ### output_file(self._imagename+'_webpage/index.html')
977
- pass
962
+ #if self._is_notebook:
963
+ # output_notebook()
964
+ #else:
965
+ # ### Directory is created when an HTTP server is running
966
+ # ### (MAX)
967
+ ### # output_file(self._imagename+'_webpage/index.html')
968
+ # pass
978
969
 
979
- show(self._fig['layout'])
970
+ return self._fig['layout']
980
971
 
981
972
  def _create_colormap_adjust( self, imdetails ):
982
973
  palette = imdetails['gui']['cube'].palette( reuse=self._cube_palette )
@@ -1014,7 +1005,7 @@ class InteractiveCleanUI:
1014
1005
  title='Colormap' ),
1015
1006
  TabPanel( child=column( *imdetails['gui']['stats'] ),
1016
1007
  title='Statistics' ) ] + imdetails['gui']['auto-masking-panel'],
1017
- width=500, sizing_mode='stretch_height', tabs_location='below' )
1008
+ sizing_mode='stretch_height', tabs_location='below' )
1018
1009
 
1019
1010
  if not hasattr(self,'_image_control_tab_groups'):
1020
1011
  self._image_control_tab_groups = { }
@@ -1045,7 +1036,7 @@ class InteractiveCleanUI:
1045
1036
  height_policy='max', width_policy='max' ),
1046
1037
  height_policy='max', width_policy='max' ), title=imid )
1047
1038
 
1048
- def __call__( self, setting, exec_context, id=None ):
1039
+ def __call__( self, exec_context, task_id=None ):
1049
1040
  '''Display GUI and process events until the user stops the application.
1050
1041
 
1051
1042
  Example:
@@ -1074,13 +1065,12 @@ class InteractiveCleanUI:
1074
1065
 
1075
1066
  ###
1076
1067
  ### cubevis.exe subpkg supports adding a stop condition to allow for interrupt,
1077
- ### but it is not needed for synchronous execution
1068
+ ### but it is not needed for synchronous execution, e.g.
1069
+ ### self._exec['stop-condition'], self._exec['id'] = exec_context.create_stop_condition(task_id)
1078
1070
  ###
1079
1071
  self._exec = { 'stop-condition': None }
1080
- #self._exec['stop-condition'], self._exec['id'] = exec_context.create_stop_condition(id)
1081
1072
 
1082
- return exe.Task( self._task_server )
1083
- # , stop_condition=self._exec['stop-condition'] )
1073
+ return self._build_bokeh( ), exe.Task( self._task_server )
1084
1074
 
1085
1075
  async def _task_server( self ):
1086
1076
  """Wrapper for your serve() context manager"""
@@ -1130,8 +1120,6 @@ class InteractiveCleanUI:
1130
1120
 
1131
1121
  httpd.serve_forever()
1132
1122
 
1133
- self._launch_gui( )
1134
-
1135
1123
  async with CMC( *( [ ctx for img in self._clean_targets.keys( ) for ctx in
1136
1124
  [
1137
1125
  self._clean_targets[img]['gui']['cube'].serve(self.__stop),
@@ -1295,6 +1283,7 @@ class InteractiveCleanUI:
1295
1283
  ### -- document is used storing state --
1296
1284
  ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1297
1285
  'initialize': '''if ( ! document._casa_initialized ) {
1286
+ console.log(`casalib version: ${casalib.version}`)
1298
1287
  document._casa_image_name = initial_image
1299
1288
  document._casa_initialized = true
1300
1289
  document._casa_window_closed = false
@@ -1302,7 +1291,7 @@ class InteractiveCleanUI:
1302
1291
  function (e) {
1303
1292
  // if the window is already closed this message is never
1304
1293
  // delivered (unless interactive clean is called again then
1305
- // the event shows up in the newly created control pipe
1294
+ // the event shows up in the newly created control pipe)
1306
1295
  if ( document._casa_window_closed == false ) {
1307
1296
  ctrl_pipe.send( ids['stop'],
1308
1297
  { action: 'stop', value: { } },
cubevis/utils/__init__.py CHANGED
@@ -45,6 +45,7 @@ from ._copydoc import copydoc
45
45
  from ._pkgs import find_pkg, load_pkg
46
46
  from ._jupyter import is_interactive_jupyter
47
47
  from ._browser import have_firefox
48
+ from ._git import max_git_version
48
49
 
49
50
  from astropy import units
50
51
  from regions import PixCoord
@@ -167,28 +168,129 @@ def warn_import(package):
167
168
  error_msg("warning, %s" % warn_import.msgs[package].format(package=package))
168
169
 
169
170
 
170
- @static_vars(url='http://clients3.google.com/generate_204')
171
+ @static_vars(url='http://clients3.google.com/generate_204',
172
+ cached_result=None,
173
+ cache_timeout=30) # Cache result for 30 seconds
171
174
  def have_network():
172
- '''check to see if an active network with general internet connectivity
173
- is available. returns ``True`` if we have internet connectivity and
175
+ '''Check to see if an active network with general internet connectivity
176
+ is available. Returns ``True`` if we have internet connectivity and
174
177
  ``False`` if we do not.
178
+
179
+ Uses Google's connectivity check endpoint and caches the result briefly
180
+ to avoid repeated network calls.
175
181
  '''
176
- ###
177
- ### see: https://stackoverflow.com/questions/50558000/test-internet-connection-for-python3
178
- ###
179
- try:
180
- with urllib.request.urlopen(have_network.url) as response:
181
- return response.status == 204
182
- except urllib.error.HTTPError:
183
- ### http error
184
- return False
185
- except urllib.error.ContentTooShortError:
186
- return False
187
- except urllib.error.URLError:
188
- return False
189
- except Exception:
190
- return False
182
+ import time
183
+
184
+ # Check cache first
185
+ current_time = time.time()
186
+ if (hasattr(have_network, 'last_check_time') and
187
+ have_network.cached_result is not None and
188
+ current_time - have_network.last_check_time < have_network.cache_timeout):
189
+ return have_network.cached_result
190
+
191
+ def check_connectivity():
192
+ try:
193
+ # Set a reasonable timeout to avoid hanging
194
+ request = urllib.request.Request(
195
+ have_network.url,
196
+ headers={'User-Agent': 'Mozilla/5.0 (compatible; connectivity-check)'}
197
+ )
198
+
199
+ with urllib.request.urlopen(request, timeout=5) as response:
200
+ return response.status == 204
201
+
202
+ except urllib.error.HTTPError as e:
203
+ # Some proxies might return different status codes but still have connectivity
204
+ if e.code in [200, 301, 302]:
205
+ return True
206
+ return False
207
+ except (urllib.error.ContentTooShortError,
208
+ urllib.error.URLError,
209
+ socket.timeout,
210
+ socket.gaierror, # DNS resolution errors
211
+ OSError) as e:
212
+ return False
213
+ except Exception:
214
+ # Catch any other unexpected errors
215
+ return False
216
+
217
+ # Perform the check
218
+ result = check_connectivity()
219
+
220
+ # Cache the result
221
+ have_network.cached_result = result
222
+ have_network.last_check_time = current_time
223
+
224
+ return result
191
225
 
226
+ # Alternative version with fallback URLs
227
+ @static_vars(urls=['http://clients3.google.com/generate_204',
228
+ 'http://detectportal.firefox.com/success.txt',
229
+ 'http://www.msftconnecttest.com/connecttest.txt'],
230
+ cached_result=None,
231
+ cache_timeout=30)
232
+ def have_network_with_fallback():
233
+ '''Check network connectivity using multiple fallback endpoints.
234
+
235
+ Tries multiple well-known connectivity check endpoints to increase
236
+ reliability in different network environments.
237
+ '''
238
+ import time
239
+
240
+ # Check cache first
241
+ current_time = time.time()
242
+ if (hasattr(have_network_with_fallback, 'last_check_time') and
243
+ have_network_with_fallback.cached_result is not None and
244
+ current_time - have_network_with_fallback.last_check_time < have_network_with_fallback.cache_timeout):
245
+ return have_network_with_fallback.cached_result
246
+
247
+ def try_url(url, expected_status=None, expected_content=None):
248
+ try:
249
+ request = urllib.request.Request(
250
+ url,
251
+ headers={'User-Agent': 'Mozilla/5.0 (compatible; connectivity-check)'}
252
+ )
253
+
254
+ with urllib.request.urlopen(request, timeout=5) as response:
255
+ if expected_status and response.status != expected_status:
256
+ return False
257
+
258
+ if expected_content:
259
+ content = response.read().decode('utf-8').strip()
260
+ return content == expected_content
261
+
262
+ # For Google's endpoint, expect 204 No Content
263
+ if 'generate_204' in url:
264
+ return response.status == 204
265
+
266
+ # For other endpoints, any successful response is good
267
+ return 200 <= response.status < 400
268
+
269
+ except Exception:
270
+ return False
271
+
272
+ # Try each URL until one succeeds
273
+ for url in have_network_with_fallback.urls:
274
+ if url == 'http://detectportal.firefox.com/success.txt':
275
+ if try_url(url, expected_content='success'):
276
+ result = True
277
+ break
278
+ elif url == 'http://www.msftconnecttest.com/connecttest.txt':
279
+ if try_url(url, expected_content='Microsoft Connect Test'):
280
+ result = True
281
+ break
282
+ else: # Google's endpoint
283
+ if try_url(url):
284
+ result = True
285
+ break
286
+ else:
287
+ result = False
288
+
289
+ # Cache the result
290
+ have_network_with_fallback.cached_result = result
291
+ have_network_with_fallback.last_check_time = current_time
292
+
293
+ return result
192
294
 
193
295
  def ranges(iterable, order=sorted, key=lambda x: x):
194
296
  '''collect elements of ``iterable`` into tuple ranges where each tuple represents
cubevis/utils/_git.py ADDED
@@ -0,0 +1,36 @@
1
+ import subprocess
2
+ from os import path
3
+ from typing import Optional
4
+ from packaging import version
5
+
6
+ _VERSION = None
7
+ def max_git_version() -> Optional[str]:
8
+ """Get maximum version from all git tags"""
9
+ global _VERSION
10
+ package_parent = path.dirname(path.dirname(path.dirname(path.realpath(__file__))))
11
+ if _VERSION is None and path.basename( package_parent ) == 'cubevis' and \
12
+ path.isdir(path.join( package_parent, '.git' )):
13
+ result = subprocess.run(
14
+ ['git', 'tag'],
15
+ cwd=package_parent,
16
+ capture_output=True,
17
+ text=True,
18
+ check=True
19
+ )
20
+
21
+ # Filter tags matching version pattern v#.#.#
22
+ tags = [
23
+ tag[1:] for tag in result.stdout.strip().split('\n')
24
+ if tag.startswith('v') and all(part.isdigit() for part in tag[1:].split('.'))
25
+ ]
26
+
27
+ if not tags:
28
+ return None
29
+
30
+ # Sort by version number and get maximum
31
+ try:
32
+ max_tag = max(tags, key=lambda v: version.parse(v))
33
+ _VERSION = max_tag
34
+ except: pass
35
+
36
+ return _VERSION
cubevis/utils/_jupyter.py CHANGED
@@ -1,3 +1,6 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
1
4
 
2
5
  #def is_notebook() -> bool:
3
6
  # try:
@@ -31,10 +34,12 @@ def is_interactive_jupyter( ) -> bool:
31
34
  ipython = get_ipython()
32
35
 
33
36
  if ipython is None:
37
+ logger.debug(f"\tis_interactive_jupyter<1>: False")
34
38
  return False
35
39
 
36
40
  # Check if we're in a ZMQ-based shell (kernel)
37
41
  if ipython.__class__.__name__ != 'ZMQInteractiveShell':
42
+ logger.debug(f"\tis_interactive_jupyter<2>: False")
38
43
  return False
39
44
 
40
45
  # Check for active frontend connection
@@ -45,12 +50,14 @@ def is_interactive_jupyter( ) -> bool:
45
50
  if hasattr(kernel, 'shell_socket') and kernel.shell_socket is not None:
46
51
  # For newer Jupyter versions, check connection count
47
52
  if hasattr(kernel, 'connection_count'):
53
+ logger.debug(f"\tis_interactive_jupyter<3>: {kernel.connection_count > 0}")
48
54
  return kernel.connection_count > 0
49
55
 
50
56
  # For older versions, check if socket is connected
51
57
  try:
52
58
  # Try to get socket state - if it fails, likely no frontend
53
59
  socket_state = kernel.shell_socket.closed
60
+ logger.debug(f"\tis_interactive_jupyter<4>: {not socket_state}")
54
61
  return not socket_state
55
62
  except AttributeError:
56
63
  pass
@@ -60,12 +67,14 @@ def is_interactive_jupyter( ) -> bool:
60
67
  try:
61
68
  parent = kernel.get_parent()
62
69
  # If there's a parent message, we're likely in interactive mode
70
+ logger.debug(f"\tis_interactive_jupyter<5>: {parent is not None and len(parent) > 0}")
63
71
  return parent is not None and len(parent) > 0
64
72
  except Exception:
65
73
  pass
66
74
 
67
75
  # Method 3: Check for execution context
68
76
  if hasattr(kernel, '_parent_ident') and kernel._parent_ident:
77
+ logger.debug(f"\tis_interactive_jupyter<6>: True")
69
78
  return True
70
79
 
71
80
  # Fallback: Check for common Jupyter notebook environment indicators
@@ -82,11 +91,14 @@ def is_interactive_jupyter( ) -> bool:
82
91
  try:
83
92
  import IPython.display
84
93
  # If we can import display and have env indicators, likely interactive
94
+ logger.debug(f"\tis_interactive_jupyter<7>: True")
85
95
  return True
86
96
  except ImportError:
87
97
  pass
88
98
 
99
+ logger.debug(f"\tis_interactive_jupyter<8>: False")
89
100
  return False
90
101
 
91
102
  except ImportError:
103
+ logger.debug(f"\tis_interactive_jupyter<9>: False")
92
104
  return False
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cubevis
3
- Version: 1.0.0
3
+ Version: 1.0.3
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>
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.11
8
8
  Requires-Dist: astropy>=5.1
9
9
  Requires-Dist: bokeh==3.6.1
10
10
  Requires-Dist: certifi
11
11
  Requires-Dist: matplotlib
12
- Requires-Dist: regions>=0.6
12
+ Requires-Dist: regions>=0.8
13
13
  Requires-Dist: websockets>=10.3
14
14
  Project-URL: Bug Tracker, https://github.com/casangi/cubevis/issues
15
15
  Project-URL: Homepage, https://github.com/casangi/cubevis?tab=readme-ov-file#cubevis---visualization-tools-for-casa-images