synodic-client 0.0.1.dev18__tar.gz → 0.0.1.dev20__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 (48) hide show
  1. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/PKG-INFO +3 -3
  2. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/pyproject.toml +5 -5
  3. synodic_client-0.0.1.dev20/synodic_client/_version.py +1 -0
  4. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/qt.py +6 -4
  5. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/updater.py +65 -29
  6. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/qt/conftest.py +1 -1
  7. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_updater.py +67 -4
  8. synodic_client-0.0.1.dev18/synodic_client/_version.py +0 -1
  9. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/LICENSE.md +0 -0
  10. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/README.md +0 -0
  11. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/__init__.py +0 -0
  12. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/__main__.py +0 -0
  13. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/__init__.py +0 -0
  14. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/icon.py +0 -0
  15. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/instance.py +0 -0
  16. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/screen/__init__.py +0 -0
  17. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/screen/install.py +0 -0
  18. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/screen/log_panel.py +0 -0
  19. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/screen/screen.py +0 -0
  20. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/screen/tray.py +0 -0
  21. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/theme.py +0 -0
  22. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/application/uri.py +0 -0
  23. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/cli.py +0 -0
  24. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/client.py +0 -0
  25. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/config.py +0 -0
  26. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/logging.py +0 -0
  27. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/protocol.py +0 -0
  28. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/py.typed +0 -0
  29. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/synodic_client/resolution.py +0 -0
  30. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/__init__.py +0 -0
  31. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/conftest.py +0 -0
  32. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/__init__.py +0 -0
  33. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/qt/__init__.py +0 -0
  34. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/qt/test_install_preview.py +0 -0
  35. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/qt/test_log_panel.py +0 -0
  36. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/qt/test_logging.py +0 -0
  37. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_cli.py +0 -0
  38. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_client_updater.py +0 -0
  39. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_client_version.py +0 -0
  40. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_config.py +0 -0
  41. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_examples.py +0 -0
  42. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_install.py +0 -0
  43. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_install_preview.py +0 -0
  44. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_resolution.py +0 -0
  45. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/test_uri.py +0 -0
  46. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/windows/__init__.py +0 -0
  47. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/windows/conftest.py +0 -0
  48. {synodic_client-0.0.1.dev18 → synodic_client-0.0.1.dev20}/tests/unit/windows/test_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev18
3
+ Version: 0.0.1.dev20
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
@@ -8,9 +8,9 @@ Project-URL: repository, https://github.com/synodic/synodic-client
8
8
  Requires-Python: <3.15,>=3.14
9
9
  Requires-Dist: pyside6>=6.10.2
10
10
  Requires-Dist: packaging>=26.0
11
- Requires-Dist: porringer>=0.2.1.dev10
11
+ Requires-Dist: porringer>=0.2.1.dev20
12
12
  Requires-Dist: velopack>=0.0.1369.dev7516
13
- Requires-Dist: typer>=0.23.0
13
+ Requires-Dist: typer>=0.23.1
14
14
  Description-Content-Type: text/markdown
15
15
 
16
16
  # Synodic Client
@@ -10,11 +10,11 @@ requires-python = ">=3.14, <3.15"
10
10
  dependencies = [
11
11
  "pyside6>=6.10.2",
12
12
  "packaging>=26.0",
13
- "porringer>=0.2.1.dev10",
13
+ "porringer>=0.2.1.dev20",
14
14
  "velopack>=0.0.1369.dev7516",
15
- "typer>=0.23.0",
15
+ "typer>=0.23.1",
16
16
  ]
17
- version = "0.0.1.dev18"
17
+ version = "0.0.1.dev20"
18
18
 
19
19
  [project.license]
20
20
  text = "LGPL-3.0-or-later"
@@ -31,10 +31,10 @@ synodic-client = "synodic_client.application.qt:application"
31
31
 
32
32
  [dependency-groups]
33
33
  build = [
34
- "pyinstaller>=6.18.0",
34
+ "pyinstaller>=6.19.0",
35
35
  ]
36
36
  lint = [
37
- "ruff>=0.15.0",
37
+ "ruff>=0.15.1",
38
38
  "pyrefly>=0.52.0",
39
39
  ]
40
40
  test = [
@@ -0,0 +1 @@
1
+ __version__ = '0.0.1.dev20'
@@ -122,6 +122,12 @@ def application(*, uri: str | None = None, dev_mode: bool = False) -> None:
122
122
  # Activate dev-mode namespacing before anything reads config paths.
123
123
  set_dev_mode(dev_mode)
124
124
 
125
+ # Configure logging before Velopack so install/uninstall hooks and
126
+ # first-run diagnostics are captured in the log file.
127
+ configure_logging()
128
+ logger = logging.getLogger('synodic_client')
129
+ _install_exception_hook(logger)
130
+
125
131
  if not dev_mode:
126
132
  # Initialize Velopack early, before any UI.
127
133
  # Console window suppression for subprocesses is handled by the
@@ -129,10 +135,6 @@ def application(*, uri: str | None = None, dev_mode: bool = False) -> None:
129
135
  initialize_velopack()
130
136
  register_protocol(sys.executable)
131
137
 
132
- configure_logging()
133
- logger = logging.getLogger('synodic_client')
134
- _install_exception_hook(logger)
135
-
136
138
  if uri:
137
139
  logger.info('Received URI: %s', uri)
138
140
 
@@ -17,7 +17,7 @@ from typing import Any
17
17
  import velopack
18
18
  from packaging.version import Version
19
19
 
20
- from synodic_client.protocol import register_protocol, remove_protocol
20
+ from synodic_client.protocol import remove_protocol
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
@@ -102,6 +102,14 @@ class Updater:
102
102
  self._state = UpdateState.NO_UPDATE
103
103
  self._update_info: UpdateInfo | None = None
104
104
  self._velopack_manager: Any = None
105
+ self._velopack_not_installed: bool = False
106
+
107
+ logger.info(
108
+ 'Updater created: version=%s, channel=%s, repo=%s',
109
+ self._current_version,
110
+ self._config.channel_name,
111
+ self._config.repo_url,
112
+ )
105
113
 
106
114
  @property
107
115
  def state(self) -> UpdateState:
@@ -110,12 +118,17 @@ class Updater:
110
118
 
111
119
  @property
112
120
  def is_installed(self) -> bool:
113
- """Check if running as a Velopack-installed application."""
121
+ """Check if running as a Velopack-installed application.
122
+
123
+ Delegates to ``_get_velopack_manager`` which creates the
124
+ ``UpdateManager``. The SDK constructor raises ``RuntimeError``
125
+ with *"not properly installed"* when no Velopack manifest is
126
+ found; that specific error is treated as "not installed" while
127
+ all other failures propagate.
128
+ """
114
129
  try:
115
- manager = self._get_velopack_manager()
116
- # If we can get the manager and it has a version, we're installed
117
- return manager is not None
118
- except Exception:
130
+ return self._get_velopack_manager() is not None
131
+ except RuntimeError:
119
132
  return False
120
133
 
121
134
  def check_for_update(self) -> UpdateInfo:
@@ -187,6 +200,7 @@ class Updater:
187
200
  return False
188
201
 
189
202
  self._state = UpdateState.DOWNLOADING
203
+ logger.info('Starting update download for %s', self._update_info._velopack_info)
190
204
 
191
205
  try:
192
206
  manager = self._get_velopack_manager()
@@ -284,42 +298,58 @@ class Updater:
284
298
  self._update_info.error = str(e)
285
299
  raise
286
300
 
301
+ _NOT_INSTALLED_SENTINEL = 'not properly installed'
302
+ """Substring the Velopack SDK includes in its ``RuntimeError`` when
303
+ the application was not installed via Velopack."""
304
+
287
305
  def _get_velopack_manager(self) -> Any:
288
306
  """Get or create the Velopack UpdateManager.
289
307
 
290
308
  Returns:
291
- UpdateManager instance, or None if not installed via Velopack
309
+ UpdateManager instance, or ``None`` when the application is
310
+ not running from a Velopack installation.
311
+
312
+ Raises:
313
+ RuntimeError: If the ``UpdateManager`` could not be created
314
+ for a reason *other* than the app not being installed
315
+ (e.g. a genuine SDK or configuration problem).
292
316
  """
293
317
  if self._velopack_manager is not None:
294
318
  return self._velopack_manager
295
319
 
320
+ if self._velopack_not_installed:
321
+ return None
322
+
296
323
  try:
297
- options = velopack.UpdateOptions()
298
- options.allow_version_downgrade = False
299
- options.explicit_channel = self._config.channel_name
324
+ options = velopack.UpdateOptions(
325
+ AllowVersionDowngrade=False,
326
+ )
327
+ options.ExplicitChannel = self._config.channel_name
300
328
 
301
329
  self._velopack_manager = velopack.UpdateManager(
302
330
  self._config.repo_url,
303
331
  options,
304
332
  )
333
+ logger.debug(
334
+ 'Velopack manager created: app_id=%s, version=%s, portable=%s',
335
+ self._velopack_manager.get_app_id(),
336
+ self._velopack_manager.get_current_version(),
337
+ self._velopack_manager.get_is_portable(),
338
+ )
305
339
  return self._velopack_manager
340
+ except RuntimeError as e:
341
+ if self._NOT_INSTALLED_SENTINEL in str(e).lower():
342
+ logger.debug('Not a Velopack install: %s', e)
343
+ self._velopack_not_installed = True
344
+ return None
345
+ logger.warning('Velopack manager creation failed: %s', e)
346
+ raise
306
347
  except Exception as e:
307
- logger.debug('Failed to create Velopack manager: %s', e)
308
- return None
309
-
348
+ logger.warning('Velopack manager creation failed: %s', e)
349
+ raise RuntimeError(f'Failed to create Velopack UpdateManager: {e}') from e
310
350
 
311
- def _on_after_install(version: str) -> None: # noqa: ARG001
312
- """Velopack hook: called after the app is installed.
313
351
 
314
- Registers the ``synodic://`` URI protocol handler.
315
-
316
- Args:
317
- version: The installed version string (provided by Velopack).
318
- """
319
- register_protocol(sys.executable)
320
-
321
-
322
- def _on_before_uninstall(version: str) -> None: # noqa: ARG001
352
+ def _on_before_uninstall(version: str) -> None:
323
353
  """Velopack hook: called before the app is uninstalled.
324
354
 
325
355
  Removes the ``synodic://`` URI protocol handler registration.
@@ -327,7 +357,12 @@ def _on_before_uninstall(version: str) -> None: # noqa: ARG001
327
357
  Args:
328
358
  version: The current version string (provided by Velopack).
329
359
  """
330
- remove_protocol()
360
+ logger.info('Velopack uninstall hook fired for version %s', version)
361
+ try:
362
+ remove_protocol()
363
+ logger.info('Protocol handler removed successfully')
364
+ except Exception:
365
+ logger.warning('Protocol removal failed during uninstall hook', exc_info=True)
331
366
 
332
367
 
333
368
  def initialize_velopack() -> None:
@@ -337,13 +372,14 @@ def initialize_velopack() -> None:
337
372
  before any UI is shown. Velopack may need to perform cleanup or apply
338
373
  pending updates.
339
374
 
340
- On Windows, install/uninstall hooks register the ``synodic://`` URI protocol.
375
+ On Windows, the uninstall hook removes the ``synodic://`` URI protocol.
376
+ Protocol registration happens on every app launch (see ``qt.application``).
341
377
  """
378
+ logger.info('Initializing Velopack (exe=%s)', sys.executable)
342
379
  try:
343
380
  app = velopack.App()
344
- app.on_after_install_fast_callback(_on_after_install)
345
381
  app.on_before_uninstall_fast_callback(_on_before_uninstall)
346
382
  app.run()
347
- logger.debug('Velopack initialized')
383
+ logger.info('Velopack initialized successfully')
348
384
  except Exception as e:
349
- logger.debug('Velopack initialization skipped: %s', e)
385
+ logger.info('Velopack initialization skipped (not a Velopack install): %s', e)
@@ -7,4 +7,4 @@ skipped automatically.
7
7
 
8
8
  import pytest
9
9
 
10
- pytest.importorskip("PySide6.QtWidgets", reason="PySide6 requires system Qt libraries")
10
+ pytest.importorskip('PySide6.QtWidgets', reason='PySide6 requires system Qt libraries')
@@ -179,7 +179,6 @@ class TestUpdater:
179
179
  @staticmethod
180
180
  def test_is_installed_not_velopack(updater: Updater) -> None:
181
181
  """Verify is_installed returns False in test environment."""
182
- # Tests run in non-Velopack environment
183
182
  with patch.object(updater, '_get_velopack_manager', return_value=None):
184
183
  assert updater.is_installed is False
185
184
 
@@ -191,9 +190,9 @@ class TestUpdater:
191
190
  assert updater.is_installed is True
192
191
 
193
192
  @staticmethod
194
- def test_is_installed_handles_exception(updater: Updater) -> None:
195
- """Verify is_installed returns False when exception occurs."""
196
- with patch.object(updater, '_get_velopack_manager', side_effect=Exception('Test')):
193
+ def test_is_installed_handles_runtime_error(updater: Updater) -> None:
194
+ """Verify is_installed returns False when RuntimeError is raised."""
195
+ with patch.object(updater, '_get_velopack_manager', side_effect=RuntimeError('fail')):
197
196
  assert updater.is_installed is False
198
197
 
199
198
 
@@ -454,3 +453,67 @@ class TestInitializeVelopack:
454
453
  with patch('synodic_client.updater.velopack.App', return_value=mock_app):
455
454
  # Should not raise
456
455
  initialize_velopack()
456
+
457
+
458
+ class TestGetVelopackManager:
459
+ """Tests for _get_velopack_manager install detection via the SDK."""
460
+
461
+ _PATCH_OPTIONS = patch('synodic_client.updater.velopack.UpdateOptions')
462
+
463
+ @staticmethod
464
+ def test_not_installed_returns_none(updater: Updater) -> None:
465
+ """Verify manager returns None when SDK says 'not properly installed'."""
466
+ error = RuntimeError('This application is not properly installed: Could not auto-locate app manifest')
467
+ with (
468
+ TestGetVelopackManager._PATCH_OPTIONS,
469
+ patch('synodic_client.updater.velopack.UpdateManager', side_effect=error),
470
+ ):
471
+ assert updater._get_velopack_manager() is None
472
+
473
+ @staticmethod
474
+ def test_not_installed_sentinel_cached(updater: Updater) -> None:
475
+ """Verify that once detected as not-installed, the SDK is not called again."""
476
+ error = RuntimeError('This application is not properly installed')
477
+ with (
478
+ TestGetVelopackManager._PATCH_OPTIONS,
479
+ patch('synodic_client.updater.velopack.UpdateManager', side_effect=error) as mock_cls,
480
+ ):
481
+ updater._get_velopack_manager()
482
+ updater._get_velopack_manager()
483
+ mock_cls.assert_called_once()
484
+
485
+ @staticmethod
486
+ def test_real_error_propagates(updater: Updater) -> None:
487
+ """Verify non-install RuntimeErrors propagate instead of returning None."""
488
+ error = RuntimeError('Some other SDK failure')
489
+ with (
490
+ TestGetVelopackManager._PATCH_OPTIONS,
491
+ patch('synodic_client.updater.velopack.UpdateManager', side_effect=error),
492
+ pytest.raises(RuntimeError, match='Some other SDK failure'),
493
+ ):
494
+ updater._get_velopack_manager()
495
+
496
+ @staticmethod
497
+ def test_non_runtime_error_propagates(updater: Updater) -> None:
498
+ """Verify non-RuntimeError exceptions are wrapped and propagated."""
499
+ error = ValueError('bad config')
500
+ with (
501
+ TestGetVelopackManager._PATCH_OPTIONS,
502
+ patch('synodic_client.updater.velopack.UpdateManager', side_effect=error),
503
+ pytest.raises(RuntimeError, match='Failed to create Velopack UpdateManager'),
504
+ ):
505
+ updater._get_velopack_manager()
506
+
507
+ @staticmethod
508
+ def test_success_caches_manager(updater: Updater) -> None:
509
+ """Verify successful manager creation is cached."""
510
+ mock_manager = MagicMock()
511
+ with (
512
+ TestGetVelopackManager._PATCH_OPTIONS,
513
+ patch('synodic_client.updater.velopack.UpdateManager', return_value=mock_manager) as mock_cls,
514
+ ):
515
+ result1 = updater._get_velopack_manager()
516
+ result2 = updater._get_velopack_manager()
517
+ assert result1 is mock_manager
518
+ assert result2 is mock_manager
519
+ mock_cls.assert_called_once()
@@ -1 +0,0 @@
1
- __version__ = '0.0.1.dev18'