setiastrosuitepro 1.6.7__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.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/add_stars.py +29 -5
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/cosmicclarity.py +125 -18
- setiastro/saspro/crop_dialog_pro.py +96 -2
- setiastro/saspro/curve_editor_pro.py +60 -39
- setiastro/saspro/frequency_separation.py +1159 -208
- setiastro/saspro/gui/main_window.py +131 -31
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/update_mixin.py +121 -33
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/live_stacking.py +158 -70
- setiastro/saspro/multiscale_decomp.py +47 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/shortcuts.py +122 -12
- setiastro/saspro/signature_insert.py +688 -33
- setiastro/saspro/stacking_suite.py +523 -316
- setiastro/saspro/stat_stretch.py +688 -130
- setiastro/saspro/subwindow.py +302 -71
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +7 -7
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +37 -31
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
|
@@ -10,7 +10,7 @@ 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
|
|
@@ -58,21 +58,20 @@ class UpdateMixin:
|
|
|
58
58
|
self._ensure_network_manager()
|
|
59
59
|
url_str = self.settings.value("updates/url", self._updates_url, type=str) or self._updates_url
|
|
60
60
|
|
|
61
|
-
# ---- TLS availability guard (prevents crash on OpenSSL mismatch) ----
|
|
62
61
|
if url_str.lower().startswith("https://"):
|
|
63
62
|
try:
|
|
64
63
|
if not QSslSocket.supportsSsl():
|
|
65
|
-
msg = "TLS unavailable in Qt (QSslSocket.supportsSsl()=False). Skipping update check."
|
|
66
64
|
if self.statusBar():
|
|
67
|
-
self.statusBar().showMessage(self.tr("Update check unavailable (TLS missing)."),
|
|
65
|
+
self.statusBar().showMessage(self.tr("Update check unavailable (TLS missing)."), 8000)
|
|
68
66
|
if interactive:
|
|
69
|
-
QMessageBox.information(
|
|
70
|
-
|
|
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
71
|
else:
|
|
72
|
-
print(
|
|
72
|
+
print("[updates] TLS unavailable in Qt; skipping update check.")
|
|
73
73
|
return
|
|
74
74
|
except Exception as e:
|
|
75
|
-
# If QtNetwork is in a weird state, fail safe (do not attempt TLS)
|
|
76
75
|
print(f"[updates] TLS probe failed ({e}); skipping update check.")
|
|
77
76
|
return
|
|
78
77
|
|
|
@@ -111,20 +110,22 @@ class UpdateMixin:
|
|
|
111
110
|
def _on_update_reply(self, reply: QNetworkReply):
|
|
112
111
|
"""Handle network reply from update check or download."""
|
|
113
112
|
interactive = bool(reply.property("interactive"))
|
|
114
|
-
|
|
113
|
+
|
|
115
114
|
# Was this the second request (the actual installer download)?
|
|
116
115
|
if bool(reply.property("is_update_download")):
|
|
117
116
|
self._on_windows_update_download_finished(reply)
|
|
118
117
|
return
|
|
119
|
-
|
|
118
|
+
|
|
120
119
|
try:
|
|
121
120
|
if reply.error() != QNetworkReply.NetworkError.NoError:
|
|
122
121
|
err = reply.errorString()
|
|
123
122
|
if self.statusBar():
|
|
124
|
-
self.statusBar().showMessage("Update check failed.", 5000)
|
|
123
|
+
self.statusBar().showMessage(self.tr("Update check failed."), 5000)
|
|
125
124
|
if interactive:
|
|
126
|
-
QMessageBox.warning(
|
|
127
|
-
|
|
125
|
+
QMessageBox.warning(
|
|
126
|
+
self, self.tr("Update Check Failed"),
|
|
127
|
+
self.tr("Unable to check for updates.\n\n{0}").format(err)
|
|
128
|
+
)
|
|
128
129
|
else:
|
|
129
130
|
print(f"[updates] check failed: {err}")
|
|
130
131
|
return
|
|
@@ -134,12 +135,14 @@ class UpdateMixin:
|
|
|
134
135
|
data = json.loads(raw.decode("utf-8"))
|
|
135
136
|
except Exception as je:
|
|
136
137
|
if self.statusBar():
|
|
137
|
-
self.statusBar().showMessage("Update check failed (bad JSON).", 5000)
|
|
138
|
+
self.statusBar().showMessage(self.tr("Update check failed (bad JSON)."), 5000)
|
|
138
139
|
if interactive:
|
|
139
|
-
QMessageBox.warning(
|
|
140
|
-
|
|
140
|
+
QMessageBox.warning(
|
|
141
|
+
self, self.tr("Update Check Failed"),
|
|
142
|
+
self.tr("Update JSON is invalid.\n\n{0}").format(str(je))
|
|
143
|
+
)
|
|
141
144
|
else:
|
|
142
|
-
print(f"[updates] bad JSON: {je}")
|
|
145
|
+
print(f"[updates] bad JSON: {je!r}")
|
|
143
146
|
return
|
|
144
147
|
|
|
145
148
|
latest_str = str(data.get("version", "")).strip()
|
|
@@ -148,25 +151,53 @@ class UpdateMixin:
|
|
|
148
151
|
|
|
149
152
|
if not latest_str:
|
|
150
153
|
if self.statusBar():
|
|
151
|
-
self.statusBar().showMessage("Update check failed (no
|
|
154
|
+
self.statusBar().showMessage(self.tr("Update check failed (no version)."), 5000)
|
|
152
155
|
if interactive:
|
|
153
|
-
QMessageBox.warning(
|
|
154
|
-
|
|
156
|
+
QMessageBox.warning(
|
|
157
|
+
self, self.tr("Update Check Failed"),
|
|
158
|
+
self.tr("Update JSON missing the 'version' field.")
|
|
159
|
+
)
|
|
155
160
|
else:
|
|
156
161
|
print("[updates] JSON missing 'version'")
|
|
157
162
|
return
|
|
158
163
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
162
183
|
|
|
163
184
|
if available:
|
|
164
185
|
if self.statusBar():
|
|
165
186
|
self.statusBar().showMessage(self.tr("Update available: {0}").format(latest_str), 5000)
|
|
187
|
+
|
|
166
188
|
msg_box = QMessageBox(self)
|
|
167
189
|
msg_box.setIcon(QMessageBox.Icon.Information)
|
|
168
190
|
msg_box.setWindowTitle(self.tr("Update Available"))
|
|
169
|
-
|
|
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
|
+
)
|
|
170
201
|
if notes:
|
|
171
202
|
msg_box.setInformativeText(self.tr("Release Notes:\n{0}").format(notes))
|
|
172
203
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
@@ -178,27 +209,48 @@ class UpdateMixin:
|
|
|
178
209
|
|
|
179
210
|
if msg_box.exec() == QMessageBox.StandardButton.Yes:
|
|
180
211
|
plat = sys.platform
|
|
181
|
-
|
|
212
|
+
key = (
|
|
182
213
|
"Windows" if plat.startswith("win") else
|
|
183
|
-
"macOS"
|
|
184
|
-
"Linux"
|
|
214
|
+
"macOS" if plat.startswith("darwin") else
|
|
215
|
+
"Linux" if plat.startswith("linux") else
|
|
216
|
+
""
|
|
185
217
|
)
|
|
218
|
+
link = downloads.get(key, "")
|
|
186
219
|
if not link:
|
|
187
|
-
QMessageBox.warning(self, self.tr("Download"),
|
|
220
|
+
QMessageBox.warning(self, self.tr("Download"),
|
|
221
|
+
self.tr("No download link available for this platform."))
|
|
188
222
|
return
|
|
189
223
|
|
|
190
224
|
if plat.startswith("win"):
|
|
191
|
-
# Use in-app updater for Windows
|
|
192
225
|
self._start_windows_update_download(link)
|
|
193
226
|
else:
|
|
194
|
-
# Open browser for other platforms
|
|
195
227
|
webbrowser.open(link)
|
|
196
228
|
else:
|
|
197
229
|
if self.statusBar():
|
|
198
|
-
self.statusBar().showMessage("You're up to date.", 3000)
|
|
230
|
+
self.statusBar().showMessage(self.tr("You're up to date."), 3000)
|
|
231
|
+
|
|
199
232
|
if interactive:
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
)
|
|
202
254
|
finally:
|
|
203
255
|
reply.deleteLater()
|
|
204
256
|
|
|
@@ -321,3 +373,39 @@ class UpdateMixin:
|
|
|
321
373
|
|
|
322
374
|
# Close app so the installer can overwrite files
|
|
323
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
|