synodic-client 0.0.1.dev82__tar.gz → 0.0.1.dev84__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.
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/tray.py +26 -49
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/schema.py +6 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/updater.py +92 -5
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_updater.py +267 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/README.md +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/debug.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/schema.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/wsl.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/update_controller.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/update_model.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/config.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/context.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/debug.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/install.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/output.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/project.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/tool.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/cli/update.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/config.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/install.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/project.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/schema.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/tool.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/update.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_config.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_install.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_install_plan.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_project.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_tool.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_update.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_package_state.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_update_controller.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_bootstrap.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/windows/test_startup.py +0 -0
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from PySide6.QtCore import QTimer
|
|
7
|
-
from PySide6.QtGui import QAction
|
|
7
|
+
from PySide6.QtGui import QAction
|
|
8
8
|
from PySide6.QtWidgets import (
|
|
9
9
|
QApplication,
|
|
10
10
|
QMainWindow,
|
|
@@ -54,13 +54,15 @@ class TrayScreen:
|
|
|
54
54
|
self.tray.setIcon(app_icon())
|
|
55
55
|
self.tray.activated.connect(self._on_tray_activated)
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
self._tray_retry_count = 0
|
|
60
|
-
self._tray_retry_timer: QTimer | None = None
|
|
61
|
-
self._show_tray_icon()
|
|
57
|
+
self._build_menu()
|
|
58
|
+
self.tray.setContextMenu(self._menu)
|
|
62
59
|
|
|
63
|
-
|
|
60
|
+
# At early Windows login the notification area may not be ready.
|
|
61
|
+
# Poll until the tray is available, then show the icon.
|
|
62
|
+
self._tray_poll = QTimer()
|
|
63
|
+
self._tray_poll.setInterval(2000)
|
|
64
|
+
self._tray_poll.timeout.connect(self._try_show_tray_icon)
|
|
65
|
+
self._try_show_tray_icon()
|
|
64
66
|
|
|
65
67
|
# Settings window (created once, shown/hidden on demand)
|
|
66
68
|
self._settings_window = SettingsWindow(
|
|
@@ -109,13 +111,13 @@ class TrayScreen:
|
|
|
109
111
|
# Connect ToolsView signals - deferred because ToolsView is created lazily
|
|
110
112
|
window.tools_view_created.connect(self._tool_orchestrator.connect_tools_view)
|
|
111
113
|
|
|
112
|
-
def _build_menu(self
|
|
114
|
+
def _build_menu(self) -> None:
|
|
113
115
|
"""Build the tray context menu."""
|
|
114
116
|
self._menu = QMenu()
|
|
115
117
|
|
|
116
118
|
self._open_action = QAction('Open', self._menu)
|
|
117
119
|
self._menu.addAction(self._open_action)
|
|
118
|
-
self._open_action.triggered.connect(
|
|
120
|
+
self._open_action.triggered.connect(self._show_window)
|
|
119
121
|
|
|
120
122
|
self._menu.addSeparator()
|
|
121
123
|
|
|
@@ -131,55 +133,27 @@ class TrayScreen:
|
|
|
131
133
|
|
|
132
134
|
self._menu.aboutToShow.connect(lambda: logger.debug('Tray context menu about to show'))
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Delay between retries in milliseconds.
|
|
137
|
-
_TRAY_RETRY_DELAY_MS = 2000
|
|
138
|
-
|
|
139
|
-
def _show_tray_icon(self) -> None:
|
|
140
|
-
"""Show the tray icon, retrying if the system tray is not ready."""
|
|
136
|
+
def _try_show_tray_icon(self) -> None:
|
|
137
|
+
"""Show the tray icon once the system tray is available."""
|
|
141
138
|
if QSystemTrayIcon.isSystemTrayAvailable():
|
|
139
|
+
self._tray_poll.stop()
|
|
142
140
|
self.tray.setVisible(True)
|
|
143
141
|
logger.debug('System tray icon shown')
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
self._tray_retry_count += 1
|
|
148
|
-
logger.warning(
|
|
149
|
-
'System tray not available, retrying (%d/%d)',
|
|
150
|
-
self._tray_retry_count,
|
|
151
|
-
self._TRAY_MAX_RETRIES,
|
|
152
|
-
)
|
|
153
|
-
self._tray_retry_timer = QTimer()
|
|
154
|
-
self._tray_retry_timer.setSingleShot(True)
|
|
155
|
-
self._tray_retry_timer.timeout.connect(self._show_tray_icon)
|
|
156
|
-
self._tray_retry_timer.start(self._TRAY_RETRY_DELAY_MS)
|
|
157
|
-
else:
|
|
158
|
-
# Exhausted retries — show anyway as a best-effort fallback.
|
|
159
|
-
logger.warning(
|
|
160
|
-
'System tray still not available after %d retries, forcing visibility',
|
|
161
|
-
self._TRAY_MAX_RETRIES,
|
|
162
|
-
)
|
|
163
|
-
self.tray.setVisible(True)
|
|
164
|
-
|
|
165
|
-
# Delay before showing the context menu, in milliseconds.
|
|
166
|
-
# Absorbs residual mouse-up events from touchpad two-finger taps
|
|
167
|
-
# that would otherwise land on a menu item (typically "Quit").
|
|
168
|
-
_MENU_POPUP_DELAY_MS = 80
|
|
142
|
+
elif not self._tray_poll.isActive():
|
|
143
|
+
logger.warning('System tray not available, polling until ready')
|
|
144
|
+
self._tray_poll.start()
|
|
169
145
|
|
|
170
146
|
def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None:
|
|
171
147
|
"""Handle tray icon activation."""
|
|
172
148
|
logger.debug('Tray activated: reason=%s', reason.name)
|
|
173
149
|
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
|
174
|
-
self.
|
|
175
|
-
self._window.raise_()
|
|
176
|
-
self._window.activateWindow()
|
|
177
|
-
elif reason == QSystemTrayIcon.ActivationReason.Context:
|
|
178
|
-
QTimer.singleShot(self._MENU_POPUP_DELAY_MS, self._show_tray_menu)
|
|
150
|
+
self._show_window()
|
|
179
151
|
|
|
180
|
-
def
|
|
181
|
-
"""Show
|
|
182
|
-
self.
|
|
152
|
+
def _show_window(self) -> None:
|
|
153
|
+
"""Show, raise, and focus the main window."""
|
|
154
|
+
self._window.show()
|
|
155
|
+
self._window.raise_()
|
|
156
|
+
self._window.activateWindow()
|
|
183
157
|
|
|
184
158
|
def _on_quit_triggered(self) -> None:
|
|
185
159
|
"""Handle the Quit menu action."""
|
|
@@ -189,6 +163,8 @@ class TrayScreen:
|
|
|
189
163
|
def _show_settings(self) -> None:
|
|
190
164
|
"""Show the settings window."""
|
|
191
165
|
self._settings_window.show()
|
|
166
|
+
self._settings_window.raise_()
|
|
167
|
+
self._settings_window.activateWindow()
|
|
192
168
|
|
|
193
169
|
@staticmethod
|
|
194
170
|
def _is_user_active() -> bool:
|
|
@@ -222,6 +198,7 @@ class TrayScreen:
|
|
|
222
198
|
|
|
223
199
|
def shutdown(self) -> None:
|
|
224
200
|
"""Stop all timers and cancel in-flight tasks for a clean exit."""
|
|
201
|
+
self._tray_poll.stop()
|
|
225
202
|
self._update_controller.shutdown()
|
|
226
203
|
self._tool_orchestrator.shutdown()
|
|
227
204
|
logger.info('TrayScreen shut down')
|
|
@@ -151,6 +151,12 @@ class UpdateInfo:
|
|
|
151
151
|
# Internal: Velopack update info for download/apply
|
|
152
152
|
_velopack_info: Any = field(default=None, repr=False)
|
|
153
153
|
|
|
154
|
+
# Internal: True when the update was discovered via the manifest
|
|
155
|
+
# fallback rather than the Velopack SDK. The download path uses
|
|
156
|
+
# this to route to a direct HTTP download instead of the SDK's
|
|
157
|
+
# GithubSource (which cannot find prerelease assets).
|
|
158
|
+
_used_manifest_fallback: bool = field(default=False, repr=False)
|
|
159
|
+
|
|
154
160
|
|
|
155
161
|
# Default interval for automatic update checks (minutes)
|
|
156
162
|
DEFAULT_AUTO_UPDATE_INTERVAL_MINUTES = 5
|
|
@@ -8,11 +8,13 @@ For non-installed (development) environments, updates are not supported.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import contextlib
|
|
11
|
+
import hashlib
|
|
11
12
|
import json
|
|
12
13
|
import logging
|
|
13
14
|
import sys
|
|
14
15
|
import urllib.request
|
|
15
16
|
from collections.abc import Callable
|
|
17
|
+
from pathlib import Path
|
|
16
18
|
from typing import Any
|
|
17
19
|
|
|
18
20
|
import velopack
|
|
@@ -175,12 +177,14 @@ class Updater:
|
|
|
175
177
|
error='Not installed via Velopack',
|
|
176
178
|
)
|
|
177
179
|
|
|
180
|
+
used_fallback = False
|
|
178
181
|
try:
|
|
179
182
|
velopack_info = manager.check_for_updates()
|
|
180
183
|
except Exception as sdk_err:
|
|
181
184
|
if '404' in str(sdk_err):
|
|
182
185
|
logger.debug('SDK check failed with 404, trying manifest fallback: %s', sdk_err)
|
|
183
186
|
velopack_info = self._check_manifest_fallback()
|
|
187
|
+
used_fallback = velopack_info is not None
|
|
184
188
|
else:
|
|
185
189
|
raise
|
|
186
190
|
|
|
@@ -188,6 +192,7 @@ class Updater:
|
|
|
188
192
|
# SDK returned no update; try the manual manifest fallback
|
|
189
193
|
# in case the SDK's GithubSource skipped prerelease entries.
|
|
190
194
|
velopack_info = self._check_manifest_fallback()
|
|
195
|
+
used_fallback = velopack_info is not None
|
|
191
196
|
|
|
192
197
|
if velopack_info is not None:
|
|
193
198
|
latest = Version(velopack_info.TargetFullRelease.Version)
|
|
@@ -197,6 +202,7 @@ class Updater:
|
|
|
197
202
|
current_version=self._current_version,
|
|
198
203
|
latest_version=latest,
|
|
199
204
|
_velopack_info=velopack_info,
|
|
205
|
+
_used_manifest_fallback=used_fallback,
|
|
200
206
|
)
|
|
201
207
|
# Only advance to UPDATE_AVAILABLE if we haven't already
|
|
202
208
|
# moved past it. A periodic re-check that discovers the
|
|
@@ -310,6 +316,85 @@ class Updater:
|
|
|
310
316
|
IsDowngrade=False,
|
|
311
317
|
)
|
|
312
318
|
|
|
319
|
+
def _download_direct(
|
|
320
|
+
self,
|
|
321
|
+
velopack_info: Any,
|
|
322
|
+
progress_callback: Callable[[int], None] | None = None,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Download the update package directly via HTTP.
|
|
325
|
+
|
|
326
|
+
Used when the update was discovered via ``_check_manifest_fallback``
|
|
327
|
+
instead of the Velopack SDK. The SDK's ``GithubSource`` cannot
|
|
328
|
+
download assets from prerelease GitHub Releases, so this method
|
|
329
|
+
fetches the ``.nupkg`` from the known GitHub Release asset URL
|
|
330
|
+
and places it in the Velopack packages directory where the SDK's
|
|
331
|
+
apply step expects to find it.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
velopack_info: A ``velopack.UpdateInfo`` whose
|
|
335
|
+
``TargetFullRelease`` describes the package to download.
|
|
336
|
+
progress_callback: Optional callback for percentage progress
|
|
337
|
+
(0–100).
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
RuntimeError: If the packages directory cannot be determined,
|
|
341
|
+
the download fails, or the checksum does not match.
|
|
342
|
+
"""
|
|
343
|
+
asset = velopack_info.TargetFullRelease
|
|
344
|
+
asset_base = github_release_asset_url(self._config.repo_url, self._config.channel)
|
|
345
|
+
download_url = f'{asset_base}/{asset.FileName}'
|
|
346
|
+
|
|
347
|
+
# Velopack stores packages under ``{root}/packages/`` where
|
|
348
|
+
# ``{root}`` is the parent of the ``current/`` directory that
|
|
349
|
+
# contains the running executable.
|
|
350
|
+
packages_dir = Path(sys.executable).resolve().parent.parent / 'packages'
|
|
351
|
+
packages_dir.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
|
|
353
|
+
target_file = packages_dir / asset.FileName
|
|
354
|
+
if target_file.exists():
|
|
355
|
+
logger.info('Package already exists, skipping download: %s', target_file)
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
partial_file = target_file.with_suffix('.partial')
|
|
359
|
+
|
|
360
|
+
logger.info('Direct download: %s -> %s', download_url, partial_file)
|
|
361
|
+
|
|
362
|
+
req = urllib.request.Request(download_url, headers={'User-Agent': 'synodic-client'})
|
|
363
|
+
with urllib.request.urlopen(req, timeout=300) as resp: # noqa: S310 — URL from known repo
|
|
364
|
+
total = int(resp.headers.get('Content-Length', 0))
|
|
365
|
+
sha256_hash = hashlib.sha256()
|
|
366
|
+
downloaded = 0
|
|
367
|
+
|
|
368
|
+
with partial_file.open('wb') as f:
|
|
369
|
+
while True:
|
|
370
|
+
chunk = resp.read(256 * 1024)
|
|
371
|
+
if not chunk:
|
|
372
|
+
break
|
|
373
|
+
f.write(chunk)
|
|
374
|
+
sha256_hash.update(chunk)
|
|
375
|
+
downloaded += len(chunk)
|
|
376
|
+
if progress_callback is not None and total > 0:
|
|
377
|
+
progress_callback(int(downloaded * 100 / total))
|
|
378
|
+
|
|
379
|
+
# Verify checksum — prefer SHA256, fall back to SHA1.
|
|
380
|
+
if asset.SHA256:
|
|
381
|
+
actual = sha256_hash.hexdigest()
|
|
382
|
+
if not actual.lower() == asset.SHA256.lower():
|
|
383
|
+
partial_file.unlink(missing_ok=True)
|
|
384
|
+
raise RuntimeError(
|
|
385
|
+
f'SHA256 mismatch for {asset.FileName}: expected {asset.SHA256}, got {actual}'
|
|
386
|
+
)
|
|
387
|
+
elif asset.SHA1:
|
|
388
|
+
actual_sha1 = hashlib.sha1(partial_file.read_bytes()).hexdigest() # noqa: S324 — verifying known digest
|
|
389
|
+
if not actual_sha1.lower() == asset.SHA1.lower():
|
|
390
|
+
partial_file.unlink(missing_ok=True)
|
|
391
|
+
raise RuntimeError(
|
|
392
|
+
f'SHA1 mismatch for {asset.FileName}: expected {asset.SHA1}, got {actual_sha1}'
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
partial_file.rename(target_file)
|
|
396
|
+
logger.info('Direct download complete: %s', target_file)
|
|
397
|
+
|
|
313
398
|
def download_update(self, progress_callback: Callable[[int], None] | None = None) -> bool:
|
|
314
399
|
"""Download the update.
|
|
315
400
|
|
|
@@ -334,11 +419,13 @@ class Updater:
|
|
|
334
419
|
logger.info('Starting update download for %s', self._update_info._velopack_info)
|
|
335
420
|
|
|
336
421
|
try:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
422
|
+
if self._update_info._used_manifest_fallback:
|
|
423
|
+
self._download_direct(self._update_info._velopack_info, progress_callback)
|
|
424
|
+
else:
|
|
425
|
+
manager = self._get_velopack_manager()
|
|
426
|
+
if manager is None:
|
|
427
|
+
raise RuntimeError('Velopack manager not available')
|
|
428
|
+
manager.download_updates(self._update_info._velopack_info, progress_callback)
|
|
342
429
|
|
|
343
430
|
self._state = UpdateState.DOWNLOADED
|
|
344
431
|
logger.info('Update downloaded successfully')
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""Tests for the self-update functionality using Velopack."""
|
|
2
2
|
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
3
6
|
from unittest.mock import MagicMock, PropertyMock, patch
|
|
4
7
|
|
|
5
8
|
import pytest
|
|
@@ -597,3 +600,267 @@ class TestPep440ToSemver:
|
|
|
597
600
|
"""SemVer-style pre-release input is normalised via PEP 440."""
|
|
598
601
|
# packaging.version.Version normalises '0.1.0-dev.5' to '0.1.0.dev5'
|
|
599
602
|
assert pep440_to_semver('0.1.0-dev.5') == '0.1.0-dev.5'
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
# Realistic dev-channel manifest payload (mirrors the real GitHub Release).
|
|
607
|
+
# The ``dev`` tag on GitHub is marked ``"prerelease": true``, which
|
|
608
|
+
# Velopack's GithubSource filters out (it hard-codes prerelease=false).
|
|
609
|
+
# ---------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
_DEV_MANIFEST: dict[str, object] = {
|
|
612
|
+
'Assets': [
|
|
613
|
+
{
|
|
614
|
+
'PackageId': 'synodic',
|
|
615
|
+
'Version': '0.1.0-dev.83',
|
|
616
|
+
'Type': 'Full',
|
|
617
|
+
'FileName': 'synodic-0.1.0-dev.83-dev-win-full.nupkg',
|
|
618
|
+
'SHA1': 'aabbccdd',
|
|
619
|
+
'SHA256': '07badc6414dc5d87b009a7ecaa4ee446febe0d15275aa86465a9720762ceab80',
|
|
620
|
+
'Size': 66358200,
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
'PackageId': 'synodic',
|
|
624
|
+
'Version': '0.1.0-dev.83',
|
|
625
|
+
'Type': 'Delta',
|
|
626
|
+
'FileName': 'synodic-0.1.0-dev.83-dev-win-delta.nupkg',
|
|
627
|
+
'SHA1': '11223344',
|
|
628
|
+
'SHA256': '5305031a791e0de45f517eda0d2cf827951ec603380ba740fe58fbac4b6246cd',
|
|
629
|
+
'Size': 15560939,
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
'PackageId': 'synodic',
|
|
633
|
+
'Version': '0.1.0-dev.80',
|
|
634
|
+
'Type': 'Full',
|
|
635
|
+
'FileName': 'synodic-0.1.0-dev.80-dev-win-full.nupkg',
|
|
636
|
+
'SHA1': 'deadbeef',
|
|
637
|
+
'SHA256': '09bc2032a6a374e1d722cb3c42d1e586a9113c7fd6877721ad5e1ecb611dbbc3',
|
|
638
|
+
'Size': 65808335,
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _make_urlopen_response(data: dict[str, object]) -> MagicMock:
|
|
645
|
+
"""Build a mock ``urlopen`` return value that reads as JSON."""
|
|
646
|
+
body = json.dumps(data).encode()
|
|
647
|
+
resp = MagicMock()
|
|
648
|
+
resp.read.return_value = body
|
|
649
|
+
resp.__enter__ = lambda s: s
|
|
650
|
+
resp.__exit__ = MagicMock(return_value=False)
|
|
651
|
+
resp.headers = {'Content-Length': str(len(body))}
|
|
652
|
+
return resp
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@pytest.fixture
|
|
656
|
+
def dev_updater() -> Updater:
|
|
657
|
+
"""Create an Updater on the dev channel at version 0.1.0-dev.80."""
|
|
658
|
+
config = UpdateConfig(
|
|
659
|
+
repo_url=GITHUB_REPO_URL,
|
|
660
|
+
channel=UpdateChannel.DEVELOPMENT,
|
|
661
|
+
)
|
|
662
|
+
u = Updater(current_version=Version('0.1.0.dev80'), config=config)
|
|
663
|
+
u._velopack_manager = None
|
|
664
|
+
u._velopack_not_installed = False
|
|
665
|
+
return u
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class TestDevChannelGithubPrerelease:
|
|
669
|
+
"""Regression tests: dev channel uses a GitHub prerelease that Velopack's
|
|
670
|
+
GithubSource silently ignores (``prerelease=false``).
|
|
671
|
+
|
|
672
|
+
The check path already has ``_check_manifest_fallback``. These tests
|
|
673
|
+
verify that the *download* path also works when the update was discovered
|
|
674
|
+
via the fallback.
|
|
675
|
+
"""
|
|
676
|
+
|
|
677
|
+
@staticmethod
|
|
678
|
+
def test_check_finds_update_via_manifest_fallback(dev_updater: Updater) -> None:
|
|
679
|
+
"""check_for_update discovers dev.83 via manifest fallback when SDK returns None."""
|
|
680
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
681
|
+
# SDK's GithubSource filters prereleases → returns None
|
|
682
|
+
mock_manager.check_for_updates.return_value = None
|
|
683
|
+
|
|
684
|
+
manifest_resp = _make_urlopen_response(_DEV_MANIFEST)
|
|
685
|
+
|
|
686
|
+
with (
|
|
687
|
+
patch.object(dev_updater, '_get_velopack_manager', return_value=mock_manager),
|
|
688
|
+
patch('synodic_client.updater.urllib.request.urlopen', return_value=manifest_resp),
|
|
689
|
+
):
|
|
690
|
+
info = dev_updater.check_for_update()
|
|
691
|
+
|
|
692
|
+
assert info.available is True
|
|
693
|
+
assert info.latest_version == Version('0.1.0.dev83')
|
|
694
|
+
assert dev_updater.state == UpdateState.UPDATE_AVAILABLE
|
|
695
|
+
|
|
696
|
+
@staticmethod
|
|
697
|
+
def test_check_sets_manifest_fallback_flag(dev_updater: Updater) -> None:
|
|
698
|
+
"""check_for_update sets _used_manifest_fallback when fallback discovered the update."""
|
|
699
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
700
|
+
mock_manager.check_for_updates.return_value = None
|
|
701
|
+
|
|
702
|
+
manifest_resp = _make_urlopen_response(_DEV_MANIFEST)
|
|
703
|
+
|
|
704
|
+
with (
|
|
705
|
+
patch.object(dev_updater, '_get_velopack_manager', return_value=mock_manager),
|
|
706
|
+
patch('synodic_client.updater.urllib.request.urlopen', return_value=manifest_resp),
|
|
707
|
+
):
|
|
708
|
+
info = dev_updater.check_for_update()
|
|
709
|
+
|
|
710
|
+
assert info._used_manifest_fallback is True
|
|
711
|
+
|
|
712
|
+
@staticmethod
|
|
713
|
+
def test_check_sdk_success_does_not_set_fallback_flag(dev_updater: Updater) -> None:
|
|
714
|
+
"""_used_manifest_fallback stays False when the SDK itself found the update."""
|
|
715
|
+
mock_target = MagicMock(spec=velopack.VelopackAsset)
|
|
716
|
+
mock_target.Version = '0.1.0-dev.83'
|
|
717
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
718
|
+
mock_velopack_info.TargetFullRelease = mock_target
|
|
719
|
+
|
|
720
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
721
|
+
mock_manager.check_for_updates.return_value = mock_velopack_info
|
|
722
|
+
|
|
723
|
+
with patch.object(dev_updater, '_get_velopack_manager', return_value=mock_manager):
|
|
724
|
+
info = dev_updater.check_for_update()
|
|
725
|
+
|
|
726
|
+
assert info.available is True
|
|
727
|
+
assert info._used_manifest_fallback is False
|
|
728
|
+
|
|
729
|
+
@staticmethod
|
|
730
|
+
def test_download_succeeds_after_manifest_fallback(dev_updater: Updater) -> None:
|
|
731
|
+
"""download_update routes to _download_direct when the manifest fallback was used."""
|
|
732
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
733
|
+
mock_manager.check_for_updates.return_value = None
|
|
734
|
+
|
|
735
|
+
manifest_resp = _make_urlopen_response(_DEV_MANIFEST)
|
|
736
|
+
|
|
737
|
+
with (
|
|
738
|
+
patch.object(dev_updater, '_get_velopack_manager', return_value=mock_manager),
|
|
739
|
+
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
|
|
740
|
+
patch('synodic_client.updater.urllib.request.urlopen', return_value=manifest_resp),
|
|
741
|
+
):
|
|
742
|
+
info = dev_updater.check_for_update()
|
|
743
|
+
assert info.available is True
|
|
744
|
+
assert info._used_manifest_fallback is True
|
|
745
|
+
|
|
746
|
+
# Now mock the direct download — _download_direct is called instead of
|
|
747
|
+
# manager.download_updates, so the SDK never touches GithubSource.
|
|
748
|
+
with (
|
|
749
|
+
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
|
|
750
|
+
patch.object(dev_updater, '_download_direct') as mock_direct,
|
|
751
|
+
):
|
|
752
|
+
result = dev_updater.download_update()
|
|
753
|
+
|
|
754
|
+
assert result is True
|
|
755
|
+
assert dev_updater.state == UpdateState.DOWNLOADED
|
|
756
|
+
mock_direct.assert_called_once_with(info._velopack_info, None)
|
|
757
|
+
# The SDK's download_updates should NOT have been called
|
|
758
|
+
mock_manager.download_updates.assert_not_called()
|
|
759
|
+
|
|
760
|
+
@staticmethod
|
|
761
|
+
def test_sdk_download_used_when_sdk_found_update(dev_updater: Updater) -> None:
|
|
762
|
+
"""download_update uses the SDK when the update was found without the fallback."""
|
|
763
|
+
mock_target = MagicMock(spec=velopack.VelopackAsset)
|
|
764
|
+
mock_target.Version = '0.1.0-dev.83'
|
|
765
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
766
|
+
mock_velopack_info.TargetFullRelease = mock_target
|
|
767
|
+
|
|
768
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
769
|
+
mock_manager.check_for_updates.return_value = mock_velopack_info
|
|
770
|
+
|
|
771
|
+
with (
|
|
772
|
+
patch.object(dev_updater, '_get_velopack_manager', return_value=mock_manager),
|
|
773
|
+
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
|
|
774
|
+
):
|
|
775
|
+
info = dev_updater.check_for_update()
|
|
776
|
+
assert info._used_manifest_fallback is False
|
|
777
|
+
|
|
778
|
+
result = dev_updater.download_update()
|
|
779
|
+
|
|
780
|
+
assert result is True
|
|
781
|
+
assert dev_updater.state == UpdateState.DOWNLOADED
|
|
782
|
+
mock_manager.download_updates.assert_called_once_with(mock_velopack_info, None)
|
|
783
|
+
|
|
784
|
+
@staticmethod
|
|
785
|
+
def test_download_direct_constructs_correct_url(dev_updater: Updater, tmp_path: Path) -> None:
|
|
786
|
+
"""_download_direct fetches from the correct GitHub release asset URL."""
|
|
787
|
+
mock_velopack_info = MagicMock()
|
|
788
|
+
mock_velopack_info.TargetFullRelease.FileName = 'synodic-0.1.0-dev.83-dev-win-full.nupkg'
|
|
789
|
+
mock_velopack_info.TargetFullRelease.SHA256 = ''
|
|
790
|
+
mock_velopack_info.TargetFullRelease.SHA1 = ''
|
|
791
|
+
mock_velopack_info.TargetFullRelease.Size = 0
|
|
792
|
+
|
|
793
|
+
nupkg_content = b'fake-nupkg-content'
|
|
794
|
+
resp = MagicMock()
|
|
795
|
+
resp.read.side_effect = [nupkg_content, b'']
|
|
796
|
+
resp.__enter__ = lambda s: s
|
|
797
|
+
resp.__exit__ = MagicMock(return_value=False)
|
|
798
|
+
resp.headers = {'Content-Length': str(len(nupkg_content))}
|
|
799
|
+
|
|
800
|
+
with (
|
|
801
|
+
patch('synodic_client.updater.sys') as mock_sys,
|
|
802
|
+
patch('synodic_client.updater.urllib.request.urlopen', return_value=resp) as mock_urlopen,
|
|
803
|
+
):
|
|
804
|
+
mock_sys.executable = str(tmp_path / 'current' / 'synodic.exe')
|
|
805
|
+
(tmp_path / 'current').mkdir()
|
|
806
|
+
|
|
807
|
+
dev_updater._download_direct(mock_velopack_info)
|
|
808
|
+
|
|
809
|
+
# Verify the URL
|
|
810
|
+
call_args = mock_urlopen.call_args
|
|
811
|
+
req = call_args[0][0]
|
|
812
|
+
expected_url = (
|
|
813
|
+
f'{GITHUB_REPO_URL}/releases/download/dev'
|
|
814
|
+
'/synodic-0.1.0-dev.83-dev-win-full.nupkg'
|
|
815
|
+
)
|
|
816
|
+
assert req.full_url == expected_url
|
|
817
|
+
|
|
818
|
+
# Verify the file was written
|
|
819
|
+
target_file = tmp_path / 'packages' / 'synodic-0.1.0-dev.83-dev-win-full.nupkg'
|
|
820
|
+
assert target_file.exists()
|
|
821
|
+
assert target_file.read_bytes() == nupkg_content
|
|
822
|
+
|
|
823
|
+
@staticmethod
|
|
824
|
+
def test_download_direct_sha256_mismatch(dev_updater: Updater, tmp_path: Path) -> None:
|
|
825
|
+
"""_download_direct raises on SHA256 mismatch and cleans up the partial."""
|
|
826
|
+
mock_velopack_info = MagicMock()
|
|
827
|
+
mock_velopack_info.TargetFullRelease.FileName = 'test.nupkg'
|
|
828
|
+
mock_velopack_info.TargetFullRelease.SHA256 = 'wrong_hash'
|
|
829
|
+
mock_velopack_info.TargetFullRelease.SHA1 = ''
|
|
830
|
+
mock_velopack_info.TargetFullRelease.Size = 0
|
|
831
|
+
|
|
832
|
+
resp = MagicMock()
|
|
833
|
+
resp.read.side_effect = [b'some content', b'']
|
|
834
|
+
resp.__enter__ = lambda s: s
|
|
835
|
+
resp.__exit__ = MagicMock(return_value=False)
|
|
836
|
+
resp.headers = {'Content-Length': '12'}
|
|
837
|
+
|
|
838
|
+
with (
|
|
839
|
+
patch('synodic_client.updater.sys') as mock_sys,
|
|
840
|
+
patch('synodic_client.updater.urllib.request.urlopen', return_value=resp),
|
|
841
|
+
pytest.raises(RuntimeError, match='SHA256 mismatch'),
|
|
842
|
+
):
|
|
843
|
+
mock_sys.executable = str(tmp_path / 'current' / 'synodic.exe')
|
|
844
|
+
(tmp_path / 'current').mkdir()
|
|
845
|
+
|
|
846
|
+
dev_updater._download_direct(mock_velopack_info)
|
|
847
|
+
|
|
848
|
+
# Partial file should be cleaned up
|
|
849
|
+
assert not (tmp_path / 'packages' / 'test.nupkg.partial').exists()
|
|
850
|
+
assert not (tmp_path / 'packages' / 'test.nupkg').exists()
|
|
851
|
+
|
|
852
|
+
@staticmethod
|
|
853
|
+
def test_download_direct_skips_existing(dev_updater: Updater, tmp_path: Path) -> None:
|
|
854
|
+
"""_download_direct skips download if the package already exists on disk."""
|
|
855
|
+
mock_velopack_info = MagicMock()
|
|
856
|
+
mock_velopack_info.TargetFullRelease.FileName = 'already-there.nupkg'
|
|
857
|
+
|
|
858
|
+
with patch('synodic_client.updater.sys') as mock_sys:
|
|
859
|
+
mock_sys.executable = str(tmp_path / 'current' / 'synodic.exe')
|
|
860
|
+
(tmp_path / 'current').mkdir()
|
|
861
|
+
packages = tmp_path / 'packages'
|
|
862
|
+
packages.mkdir()
|
|
863
|
+
(packages / 'already-there.nupkg').write_bytes(b'existing')
|
|
864
|
+
|
|
865
|
+
# Should not hit network at all
|
|
866
|
+
dev_updater._download_direct(mock_velopack_info)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/config_store.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/debug.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/card.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/screen/wsl.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/application/update_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/config.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/install.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/project.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/operations/update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/synodic_client/subprocess_patch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_config.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_install.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_install_plan.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_project.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_tool.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/operations/test_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_package_state.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_update_controller.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/qt/test_update_feedback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev82 → synodic_client-0.0.1.dev84}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|