synodic-client 0.0.1.dev74__tar.gz → 0.0.1.dev75__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.dev74 → synodic_client-0.0.1.dev75}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/debug.py +27 -46
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/qt.py +6 -23
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/__init__.py +11 -17
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/action_card.py +26 -35
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/install.py +6 -23
- synodic_client-0.0.1.dev75/synodic_client/application/screen/install_workers.py +152 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/schema.py +2 -9
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/screen.py +6 -63
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/tool_update_controller.py +40 -83
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/update_controller.py +7 -11
- synodic_client-0.0.1.dev75/synodic_client/application/workers.py +20 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/cli/config.py +6 -9
- synodic_client-0.0.1.dev75/synodic_client/cli/debug.py +187 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/cli/tool.py +1 -3
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/operations/__init__.py +47 -3
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/operations/config.py +21 -0
- synodic_client-0.0.1.dev75/synodic_client/operations/install.py +272 -0
- synodic_client-0.0.1.dev75/synodic_client/operations/project.py +270 -0
- synodic_client-0.0.1.dev75/synodic_client/operations/schema.py +321 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/operations/tool.py +52 -43
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/resolution.py +5 -70
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/subprocess_patch.py +6 -1
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/updater.py +1 -1
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_action_card.py +46 -33
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_gather_packages.py +11 -11
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_install_preview.py +12 -12
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_tray_window_show.py +6 -5
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_update_controller.py +14 -13
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_cli.py +69 -18
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_workers.py +21 -23
- synodic_client-0.0.1.dev74/synodic_client/application/screen/install_workers.py +0 -251
- synodic_client-0.0.1.dev74/synodic_client/application/workers.py +0 -214
- synodic_client-0.0.1.dev74/synodic_client/cli/debug.py +0 -85
- synodic_client-0.0.1.dev74/synodic_client/operations/install.py +0 -150
- synodic_client-0.0.1.dev74/synodic_client/operations/project.py +0 -87
- synodic_client-0.0.1.dev74/synodic_client/operations/schema.py +0 -180
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/README.md +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/update_model.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/cli/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/cli/context.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/cli/output.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/cli/project.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/cli/update.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/operations/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/operations/update.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/operations/test_config.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/operations/test_install.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/operations/test_project.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/operations/test_tool.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/operations/test_update.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/tests/unit/windows/test_startup.py +0 -0
{synodic_client-0.0.1.dev74 → synodic_client-0.0.1.dev75}/synodic_client/application/debug.py
RENAMED
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
from typing import TYPE_CHECKING
|
|
18
18
|
|
|
19
19
|
from synodic_client.config import is_dev_mode
|
|
20
|
+
from synodic_client.operations.schema import DEBUG_ACTIONS
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
23
|
from porringer.api import API
|
|
@@ -32,19 +33,7 @@ if TYPE_CHECKING:
|
|
|
32
33
|
|
|
33
34
|
logger = logging.getLogger(__name__)
|
|
34
35
|
|
|
35
|
-
_ACTIONS
|
|
36
|
-
'check_update': 'Trigger a self-update check.',
|
|
37
|
-
'tool_update': 'Run tool/package updates for all plugins.',
|
|
38
|
-
'refresh_data': 'Mark cached data as stale (next refresh re-fetches).',
|
|
39
|
-
'show_main': 'Show and raise the main window.',
|
|
40
|
-
'show_settings': 'Show the settings window.',
|
|
41
|
-
'apply_update': 'Apply a downloaded update and restart.',
|
|
42
|
-
'list_projects': 'List cached project directories with validation status.',
|
|
43
|
-
'add_project': 'Add a directory to the project cache. Arg: <path>',
|
|
44
|
-
'remove_project': 'Remove a directory from the project cache. Arg: <path>',
|
|
45
|
-
'project_status': 'Dump per-action preview status. Arg (optional): <path>',
|
|
46
|
-
'select_project': 'Select a project in the sidebar. Arg: <path>',
|
|
47
|
-
}
|
|
36
|
+
_ACTIONS = DEBUG_ACTIONS
|
|
48
37
|
|
|
49
38
|
|
|
50
39
|
@dataclasses.dataclass
|
|
@@ -164,49 +153,39 @@ class DebugHandler:
|
|
|
164
153
|
|
|
165
154
|
def _handle_list_projects(self) -> str:
|
|
166
155
|
"""List all cached project directories with validation status."""
|
|
167
|
-
from synodic_client.operations.project import
|
|
156
|
+
from synodic_client.operations.project import run_project_action
|
|
168
157
|
|
|
169
|
-
|
|
170
|
-
return json.dumps({'projects': [dataclasses.asdict(p) for p in projects]})
|
|
158
|
+
return json.dumps(run_project_action('list_projects', None, self._s.porringer))
|
|
171
159
|
|
|
172
160
|
def _handle_add_project(self, arg: str | None) -> str:
|
|
173
161
|
"""Add a directory to the porringer cache."""
|
|
174
|
-
|
|
175
|
-
return json.dumps({'error': 'add_project requires a path argument'})
|
|
176
|
-
|
|
177
|
-
from synodic_client.operations.project import add_project
|
|
162
|
+
from synodic_client.operations.project import run_project_action
|
|
178
163
|
|
|
179
|
-
|
|
180
|
-
add_project(self._s.porringer, arg)
|
|
181
|
-
except (NotADirectoryError, ValueError) as exc:
|
|
182
|
-
return json.dumps({'error': str(exc)})
|
|
164
|
+
result = run_project_action('add_project', arg, self._s.porringer)
|
|
183
165
|
|
|
184
|
-
if
|
|
185
|
-
self._s.coordinator
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
166
|
+
if 'error' not in result:
|
|
167
|
+
if self._s.coordinator is not None:
|
|
168
|
+
self._s.coordinator.invalidate()
|
|
169
|
+
projects_view = self._s.main_window._projects_view
|
|
170
|
+
if projects_view is not None:
|
|
171
|
+
projects_view.refresh()
|
|
190
172
|
|
|
191
|
-
return json.dumps(
|
|
173
|
+
return json.dumps(result)
|
|
192
174
|
|
|
193
175
|
def _handle_remove_project(self, arg: str | None) -> str:
|
|
194
176
|
"""Remove a directory from the porringer cache."""
|
|
195
|
-
|
|
196
|
-
return json.dumps({'error': 'remove_project requires a path argument'})
|
|
177
|
+
from synodic_client.operations.project import run_project_action
|
|
197
178
|
|
|
198
|
-
|
|
179
|
+
result = run_project_action('remove_project', arg, self._s.porringer)
|
|
199
180
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
self._s.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if projects_view is not None:
|
|
207
|
-
projects_view.refresh()
|
|
181
|
+
if 'error' not in result:
|
|
182
|
+
if self._s.coordinator is not None:
|
|
183
|
+
self._s.coordinator.invalidate()
|
|
184
|
+
projects_view = self._s.main_window._projects_view
|
|
185
|
+
if projects_view is not None:
|
|
186
|
+
projects_view.refresh()
|
|
208
187
|
|
|
209
|
-
return json.dumps(
|
|
188
|
+
return json.dumps(result)
|
|
210
189
|
|
|
211
190
|
def _handle_project_status(self, arg: str | None) -> str:
|
|
212
191
|
"""Dump per-action preview status for a project."""
|
|
@@ -243,9 +222,11 @@ class DebugHandler:
|
|
|
243
222
|
entry['installer'] = act.installer
|
|
244
223
|
actions.append(entry)
|
|
245
224
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
225
|
+
from synodic_client.operations.schema import classify_status
|
|
226
|
+
|
|
227
|
+
needed = sum(1 for s in model.action_states if classify_status(s.status) == 'needed')
|
|
228
|
+
satisfied = sum(1 for s in model.action_states if classify_status(s.status) == 'satisfied')
|
|
229
|
+
pending = sum(1 for s in model.action_states if classify_status(s.status) == 'pending')
|
|
249
230
|
upgradable = len(model.upgradable_keys)
|
|
250
231
|
|
|
251
232
|
return json.dumps({
|
|
@@ -13,7 +13,6 @@ from collections.abc import Callable
|
|
|
13
13
|
|
|
14
14
|
import qasync
|
|
15
15
|
from porringer.api import API
|
|
16
|
-
from porringer.schema import LocalConfiguration
|
|
17
16
|
from PySide6.QtCore import QEvent, QObject, Qt, QTimer
|
|
18
17
|
from PySide6.QtWidgets import QApplication, QWidget
|
|
19
18
|
|
|
@@ -29,12 +28,9 @@ from synodic_client.application.uri import parse_uri
|
|
|
29
28
|
from synodic_client.client import Client
|
|
30
29
|
from synodic_client.config import set_dev_mode
|
|
31
30
|
from synodic_client.logging import configure_logging, log_path, set_debug_level
|
|
31
|
+
from synodic_client.operations.bootstrap import init_services
|
|
32
32
|
from synodic_client.protocol import extract_uri_from_args
|
|
33
|
-
from synodic_client.resolution import
|
|
34
|
-
ResolvedConfig,
|
|
35
|
-
resolve_config,
|
|
36
|
-
resolve_update_config,
|
|
37
|
-
)
|
|
33
|
+
from synodic_client.resolution import ResolvedConfig
|
|
38
34
|
from synodic_client.subprocess_patch import apply as _apply_subprocess_patch
|
|
39
35
|
from synodic_client.updater import initialize_velopack
|
|
40
36
|
|
|
@@ -42,27 +38,14 @@ from synodic_client.updater import initialize_velopack
|
|
|
42
38
|
def _init_services(logger: logging.Logger) -> tuple[Client, API, ResolvedConfig]:
|
|
43
39
|
"""Create and configure core services.
|
|
44
40
|
|
|
41
|
+
Delegates to :func:`~synodic_client.operations.bootstrap.init_services`
|
|
42
|
+
and adds GUI-specific debug logging.
|
|
43
|
+
|
|
45
44
|
Returns:
|
|
46
45
|
A (Client, porringer API, resolved config) tuple.
|
|
47
46
|
"""
|
|
48
|
-
config =
|
|
49
|
-
client = Client()
|
|
50
|
-
|
|
51
|
-
local_config = LocalConfiguration()
|
|
52
|
-
porringer = API(local_config)
|
|
53
|
-
|
|
54
|
-
update_config = resolve_update_config(config)
|
|
55
|
-
client.initialize_updater(update_config)
|
|
47
|
+
client, porringer, config = init_services()
|
|
56
48
|
|
|
57
|
-
cached_dirs = porringer.cache.list_directories()
|
|
58
|
-
|
|
59
|
-
logger.info(
|
|
60
|
-
'Synodic Client v%s started (channel: %s, source: %s, cached_projects: %d)',
|
|
61
|
-
client.version,
|
|
62
|
-
update_config.channel.name,
|
|
63
|
-
update_config.repo_url,
|
|
64
|
-
len(cached_dirs),
|
|
65
|
-
)
|
|
66
49
|
logger.debug(
|
|
67
50
|
'Resolved config: update_source=%s update_channel=%s auto_update=%dm tool_update=%dm '
|
|
68
51
|
'auto_apply=%s auto_start=%s debug_logging=%s prerelease_packages=%s plugin_auto_update=%s',
|
|
@@ -9,9 +9,19 @@ from __future__ import annotations
|
|
|
9
9
|
import re
|
|
10
10
|
from datetime import UTC, datetime
|
|
11
11
|
|
|
12
|
-
from porringer.schema import SetupAction, SetupActionResult
|
|
12
|
+
from porringer.schema import SetupAction, SetupActionResult
|
|
13
13
|
from porringer.schema.plugin import PluginKind
|
|
14
14
|
|
|
15
|
+
from synodic_client.operations.schema import (
|
|
16
|
+
SKIP_REASON_LABELS as SKIP_REASON_LABELS,
|
|
17
|
+
)
|
|
18
|
+
from synodic_client.operations.schema import (
|
|
19
|
+
resolve_action_status as resolve_action_status,
|
|
20
|
+
)
|
|
21
|
+
from synodic_client.operations.schema import (
|
|
22
|
+
skip_reason_label as skip_reason_label,
|
|
23
|
+
)
|
|
24
|
+
|
|
15
25
|
_SECONDS_PER_MINUTE = 60
|
|
16
26
|
_MINUTES_PER_HOUR = 60
|
|
17
27
|
_HOURS_PER_DAY = 24
|
|
@@ -48,22 +58,6 @@ def plugin_kind_group_label(kind: PluginKind) -> str:
|
|
|
48
58
|
return PLUGIN_KIND_GROUP_LABELS.get(kind, kind.name.replace('_', ' ').title())
|
|
49
59
|
|
|
50
60
|
|
|
51
|
-
SKIP_REASON_LABELS: dict[SkipReason, str] = {
|
|
52
|
-
SkipReason.ALREADY_INSTALLED: 'Already installed',
|
|
53
|
-
SkipReason.NOT_INSTALLED: 'Not installed',
|
|
54
|
-
SkipReason.ALREADY_LATEST: 'Already latest',
|
|
55
|
-
SkipReason.NO_PROJECT_DIRECTORY: 'No project directory',
|
|
56
|
-
SkipReason.UPDATE_AVAILABLE: 'Update available',
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def skip_reason_label(reason: SkipReason | None) -> str:
|
|
61
|
-
"""Return a human-readable label for a skip reason."""
|
|
62
|
-
if reason is None:
|
|
63
|
-
return 'Skipped'
|
|
64
|
-
return SKIP_REASON_LABELS.get(reason, reason.name.replace('_', ' ').capitalize())
|
|
65
|
-
|
|
66
|
-
|
|
67
61
|
def format_cli_command(
|
|
68
62
|
action: SetupAction,
|
|
69
63
|
*,
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import logging
|
|
15
15
|
|
|
16
16
|
from porringer.backend.command.core.action_builder import PHASE_ORDER
|
|
17
|
-
from porringer.schema import SetupAction, SetupActionResult
|
|
17
|
+
from porringer.schema import SetupAction, SetupActionResult
|
|
18
18
|
from porringer.schema.plugin import PluginKind
|
|
19
19
|
from PySide6.QtCore import Qt, QTimer, Signal
|
|
20
20
|
from PySide6.QtGui import QColor
|
|
@@ -443,56 +443,47 @@ class ActionCard(QFrame):
|
|
|
443
443
|
# Public API — dry-run check result
|
|
444
444
|
# ------------------------------------------------------------------
|
|
445
445
|
|
|
446
|
-
def set_check_result(self, result: SetupActionResult) -> None:
|
|
446
|
+
def set_check_result(self, result: SetupActionResult, status: str) -> None:
|
|
447
447
|
"""Update the card with a dry-run check result.
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
* **Skipped (other)** — muted satisfied badge.
|
|
453
|
-
* **Failed** — red "Failed" badge with diagnostic tooltip.
|
|
454
|
-
* **Bare command** (kind=None, success=True) — keeps "Pending".
|
|
455
|
-
* **Project sync** (kind=PROJECT, success=True) — "Ready".
|
|
456
|
-
* **Needed** — default blue badge.
|
|
449
|
+
The *status* string is pre-resolved by the operations layer via
|
|
450
|
+
:func:`resolve_action_status`. This method maps it to the
|
|
451
|
+
appropriate style and updates the version / tooltip columns.
|
|
457
452
|
|
|
458
453
|
Args:
|
|
459
454
|
result: The action check result from the preview worker.
|
|
455
|
+
status: Pre-resolved human-readable status label.
|
|
460
456
|
"""
|
|
461
457
|
if self._is_skeleton:
|
|
462
458
|
return
|
|
463
459
|
|
|
464
460
|
self._stop_spinner()
|
|
465
461
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
462
|
+
# Status-to-style mapping
|
|
463
|
+
_STATUS_STYLES: dict[str, str] = {
|
|
464
|
+
'Update available': ACTION_CARD_STATUS_UPDATE,
|
|
465
|
+
'Failed': ACTION_CARD_STATUS_FAILED,
|
|
466
|
+
'Pending': ACTION_CARD_STATUS_PENDING,
|
|
467
|
+
'Ready': ACTION_CARD_STATUS_SATISFIED,
|
|
468
|
+
'Needed': ACTION_CARD_STATUS_NEEDED,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
style = _STATUS_STYLES.get(status, ACTION_CARD_STATUS_SATISFIED)
|
|
472
|
+
display = status
|
|
473
|
+
|
|
474
|
+
# Satisfied (skipped) statuses get a checkmark prefix
|
|
475
|
+
if result.skipped and status not in _STATUS_STYLES:
|
|
476
|
+
display = f'\u2713 {status}'
|
|
477
|
+
|
|
478
|
+
if not result.success and status == 'Failed':
|
|
478
479
|
logger.warning(
|
|
479
480
|
'Dry-run check failed for %s: %s',
|
|
480
481
|
self._action.description if self._action else '(unknown)',
|
|
481
482
|
result.message or 'unknown error',
|
|
482
483
|
)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
self._status_label.setText(label)
|
|
487
|
-
self._status_label.setStyleSheet(ACTION_CARD_STATUS_PENDING)
|
|
488
|
-
elif self._action is not None and self._action.kind == PluginKind.PROJECT:
|
|
489
|
-
label = 'Ready'
|
|
490
|
-
self._status_label.setText(label)
|
|
491
|
-
self._status_label.setStyleSheet(ACTION_CARD_STATUS_SATISFIED)
|
|
492
|
-
else:
|
|
493
|
-
label = 'Needed'
|
|
494
|
-
self._status_label.setText(label)
|
|
495
|
-
self._status_label.setStyleSheet(ACTION_CARD_STATUS_NEEDED)
|
|
484
|
+
|
|
485
|
+
self._status_label.setText(display)
|
|
486
|
+
self._status_label.setStyleSheet(style)
|
|
496
487
|
|
|
497
488
|
# Surface diagnostic detail (e.g. SCM URL mismatch) as a tooltip
|
|
498
489
|
if result.message:
|
|
@@ -27,7 +27,6 @@ from porringer.schema import (
|
|
|
27
27
|
SubActionProgress,
|
|
28
28
|
SyncStrategy,
|
|
29
29
|
)
|
|
30
|
-
from porringer.schema.plugin import PluginKind
|
|
31
30
|
from PySide6.QtCore import Qt, QTimer, Signal
|
|
32
31
|
from PySide6.QtGui import QShowEvent
|
|
33
32
|
from PySide6.QtWidgets import (
|
|
@@ -45,7 +44,6 @@ from PySide6.QtWidgets import (
|
|
|
45
44
|
)
|
|
46
45
|
|
|
47
46
|
from synodic_client.application.package_state import PackageStateStore
|
|
48
|
-
from synodic_client.application.screen import skip_reason_label
|
|
49
47
|
from synodic_client.application.screen.action_card import ActionCardList
|
|
50
48
|
from synodic_client.application.screen.card import CardFrame
|
|
51
49
|
from synodic_client.application.screen.install_workers import run_install, run_preview
|
|
@@ -615,28 +613,13 @@ class SetupPreviewWidget(QWidget):
|
|
|
615
613
|
if preview.metadata:
|
|
616
614
|
self.metadata_ready.emit(preview)
|
|
617
615
|
|
|
618
|
-
def _on_action_checked(self, row: int, result: SetupActionResult) -> None:
|
|
616
|
+
def _on_action_checked(self, row: int, result: SetupActionResult, status: str) -> None:
|
|
619
617
|
"""Update the model and action card with a dry-run result."""
|
|
620
618
|
m = self._model
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
elif result.skipped:
|
|
626
|
-
label = skip_reason_label(result.skip_reason)
|
|
627
|
-
elif not result.success:
|
|
628
|
-
label = 'Failed'
|
|
629
|
-
else:
|
|
630
|
-
# Bare commands (kind=None) and PROJECT actions always return
|
|
631
|
-
# success=True from porringer's dry_run_action. Don't
|
|
632
|
-
# overwrite their initial status to "Needed".
|
|
633
|
-
action = m.action_states[row].action if 0 <= row < len(m.action_states) else None
|
|
634
|
-
if action is not None and action.kind is None:
|
|
635
|
-
label = 'Pending'
|
|
636
|
-
elif action is not None and action.kind == PluginKind.PROJECT:
|
|
637
|
-
label = 'Ready'
|
|
638
|
-
else:
|
|
639
|
-
label = 'Needed'
|
|
619
|
+
label = status
|
|
620
|
+
|
|
621
|
+
if result.skipped and result.skip_reason == SkipReason.UPDATE_AVAILABLE and 0 <= row < len(m.action_states):
|
|
622
|
+
m.upgradable_keys.add(m.action_states[row].action)
|
|
640
623
|
|
|
641
624
|
if 0 <= row < len(m.action_states):
|
|
642
625
|
m.action_states[row].status = label
|
|
@@ -657,7 +640,7 @@ class SetupPreviewWidget(QWidget):
|
|
|
657
640
|
action = m.preview.actions[row]
|
|
658
641
|
card = self._card_list.get_card(action)
|
|
659
642
|
if card is not None:
|
|
660
|
-
card.set_check_result(result)
|
|
643
|
+
card.set_check_result(result, status)
|
|
661
644
|
|
|
662
645
|
# Record in shared store so ToolsView can reflect the update
|
|
663
646
|
if self._package_store is not None and action.installer and action.package:
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Async worker coroutines for install and preview operations.
|
|
2
|
+
|
|
3
|
+
Contains ``run_install``, ``run_preview``, and supporting helpers that
|
|
4
|
+
stream porringer events back to the GUI via callbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from porringer.api import API
|
|
14
|
+
from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
15
|
+
from porringer.schema import (
|
|
16
|
+
ActionCompletedEvent,
|
|
17
|
+
ActionStartedEvent,
|
|
18
|
+
ManifestLoadedEvent,
|
|
19
|
+
SetupAction,
|
|
20
|
+
SetupActionResult,
|
|
21
|
+
SetupParameters,
|
|
22
|
+
SetupResults,
|
|
23
|
+
SubActionProgressEvent,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from synodic_client.application.screen.schema import (
|
|
27
|
+
InstallCallbacks,
|
|
28
|
+
InstallConfig,
|
|
29
|
+
PreviewCallbacks,
|
|
30
|
+
PreviewConfig,
|
|
31
|
+
)
|
|
32
|
+
from synodic_client.application.uri import safe_rmtree
|
|
33
|
+
from synodic_client.operations.install import preview_manifest_stream
|
|
34
|
+
from synodic_client.operations.schema import (
|
|
35
|
+
PreviewActionChecked,
|
|
36
|
+
PreviewManifestParsed,
|
|
37
|
+
PreviewPluginsQueried,
|
|
38
|
+
PreviewReady,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# run_install — execute setup actions via porringer
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def run_install(
|
|
50
|
+
porringer: API,
|
|
51
|
+
manifest_path: Path,
|
|
52
|
+
config: InstallConfig | None = None,
|
|
53
|
+
callbacks: InstallCallbacks | None = None,
|
|
54
|
+
*,
|
|
55
|
+
plugins: DiscoveredPlugins | None = None,
|
|
56
|
+
) -> SetupResults:
|
|
57
|
+
"""Execute setup actions via porringer and stream progress.
|
|
58
|
+
|
|
59
|
+
Runs on the caller's event loop (typically the qasync main-thread
|
|
60
|
+
loop). Callbacks are invoked between ``await`` points so the GUI
|
|
61
|
+
stays responsive without cross-thread signalling.
|
|
62
|
+
"""
|
|
63
|
+
cfg = config or InstallConfig()
|
|
64
|
+
cb = callbacks or InstallCallbacks()
|
|
65
|
+
params = SetupParameters(
|
|
66
|
+
paths=[manifest_path],
|
|
67
|
+
project_directory=cfg.project_directory,
|
|
68
|
+
strategy=cfg.strategy,
|
|
69
|
+
prerelease_packages=cfg.prerelease_packages,
|
|
70
|
+
)
|
|
71
|
+
actions: list[SetupAction] = []
|
|
72
|
+
collected: list[SetupActionResult] = []
|
|
73
|
+
manifest_result: SetupResults | None = None
|
|
74
|
+
|
|
75
|
+
async for event in porringer.sync.execute_stream(params, plugins=plugins):
|
|
76
|
+
if isinstance(event, ManifestLoadedEvent):
|
|
77
|
+
manifest_result = event.manifest
|
|
78
|
+
actions = list(event.manifest.actions)
|
|
79
|
+
|
|
80
|
+
elif isinstance(event, ActionStartedEvent) and cb.on_action_started is not None:
|
|
81
|
+
cb.on_action_started(event.action)
|
|
82
|
+
|
|
83
|
+
elif isinstance(event, SubActionProgressEvent) and cb.on_sub_progress is not None:
|
|
84
|
+
cb.on_sub_progress(event.action, event.sub_action)
|
|
85
|
+
|
|
86
|
+
elif isinstance(event, ActionCompletedEvent):
|
|
87
|
+
collected.append(event.result)
|
|
88
|
+
if cb.on_progress is not None:
|
|
89
|
+
cb.on_progress(event.action, event.result)
|
|
90
|
+
|
|
91
|
+
return SetupResults(
|
|
92
|
+
actions=actions,
|
|
93
|
+
results=collected,
|
|
94
|
+
manifest_path=manifest_result.manifest_path if manifest_result else None,
|
|
95
|
+
metadata=manifest_result.metadata if manifest_result else None,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# run_preview — dry-run preview of a manifest
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def run_preview(
|
|
105
|
+
porringer: API,
|
|
106
|
+
url: str,
|
|
107
|
+
*,
|
|
108
|
+
config: PreviewConfig | None = None,
|
|
109
|
+
callbacks: PreviewCallbacks | None = None,
|
|
110
|
+
plugins: DiscoveredPlugins | None = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Download a manifest and perform a dry-run preview.
|
|
113
|
+
|
|
114
|
+
Delegates to :func:`preview_manifest_stream` in the operations
|
|
115
|
+
layer, then routes each :data:`PreviewEvent` to the appropriate
|
|
116
|
+
callback.
|
|
117
|
+
"""
|
|
118
|
+
logger.info('run_preview starting for: %s', url)
|
|
119
|
+
cb = callbacks or PreviewCallbacks()
|
|
120
|
+
cfg = config or PreviewConfig()
|
|
121
|
+
temp_dir: str | None = None
|
|
122
|
+
try:
|
|
123
|
+
async for event in preview_manifest_stream(
|
|
124
|
+
porringer,
|
|
125
|
+
url,
|
|
126
|
+
project_directory=cfg.project_directory,
|
|
127
|
+
prerelease_packages=cfg.prerelease_packages,
|
|
128
|
+
discovered=plugins,
|
|
129
|
+
):
|
|
130
|
+
if isinstance(event, PreviewManifestParsed):
|
|
131
|
+
temp_dir = event.temp_dir or None
|
|
132
|
+
if cb.on_manifest_parsed is not None:
|
|
133
|
+
cb.on_manifest_parsed(event.manifest, event.manifest_path, event.temp_dir)
|
|
134
|
+
|
|
135
|
+
elif isinstance(event, PreviewPluginsQueried) and cb.on_plugins_queried is not None:
|
|
136
|
+
cb.on_plugins_queried(event.availability, event.capabilities)
|
|
137
|
+
|
|
138
|
+
elif isinstance(event, PreviewReady):
|
|
139
|
+
if cb.on_preview_ready is not None:
|
|
140
|
+
cb.on_preview_ready(event.manifest, event.manifest_path, event.temp_dir)
|
|
141
|
+
|
|
142
|
+
elif isinstance(event, PreviewActionChecked) and cb.on_action_checked is not None:
|
|
143
|
+
cb.on_action_checked(event.index, event.result, event.status)
|
|
144
|
+
|
|
145
|
+
except asyncio.CancelledError:
|
|
146
|
+
if temp_dir:
|
|
147
|
+
safe_rmtree(temp_dir)
|
|
148
|
+
raise
|
|
149
|
+
except Exception:
|
|
150
|
+
if temp_dir:
|
|
151
|
+
safe_rmtree(temp_dir)
|
|
152
|
+
raise
|
|
@@ -323,8 +323,8 @@ class PreviewCallbacks:
|
|
|
323
323
|
on_preview_ready: Callable[[SetupResults, str, str], None] | None = None
|
|
324
324
|
"""``(SetupResults, manifest_path, temp_dir)`` — CLI commands resolved."""
|
|
325
325
|
|
|
326
|
-
on_action_checked: Callable[[int, SetupActionResult], None] | None = None
|
|
327
|
-
"""``(row_index, SetupActionResult)`` — per-action dry-run result."""
|
|
326
|
+
on_action_checked: Callable[[int, SetupActionResult, str], None] | None = None
|
|
327
|
+
"""``(row_index, SetupActionResult, status)`` — per-action dry-run result with resolved status."""
|
|
328
328
|
|
|
329
329
|
|
|
330
330
|
@dataclass(frozen=True, slots=True)
|
|
@@ -333,10 +333,3 @@ class PreviewConfig:
|
|
|
333
333
|
|
|
334
334
|
project_directory: Path | None = None
|
|
335
335
|
prerelease_packages: set[str] | None = None
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
@dataclass(slots=True)
|
|
339
|
-
class _DispatchState:
|
|
340
|
-
"""Mutable accumulator for :func:`_dispatch_preview_event`."""
|
|
341
|
-
|
|
342
|
-
got_parsed: bool = False
|
|
@@ -11,13 +11,11 @@ from porringer.backend.builder import Builder
|
|
|
11
11
|
from porringer.core.plugin_schema.plugin_manager import PluginManager
|
|
12
12
|
from porringer.core.plugin_schema.project_environment import ProjectEnvironment
|
|
13
13
|
from porringer.schema import (
|
|
14
|
-
ActionCompletedEvent,
|
|
15
14
|
ManifestDirectory,
|
|
16
15
|
ManifestParsedEvent,
|
|
17
16
|
PluginInfo,
|
|
18
17
|
SetupAction,
|
|
19
18
|
SetupParameters,
|
|
20
|
-
SkipReason,
|
|
21
19
|
SyncStrategy,
|
|
22
20
|
)
|
|
23
21
|
from porringer.schema.plugin import PluginKind, RuntimePackageResult
|
|
@@ -1045,14 +1043,10 @@ class ToolsView(QWidget):
|
|
|
1045
1043
|
"""
|
|
1046
1044
|
actions: list[SetupAction] = []
|
|
1047
1045
|
try:
|
|
1046
|
+
from synodic_client.operations.project import find_manifest
|
|
1047
|
+
|
|
1048
1048
|
path = Path(directory.path)
|
|
1049
|
-
|
|
1050
|
-
manifest_path: Path | None = None
|
|
1051
|
-
for fname in filenames:
|
|
1052
|
-
candidate = path / fname
|
|
1053
|
-
if candidate.exists():
|
|
1054
|
-
manifest_path = candidate
|
|
1055
|
-
break
|
|
1049
|
+
manifest_path = find_manifest(self._porringer, path)
|
|
1056
1050
|
|
|
1057
1051
|
if manifest_path is None:
|
|
1058
1052
|
return actions
|
|
@@ -1177,19 +1171,10 @@ class ToolsView(QWidget):
|
|
|
1177
1171
|
if self._coordinator is not None:
|
|
1178
1172
|
return await self._check_updates_via_coordinator()
|
|
1179
1173
|
|
|
1180
|
-
# Legacy per-directory fallback
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
async def _check_one(directory: ManifestDirectory) -> None:
|
|
1184
|
-
partial = await self._check_directory_updates(directory)
|
|
1185
|
-
for installer, packages in partial.items():
|
|
1186
|
-
available.setdefault(installer, {}).update(packages)
|
|
1187
|
-
|
|
1188
|
-
async with asyncio.TaskGroup() as tg:
|
|
1189
|
-
for d in directories:
|
|
1190
|
-
tg.create_task(_check_one(d))
|
|
1174
|
+
# Legacy per-directory fallback — delegate to the operations layer
|
|
1175
|
+
from synodic_client.operations.tool import check_tool_updates
|
|
1191
1176
|
|
|
1192
|
-
return
|
|
1177
|
+
return await check_tool_updates(self._porringer, directories)
|
|
1193
1178
|
|
|
1194
1179
|
async def _check_updates_via_coordinator(self) -> dict[str, dict[str, str]]:
|
|
1195
1180
|
"""Use the coordinator's ``check_updates`` for efficient detection.
|
|
@@ -1225,48 +1210,6 @@ class ToolsView(QWidget):
|
|
|
1225
1210
|
|
|
1226
1211
|
return available
|
|
1227
1212
|
|
|
1228
|
-
async def _check_directory_updates(
|
|
1229
|
-
self,
|
|
1230
|
-
directory: ManifestDirectory,
|
|
1231
|
-
) -> dict[str, dict[str, str]]:
|
|
1232
|
-
"""Check a single directory for available updates (dry-run).
|
|
1233
|
-
|
|
1234
|
-
Legacy fallback used when no coordinator is available.
|
|
1235
|
-
"""
|
|
1236
|
-
available: dict[str, dict[str, str]] = {}
|
|
1237
|
-
try:
|
|
1238
|
-
path = Path(directory.path)
|
|
1239
|
-
filenames = self._porringer.sync.manifest_filenames()
|
|
1240
|
-
manifest_path: Path | None = None
|
|
1241
|
-
for fname in filenames:
|
|
1242
|
-
candidate = path / fname
|
|
1243
|
-
if candidate.exists():
|
|
1244
|
-
manifest_path = candidate
|
|
1245
|
-
break
|
|
1246
|
-
|
|
1247
|
-
if manifest_path is None:
|
|
1248
|
-
return available
|
|
1249
|
-
|
|
1250
|
-
params = SetupParameters(
|
|
1251
|
-
paths=[str(manifest_path)],
|
|
1252
|
-
dry_run=True,
|
|
1253
|
-
project_directory=path,
|
|
1254
|
-
)
|
|
1255
|
-
async for event in self._porringer.sync.execute_stream(params):
|
|
1256
|
-
if isinstance(event, ActionCompletedEvent) and event.result.skip_reason == SkipReason.UPDATE_AVAILABLE:
|
|
1257
|
-
action = event.result.action
|
|
1258
|
-
if action.installer and action.package:
|
|
1259
|
-
pkg_name = str(action.package.name)
|
|
1260
|
-
latest = event.result.available_version or ''
|
|
1261
|
-
available.setdefault(action.installer, {})[pkg_name] = latest
|
|
1262
|
-
except Exception:
|
|
1263
|
-
logger.debug(
|
|
1264
|
-
'Could not detect updates for %s',
|
|
1265
|
-
directory.path,
|
|
1266
|
-
exc_info=True,
|
|
1267
|
-
)
|
|
1268
|
-
return available
|
|
1269
|
-
|
|
1270
1213
|
async def _deferred_update_check(self) -> None:
|
|
1271
1214
|
"""Run update detection in the background, then patch the widget tree.
|
|
1272
1215
|
|