setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.10__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (112) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/cosmic.svg +40 -0
  4. setiastro/images/cosmicsat.svg +24 -0
  5. setiastro/images/first_quarter.png +0 -0
  6. setiastro/images/full_moon.png +0 -0
  7. setiastro/images/graxpert.svg +19 -0
  8. setiastro/images/last_quarter.png +0 -0
  9. setiastro/images/linearfit.svg +32 -0
  10. setiastro/images/new_moon.png +0 -0
  11. setiastro/images/pixelmath.svg +42 -0
  12. setiastro/images/waning_crescent_1.png +0 -0
  13. setiastro/images/waning_crescent_2.png +0 -0
  14. setiastro/images/waning_crescent_3.png +0 -0
  15. setiastro/images/waning_crescent_4.png +0 -0
  16. setiastro/images/waning_crescent_5.png +0 -0
  17. setiastro/images/waning_gibbous_1.png +0 -0
  18. setiastro/images/waning_gibbous_2.png +0 -0
  19. setiastro/images/waning_gibbous_3.png +0 -0
  20. setiastro/images/waning_gibbous_4.png +0 -0
  21. setiastro/images/waning_gibbous_5.png +0 -0
  22. setiastro/images/waxing_crescent_1.png +0 -0
  23. setiastro/images/waxing_crescent_2.png +0 -0
  24. setiastro/images/waxing_crescent_3.png +0 -0
  25. setiastro/images/waxing_crescent_4.png +0 -0
  26. setiastro/images/waxing_crescent_5.png +0 -0
  27. setiastro/images/waxing_gibbous_1.png +0 -0
  28. setiastro/images/waxing_gibbous_2.png +0 -0
  29. setiastro/images/waxing_gibbous_3.png +0 -0
  30. setiastro/images/waxing_gibbous_4.png +0 -0
  31. setiastro/images/waxing_gibbous_5.png +0 -0
  32. setiastro/qml/ResourceMonitor.qml +84 -82
  33. setiastro/saspro/__main__.py +19 -0
  34. setiastro/saspro/_generated/build_info.py +2 -2
  35. setiastro/saspro/abe.py +37 -4
  36. setiastro/saspro/aberration_ai.py +237 -21
  37. setiastro/saspro/acv_exporter.py +379 -0
  38. setiastro/saspro/add_stars.py +33 -6
  39. setiastro/saspro/backgroundneutral.py +35 -7
  40. setiastro/saspro/blemish_blaster.py +4 -1
  41. setiastro/saspro/blink_comparator_pro.py +74 -24
  42. setiastro/saspro/clahe.py +4 -1
  43. setiastro/saspro/continuum_subtract.py +4 -1
  44. setiastro/saspro/convo.py +4 -1
  45. setiastro/saspro/cosmicclarity.py +129 -18
  46. setiastro/saspro/crop_dialog_pro.py +123 -7
  47. setiastro/saspro/curve_editor_pro.py +109 -42
  48. setiastro/saspro/doc_manager.py +67 -4
  49. setiastro/saspro/exoplanet_detector.py +120 -28
  50. setiastro/saspro/frequency_separation.py +1158 -204
  51. setiastro/saspro/ghs_dialog_pro.py +81 -16
  52. setiastro/saspro/graxpert.py +1 -0
  53. setiastro/saspro/gui/main_window.py +393 -204
  54. setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
  55. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  56. setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
  57. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  58. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  59. setiastro/saspro/halobgon.py +4 -0
  60. setiastro/saspro/histogram.py +5 -1
  61. setiastro/saspro/image_combine.py +4 -0
  62. setiastro/saspro/image_peeker_pro.py +4 -0
  63. setiastro/saspro/imageops/stretch.py +531 -62
  64. setiastro/saspro/isophote.py +4 -0
  65. setiastro/saspro/layers.py +13 -9
  66. setiastro/saspro/layers_dock.py +183 -3
  67. setiastro/saspro/legacy/image_manager.py +154 -20
  68. setiastro/saspro/legacy/numba_utils.py +43 -0
  69. setiastro/saspro/legacy/xisf.py +240 -98
  70. setiastro/saspro/live_stacking.py +180 -79
  71. setiastro/saspro/luminancerecombine.py +228 -27
  72. setiastro/saspro/mask_creation.py +174 -15
  73. setiastro/saspro/mfdeconv.py +113 -35
  74. setiastro/saspro/mfdeconvcudnn.py +119 -70
  75. setiastro/saspro/mfdeconvsport.py +112 -35
  76. setiastro/saspro/morphology.py +4 -0
  77. setiastro/saspro/multiscale_decomp.py +51 -12
  78. setiastro/saspro/numba_utils.py +72 -2
  79. setiastro/saspro/ops/commands.py +18 -18
  80. setiastro/saspro/ops/script_editor.py +5 -2
  81. setiastro/saspro/ops/scripts.py +3 -0
  82. setiastro/saspro/perfect_palette_picker.py +37 -3
  83. setiastro/saspro/plate_solver.py +84 -49
  84. setiastro/saspro/psf_viewer.py +119 -37
  85. setiastro/saspro/resources.py +67 -0
  86. setiastro/saspro/rgbalign.py +4 -0
  87. setiastro/saspro/selective_color.py +4 -1
  88. setiastro/saspro/sfcc.py +60 -2
  89. setiastro/saspro/shortcuts.py +142 -23
  90. setiastro/saspro/signature_insert.py +692 -33
  91. setiastro/saspro/stacking_suite.py +1017 -400
  92. setiastro/saspro/star_alignment.py +4 -1
  93. setiastro/saspro/star_spikes.py +4 -0
  94. setiastro/saspro/star_stretch.py +38 -3
  95. setiastro/saspro/stat_stretch.py +702 -128
  96. setiastro/saspro/subwindow.py +786 -360
  97. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  98. setiastro/saspro/wavescale_hdr.py +4 -1
  99. setiastro/saspro/wavescalede.py +4 -1
  100. setiastro/saspro/whitebalance.py +60 -12
  101. setiastro/saspro/widgets/common_utilities.py +28 -21
  102. setiastro/saspro/widgets/resource_monitor.py +109 -59
  103. setiastro/saspro/widgets/spinboxes.py +10 -13
  104. setiastro/saspro/wimi.py +27 -656
  105. setiastro/saspro/wims.py +13 -3
  106. setiastro/saspro/xisf.py +101 -11
  107. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +2 -1
  108. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
  109. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
  110. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
  111. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
  112. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
@@ -10,11 +10,13 @@ import json
10
10
  import sys
11
11
  import webbrowser
12
12
  from typing import TYPE_CHECKING
13
-
13
+ import re
14
14
  from PyQt6.QtCore import QUrl
15
15
  from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
16
16
  from PyQt6.QtWidgets import QMessageBox, QApplication
17
17
 
18
+ from PyQt6.QtNetwork import QSslSocket
19
+
18
20
  if TYPE_CHECKING:
19
21
  pass
20
22
 
@@ -44,27 +46,38 @@ class UpdateMixin:
44
46
  return str(v or "0.0.0")
45
47
 
46
48
  def _ensure_network_manager(self):
47
- """Ensure the network access manager exists."""
48
49
  from PyQt6.QtNetwork import QNetworkAccessManager
49
-
50
- if not hasattr(self, "_nam") or self._nam is None:
50
+ if getattr(self, "_nam", None) is None:
51
51
  self._nam = QNetworkAccessManager(self)
52
52
  self._nam.finished.connect(self._on_update_reply)
53
53
 
54
54
  def _kick_update_check(self, *, interactive: bool):
55
55
  """
56
56
  Start an update check request.
57
-
58
- Args:
59
- interactive: If True, show UI feedback for the check
60
57
  """
61
58
  self._ensure_network_manager()
62
59
  url_str = self.settings.value("updates/url", self._updates_url, type=str) or self._updates_url
60
+
61
+ if url_str.lower().startswith("https://"):
62
+ try:
63
+ if not QSslSocket.supportsSsl():
64
+ if self.statusBar():
65
+ self.statusBar().showMessage(self.tr("Update check unavailable (TLS missing)."), 8000)
66
+ if interactive:
67
+ QMessageBox.information(
68
+ self, self.tr("Update Check"),
69
+ self.tr("Update check is unavailable because TLS is not available on this system.")
70
+ )
71
+ else:
72
+ print("[updates] TLS unavailable in Qt; skipping update check.")
73
+ return
74
+ except Exception as e:
75
+ print(f"[updates] TLS probe failed ({e}); skipping update check.")
76
+ return
77
+
63
78
  req = QNetworkRequest(QUrl(url_str))
64
- req.setRawHeader(
65
- b"User-Agent",
66
- f"SASPro/{self._current_version_str}".encode("utf-8")
67
- )
79
+ req.setRawHeader(b"User-Agent", f"SASPro/{self._current_version_str}".encode("utf-8"))
80
+
68
81
  reply = self._nam.get(req)
69
82
  reply.setProperty("interactive", interactive)
70
83
 
@@ -97,20 +110,22 @@ class UpdateMixin:
97
110
  def _on_update_reply(self, reply: QNetworkReply):
98
111
  """Handle network reply from update check or download."""
99
112
  interactive = bool(reply.property("interactive"))
100
-
113
+
101
114
  # Was this the second request (the actual installer download)?
102
115
  if bool(reply.property("is_update_download")):
103
116
  self._on_windows_update_download_finished(reply)
104
117
  return
105
-
118
+
106
119
  try:
107
120
  if reply.error() != QNetworkReply.NetworkError.NoError:
108
121
  err = reply.errorString()
109
122
  if self.statusBar():
110
- self.statusBar().showMessage("Update check failed.", 5000)
123
+ self.statusBar().showMessage(self.tr("Update check failed."), 5000)
111
124
  if interactive:
112
- QMessageBox.warning(self, self.tr("Update Check Failed"),
113
- self.tr("Unable to check for updates.\n\n{err}").replace("{err}", err))
125
+ QMessageBox.warning(
126
+ self, self.tr("Update Check Failed"),
127
+ self.tr("Unable to check for updates.\n\n{0}").format(err)
128
+ )
114
129
  else:
115
130
  print(f"[updates] check failed: {err}")
116
131
  return
@@ -120,12 +135,14 @@ class UpdateMixin:
120
135
  data = json.loads(raw.decode("utf-8"))
121
136
  except Exception as je:
122
137
  if self.statusBar():
123
- self.statusBar().showMessage("Update check failed (bad JSON).", 5000)
138
+ self.statusBar().showMessage(self.tr("Update check failed (bad JSON)."), 5000)
124
139
  if interactive:
125
- QMessageBox.warning(self, self.tr("Update Check Failed"),
126
- self.tr("Update JSON is invalid.\n\n{je}").replace("{je}", str(je)))
140
+ QMessageBox.warning(
141
+ self, self.tr("Update Check Failed"),
142
+ self.tr("Update JSON is invalid.\n\n{0}").format(str(je))
143
+ )
127
144
  else:
128
- print(f"[updates] bad JSON: {je}")
145
+ print(f"[updates] bad JSON: {je!r}")
129
146
  return
130
147
 
131
148
  latest_str = str(data.get("version", "")).strip()
@@ -134,25 +151,53 @@ class UpdateMixin:
134
151
 
135
152
  if not latest_str:
136
153
  if self.statusBar():
137
- self.statusBar().showMessage("Update check failed (no 'version').", 5000)
154
+ self.statusBar().showMessage(self.tr("Update check failed (no version)."), 5000)
138
155
  if interactive:
139
- QMessageBox.warning(self, self.tr("Update Check Failed"),
140
- self.tr("Update JSON missing the 'version' field."))
156
+ QMessageBox.warning(
157
+ self, self.tr("Update Check Failed"),
158
+ self.tr("Update JSON missing the 'version' field.")
159
+ )
141
160
  else:
142
161
  print("[updates] JSON missing 'version'")
143
162
  return
144
163
 
145
- cur_tuple = self._parse_version_tuple(self._current_version_str)
146
- latest_tuple = self._parse_version_tuple(latest_str)
147
- available = bool(latest_tuple and cur_tuple and latest_tuple > cur_tuple)
164
+ # ---- PEP 440 version compare ----
165
+ try:
166
+ from packaging.version import Version
167
+ cur_v = Version(str(self._current_version_str).strip())
168
+ latest_v = Version(latest_str)
169
+ except Exception as e:
170
+ if self.statusBar():
171
+ self.statusBar().showMessage(self.tr("Update check failed (version parse)."), 5000)
172
+ if interactive:
173
+ QMessageBox.warning(
174
+ self, self.tr("Update Check Failed"),
175
+ self.tr("Could not compare versions.\n\nCurrent: {0}\nLatest: {1}\n\n{2}")
176
+ .format(self._current_version_str, latest_str, str(e))
177
+ )
178
+ else:
179
+ print(f"[updates] version parse failed: cur={self._current_version_str!r} latest={latest_str!r} err={e!r}")
180
+ return
181
+
182
+ available = latest_v > cur_v
148
183
 
149
184
  if available:
150
185
  if self.statusBar():
151
186
  self.statusBar().showMessage(self.tr("Update available: {0}").format(latest_str), 5000)
187
+
152
188
  msg_box = QMessageBox(self)
153
189
  msg_box.setIcon(QMessageBox.Icon.Information)
154
190
  msg_box.setWindowTitle(self.tr("Update Available"))
155
- msg_box.setText(self.tr("A new version ({0}) is available!").format(latest_str))
191
+ installed_norm = str(cur_v)
192
+ reported_norm = str(latest_v)
193
+
194
+ msg_box.setText(
195
+ self.tr(
196
+ "An update is available!\n\n"
197
+ "Installed version: {0}\n"
198
+ "Available version: {1}"
199
+ ).format(installed_norm, reported_norm)
200
+ )
156
201
  if notes:
157
202
  msg_box.setInformativeText(self.tr("Release Notes:\n{0}").format(notes))
158
203
  msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
@@ -164,27 +209,48 @@ class UpdateMixin:
164
209
 
165
210
  if msg_box.exec() == QMessageBox.StandardButton.Yes:
166
211
  plat = sys.platform
167
- link = downloads.get(
212
+ key = (
168
213
  "Windows" if plat.startswith("win") else
169
- "macOS" if plat.startswith("darwin") else
170
- "Linux" if plat.startswith("linux") else "", ""
214
+ "macOS" if plat.startswith("darwin") else
215
+ "Linux" if plat.startswith("linux") else
216
+ ""
171
217
  )
218
+ link = downloads.get(key, "")
172
219
  if not link:
173
- QMessageBox.warning(self, self.tr("Download"), self.tr("No download link available for this platform."))
220
+ QMessageBox.warning(self, self.tr("Download"),
221
+ self.tr("No download link available for this platform."))
174
222
  return
175
223
 
176
224
  if plat.startswith("win"):
177
- # Use in-app updater for Windows
178
225
  self._start_windows_update_download(link)
179
226
  else:
180
- # Open browser for other platforms
181
227
  webbrowser.open(link)
182
228
  else:
183
229
  if self.statusBar():
184
- self.statusBar().showMessage("You're up to date.", 3000)
230
+ self.statusBar().showMessage(self.tr("You're up to date."), 3000)
231
+
185
232
  if interactive:
186
- QMessageBox.information(self, self.tr("Up to Date"),
187
- self.tr("You're already running the latest version."))
233
+ # Use the same parsed versions you already computed
234
+ installed_str = str(self._current_version_str).strip()
235
+ reported_str = str(latest_str).strip()
236
+
237
+ # If you have cur_v/latest_v (packaging.Version), use their string forms too
238
+ try:
239
+ installed_norm = str(cur_v) # normalized PEP440 (e.g. 1.6.6.post3)
240
+ reported_norm = str(latest_v)
241
+ except Exception:
242
+ installed_norm = installed_str
243
+ reported_norm = reported_str
244
+
245
+ QMessageBox.information(
246
+ self,
247
+ self.tr("Up to Date"),
248
+ self.tr(
249
+ "You're already running the latest version.\n\n"
250
+ "Installed version: {0}\n"
251
+ "Update source reports: {1}"
252
+ ).format(installed_norm, reported_norm)
253
+ )
188
254
  finally:
189
255
  reply.deleteLater()
190
256
 
@@ -307,3 +373,39 @@ class UpdateMixin:
307
373
 
308
374
  # Close app so the installer can overwrite files
309
375
  QApplication.instance().quit()
376
+
377
+
378
+ def _normalize_version_str(self, v: str) -> str:
379
+ v = (v or "").strip()
380
+ # common cases: "v1.2.3", "Version 1.2.3", "1.2.3 (build xyz)"
381
+ v = re.sub(r'^[^\d]*', '', v) # strip leading non-digits
382
+ v = re.split(r'[\s\(\[]', v, 1)[0].strip() # stop at whitespace/( or [
383
+ return v
384
+
385
+ def _parse_version(self, v: str):
386
+ v = self._normalize_version_str(v)
387
+ if not v:
388
+ return None
389
+ # Prefer packaging if present
390
+ try:
391
+ from packaging.version import Version
392
+ return Version(v)
393
+ except Exception:
394
+ # Fallback: compare numeric dot parts only
395
+ parts = re.findall(r'\d+', v)
396
+ if not parts:
397
+ return None
398
+ # normalize length to 3+ (so 1.2 == 1.2.0)
399
+ nums = [int(x) for x in parts[:6]]
400
+ while len(nums) < 3:
401
+ nums.append(0)
402
+ return tuple(nums)
403
+
404
+ def _is_update_available(self, latest_str: str) -> bool:
405
+ cur = self._parse_version(self._current_version_str)
406
+ latest = self._parse_version(latest_str)
407
+ if cur is None or latest is None:
408
+ # If we cannot compare, do NOT claim "up to date".
409
+ # Treat as "unknown" and show a failure message in interactive mode.
410
+ return False
411
+ return latest > cur
@@ -193,6 +193,7 @@ class ViewMixin:
193
193
  if hasattr(view, "set_scale") and callable(view.set_scale):
194
194
  try:
195
195
  view.set_scale(float(scale))
196
+ self._ensure_smooth_resample(view)
196
197
  self._center_view(view)
197
198
  return
198
199
  except Exception:
@@ -209,6 +210,47 @@ class ViewMixin:
209
210
  except Exception:
210
211
  pass
211
212
 
213
+ def _ensure_smooth_resample(self, view):
214
+ """
215
+ Make sure the view is using smooth interpolation for the current scale.
216
+ Different view widgets in SASpro may implement this differently, so we
217
+ try a few known hooks safely.
218
+ """
219
+ # 1) Best case: explicit API
220
+ for name in ("set_smooth_scaling", "set_interpolation", "set_smooth", "enable_smooth_scaling"):
221
+ fn = getattr(view, name, None)
222
+ if callable(fn):
223
+ try:
224
+ fn(True)
225
+ return
226
+ except Exception:
227
+ pass
228
+
229
+ # 2) Some views store a mode flag
230
+ for attr in ("smooth_scaling", "_smooth_scaling", "_use_smooth_scaling", "use_smooth_scaling"):
231
+ if hasattr(view, attr):
232
+ try:
233
+ setattr(view, attr, True)
234
+ # kick a repaint/update if available
235
+ try:
236
+ view.update()
237
+ except Exception:
238
+ pass
239
+ return
240
+ except Exception:
241
+ pass
242
+
243
+ # 3) QLabel pixmap scaling: if you have a custom "rebuild pixmap" method, call it
244
+ for name in ("_rebuild_pixmap", "_update_pixmap", "_render_scaled", "rebuild_pixmap"):
245
+ fn = getattr(view, name, None)
246
+ if callable(fn):
247
+ try:
248
+ fn()
249
+ return
250
+ except Exception:
251
+ pass
252
+
253
+
212
254
  def _infer_image_size(self, view):
213
255
  """Return (img_w, img_h) in device-independent pixels (ints), best-effort."""
214
256
  # Preferred: from the label's pixmap
@@ -252,6 +252,10 @@ class HaloBGonDialogPro(QDialog):
252
252
  self.setWindowFlag(Qt.WindowType.Window, True)
253
253
  self.setWindowModality(Qt.WindowModality.NonModal)
254
254
  self.setModal(False)
255
+ try:
256
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
257
+ except Exception:
258
+ pass # older PyQt6 versions
255
259
  if icon:
256
260
  try: self.setWindowIcon(icon)
257
261
  except Exception as e:
@@ -33,13 +33,17 @@ class HistogramDialog(QDialog):
33
33
  self.setWindowFlag(Qt.WindowType.Window, True)
34
34
  self.setWindowModality(Qt.WindowModality.NonModal)
35
35
  self.setModal(False)
36
+ try:
37
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
38
+ except Exception:
39
+ pass # older PyQt6 versions
36
40
  self.doc = document
37
41
  self.image = _to_float_preserve(document.image)
38
42
 
39
43
  self.zoom_factor = 1.0 # 1.0 = 100%
40
44
  self.log_scale = False # log X
41
45
  self.log_y = False # log Y
42
- self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
46
+ #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
43
47
  self._eps_log = 1e-6 # first log bin edge (for labels)
44
48
 
45
49
  # for mapping clicks → normalized x
@@ -94,6 +94,10 @@ class ImageCombineDialog(QDialog):
94
94
  self.setWindowFlag(Qt.WindowType.Window, True)
95
95
  self.setWindowModality(Qt.WindowModality.NonModal)
96
96
  self.setModal(False)
97
+ try:
98
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
99
+ except Exception:
100
+ pass # older PyQt6 versions
97
101
  self.mw = main_window
98
102
  self.dm = getattr(main_window, "doc_manager", None) or getattr(main_window, "dm", None)
99
103
  self.zoom = 1.0
@@ -1317,6 +1317,10 @@ class ImagePeekerDialogPro(QDialog):
1317
1317
  self.setWindowFlag(Qt.WindowType.Window, True)
1318
1318
  self.setWindowModality(Qt.WindowModality.NonModal)
1319
1319
  self.setModal(False)
1320
+ try:
1321
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
1322
+ except Exception:
1323
+ pass # older PyQt6 versions
1320
1324
  self.document = self._coerce_doc(document) # <- ensure we hold a real doc
1321
1325
  self.settings = settings
1322
1326
  # status / progress line