synodic-client 0.0.1.dev53__tar.gz → 0.0.1.dev54__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.
Files changed (76) hide show
  1. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/PKG-INFO +1 -1
  2. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/pyproject.toml +1 -1
  3. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/update_controller.py +18 -4
  4. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_update_controller.py +75 -0
  5. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/LICENSE.md +0 -0
  6. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/README.md +0 -0
  7. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/__init__.py +0 -0
  8. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/__main__.py +0 -0
  9. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/__init__.py +0 -0
  10. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/bootstrap.py +0 -0
  11. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/data.py +0 -0
  12. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/icon.py +0 -0
  13. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/init.py +0 -0
  14. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/instance.py +0 -0
  15. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/qt.py +0 -0
  16. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/schema.py +0 -0
  17. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/__init__.py +0 -0
  18. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/action_card.py +0 -0
  19. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/card.py +0 -0
  20. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/install.py +0 -0
  21. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/install_workers.py +0 -0
  22. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/log_panel.py +0 -0
  23. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/plugin_row.py +0 -0
  24. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/projects.py +0 -0
  25. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/schema.py +0 -0
  26. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/screen.py +0 -0
  27. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/settings.py +0 -0
  28. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/sidebar.py +0 -0
  29. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/spinner.py +0 -0
  30. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/tool_update_controller.py +0 -0
  31. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/tray.py +0 -0
  32. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/screen/update_banner.py +0 -0
  33. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/theme.py +0 -0
  34. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/uri.py +0 -0
  35. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/application/workers.py +0 -0
  36. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/cli.py +0 -0
  37. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/client.py +0 -0
  38. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/config.py +0 -0
  39. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/logging.py +0 -0
  40. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/protocol.py +0 -0
  41. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/py.typed +0 -0
  42. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/resolution.py +0 -0
  43. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/schema.py +0 -0
  44. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/startup.py +0 -0
  45. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/synodic_client/updater.py +0 -0
  46. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/__init__.py +0 -0
  47. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/conftest.py +0 -0
  48. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/__init__.py +0 -0
  49. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/__init__.py +0 -0
  50. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/conftest.py +0 -0
  51. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_action_card.py +0 -0
  52. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_gather_packages.py +0 -0
  53. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_install_preview.py +0 -0
  54. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_log_panel.py +0 -0
  55. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_logging.py +0 -0
  56. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_preview_model.py +0 -0
  57. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_settings.py +0 -0
  58. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_sidebar.py +0 -0
  59. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_tray_window_show.py +0 -0
  60. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_update_banner.py +0 -0
  61. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/qt/test_update_feedback.py +0 -0
  62. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_cli.py +0 -0
  63. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_client_updater.py +0 -0
  64. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_client_version.py +0 -0
  65. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_config.py +0 -0
  66. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_examples.py +0 -0
  67. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_init.py +0 -0
  68. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_install.py +0 -0
  69. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_resolution.py +0 -0
  70. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_updater.py +0 -0
  71. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_uri.py +0 -0
  72. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/test_workers.py +0 -0
  73. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/windows/__init__.py +0 -0
  74. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/windows/conftest.py +0 -0
  75. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/windows/test_protocol.py +0 -0
  76. {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev54}/tests/unit/windows/test_startup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev53
3
+ Version: 0.0.1.dev54
4
4
  Author-Email: Synodic Software <contact@synodic.software>
5
5
  License: LGPL-3.0-or-later
6
6
  Project-URL: homepage, https://github.com/synodic/synodic-client
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "velopack>=0.0.1444.dev49733",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev53"
18
+ version = "0.0.1.dev54"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -84,6 +84,7 @@ class UpdateController:
84
84
  self._config = config
85
85
  self._is_user_active: Callable[[], bool] = lambda: False
86
86
  self._update_task: asyncio.Task[None] | None = None
87
+ self._pending_version: str | None = None
87
88
 
88
89
  # Derive auto-apply preference from config
89
90
  resolved = self._resolve_config()
@@ -214,8 +215,9 @@ class UpdateController:
214
215
  self._banner.show_error('Updater is not initialized.')
215
216
  return
216
217
 
217
- # Show checking state in settings
218
- self._settings_window.set_checking()
218
+ # Preserve the restart button when an update is already pending
219
+ if self._pending_version is None:
220
+ self._settings_window.set_checking()
219
221
 
220
222
  self._update_task = asyncio.create_task(self._async_check(silent=silent))
221
223
 
@@ -258,8 +260,14 @@ class UpdateController:
258
260
  logger.debug('Automatic update check: no update available')
259
261
  return
260
262
 
261
- # Update available — show status and start download
262
263
  version = str(result.latest_version)
264
+
265
+ # Already downloaded — restore the ready state without re-downloading
266
+ if version == self._pending_version:
267
+ self._show_ready(version)
268
+ return
269
+
270
+ # New update available — download it
263
271
  self._settings_window.set_update_status(
264
272
  f'v{version} available',
265
273
  UPDATE_STATUS_AVAILABLE_STYLE,
@@ -307,6 +315,8 @@ class UpdateController:
307
315
  # Persist the client update timestamp
308
316
  update_user_config(last_client_update=datetime.now(UTC).isoformat())
309
317
 
318
+ self._pending_version = version
319
+
310
320
  if self._can_auto_apply():
311
321
  # Silently apply and restart — no banner, no user interaction
312
322
  logger.info('Auto-applying update v%s', version)
@@ -317,7 +327,10 @@ class UpdateController:
317
327
  self._apply_update(silent=True)
318
328
  return
319
329
 
320
- # Manual mode (or user is active) — show ready banner and let user choose when to restart
330
+ self._show_ready(version)
331
+
332
+ def _show_ready(self, version: str) -> None:
333
+ """Present the *ready to restart* state in both UIs."""
321
334
  self._banner.show_ready(version)
322
335
  self._settings_window.set_update_status(
323
336
  f'v{version} ready',
@@ -345,6 +358,7 @@ class UpdateController:
345
358
  return
346
359
 
347
360
  try:
361
+ self._pending_version = None
348
362
  self._client.apply_update_on_exit(restart=True, silent=silent)
349
363
  logger.info('Update scheduled — restarting application')
350
364
  self._app.quit()
@@ -228,6 +228,81 @@ class TestDownloadFinished:
228
228
  assert banner.state.name == 'ERROR'
229
229
  settings.set_update_status.assert_called_with('Download failed', UPDATE_STATUS_ERROR_STYLE)
230
230
 
231
+ @staticmethod
232
+ def test_download_sets_pending_version() -> None:
233
+ """A successful download should set _pending_version."""
234
+ ctrl, app, client, banner, settings = _make_controller(auto_apply=False)
235
+ ctrl._on_download_finished(True, '2.0.0')
236
+
237
+ assert ctrl._pending_version == '2.0.0'
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Pending version — skip redundant downloads
242
+ # ---------------------------------------------------------------------------
243
+
244
+
245
+ class TestPendingVersion:
246
+ """Verify behaviour when an update is already downloaded and pending."""
247
+
248
+ @staticmethod
249
+ def test_check_skips_download_when_version_already_pending() -> None:
250
+ """Re-checking the same version should restore ready state, not re-download."""
251
+ ctrl, _app, _client, banner, settings = _make_controller(auto_apply=False)
252
+ ctrl._pending_version = '2.0.0'
253
+
254
+ result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
255
+
256
+ with patch.object(ctrl, '_start_download') as mock_dl:
257
+ ctrl._on_check_finished(result, silent=True)
258
+
259
+ mock_dl.assert_not_called()
260
+ assert banner.state.name == 'READY'
261
+ settings.show_restart_button.assert_called_once()
262
+
263
+ @staticmethod
264
+ def test_check_downloads_when_newer_version() -> None:
265
+ """A different version should trigger a fresh download."""
266
+ ctrl, _app, _client, banner, settings = _make_controller(auto_apply=False)
267
+ ctrl._pending_version = '1.5.0'
268
+
269
+ result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
270
+
271
+ with patch.object(ctrl, '_start_download') as mock_dl:
272
+ ctrl._on_check_finished(result, silent=True)
273
+
274
+ mock_dl.assert_called_once_with('2.0.0')
275
+
276
+ @staticmethod
277
+ def test_do_check_preserves_settings_ui_when_pending() -> None:
278
+ """set_checking should NOT be called when an update is already pending."""
279
+ ctrl, _app, _client, banner, settings = _make_controller()
280
+ ctrl._pending_version = '2.0.0'
281
+
282
+ with patch('asyncio.create_task'):
283
+ ctrl._do_check(silent=True)
284
+
285
+ settings.set_checking.assert_not_called()
286
+
287
+ @staticmethod
288
+ def test_do_check_shows_checking_when_no_pending() -> None:
289
+ """set_checking should be called when there is no pending update."""
290
+ ctrl, _app, _client, banner, settings = _make_controller()
291
+
292
+ with patch('asyncio.create_task'):
293
+ ctrl._do_check(silent=True)
294
+
295
+ settings.set_checking.assert_called_once()
296
+
297
+ @staticmethod
298
+ def test_apply_clears_pending_version() -> None:
299
+ """_apply_update should clear _pending_version."""
300
+ ctrl, app, client, banner, settings = _make_controller()
301
+ ctrl._pending_version = '2.0.0'
302
+ ctrl._apply_update()
303
+
304
+ assert ctrl._pending_version is None
305
+
231
306
 
232
307
  # ---------------------------------------------------------------------------
233
308
  # User-active gating