synodic-client 0.0.1.dev77__tar.gz → 0.0.1.dev78__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.dev77 → synodic_client-0.0.1.dev78}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/schema.py +0 -33
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/action_card.py +9 -9
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/install.py +29 -18
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/install_workers.py +31 -77
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/plugin_row.py +84 -28
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/projects.py +7 -10
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/schema.py +20 -18
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/screen.py +34 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/tool_update_controller.py +70 -49
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/theme.py +3 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/update_controller.py +3 -2
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/update_model.py +15 -8
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/install.py +33 -36
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/config.py +12 -12
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/install.py +94 -2
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/schema.py +0 -3
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/tool.py +82 -30
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/resolution.py +184 -211
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_tool.py +5 -63
- synodic_client-0.0.1.dev78/tests/unit/qt/conftest.py +125 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_action_card.py +112 -173
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_gather_packages.py +46 -79
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_install_preview.py +32 -25
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_log_panel.py +47 -91
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_preview_model.py +14 -47
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_settings.py +30 -58
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_sidebar.py +17 -32
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_tray_window_show.py +3 -20
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_update_controller.py +11 -40
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_update_feedback.py +78 -11
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_resolution.py +26 -7
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_updater.py +49 -150
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_workers.py +72 -1
- synodic_client-0.0.1.dev78/tests/unit/windows/conftest.py +19 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/windows/test_protocol.py +5 -7
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/windows/test_startup.py +17 -43
- synodic_client-0.0.1.dev77/synodic_client/application/workers.py +0 -20
- synodic_client-0.0.1.dev77/tests/unit/qt/conftest.py +0 -24
- synodic_client-0.0.1.dev77/tests/unit/windows/conftest.py +0 -9
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/README.md +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/debug.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/config.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/context.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/debug.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/output.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/project.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/tool.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/cli/update.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/project.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/operations/update.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_config.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_install.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_install_plan.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_project.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_update.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_package_state.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/tests/unit/windows/__init__.py +0 -0
{synodic_client-0.0.1.dev77 → synodic_client-0.0.1.dev78}/synodic_client/application/schema.py
RENAMED
|
@@ -43,36 +43,3 @@ class Snapshot:
|
|
|
43
43
|
|
|
44
44
|
plugin_capabilities: dict[str, frozenset[PluginCapability]] = field(default_factory=dict)
|
|
45
45
|
"""Protocol capabilities reported for each discovered plugin."""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass(slots=True)
|
|
49
|
-
class ToolUpdateResult:
|
|
50
|
-
"""Summary of a tool-update run across cached manifests."""
|
|
51
|
-
|
|
52
|
-
manifests_processed: int = 0
|
|
53
|
-
updated: int = 0
|
|
54
|
-
already_latest: int = 0
|
|
55
|
-
failed: int = 0
|
|
56
|
-
updated_packages: set[str] = field(default_factory=set)
|
|
57
|
-
"""Package names that were successfully upgraded."""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@dataclass(frozen=True, slots=True)
|
|
61
|
-
class UpdateTarget:
|
|
62
|
-
"""Identifies the scope of a manual tool update.
|
|
63
|
-
|
|
64
|
-
Passed to the shared completion handler so it can clear the correct
|
|
65
|
-
updating state and derive timestamp keys. ``None`` (the default in
|
|
66
|
-
the handler) means the update was periodic / automatic.
|
|
67
|
-
|
|
68
|
-
When *package* is empty the update targeted an entire plugin;
|
|
69
|
-
otherwise it targeted one specific package within the plugin.
|
|
70
|
-
*plugin* always carries the signal key (possibly composite
|
|
71
|
-
``"plugin:tag"``).
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
plugin: str
|
|
75
|
-
"""Signal key for the plugin (may be composite ``"name:tag"``)."""
|
|
76
|
-
|
|
77
|
-
package: str = ''
|
|
78
|
-
"""Package name, or empty when the whole plugin was updated."""
|
|
@@ -79,6 +79,15 @@ _SPINNER_INTERVAL = 50
|
|
|
79
79
|
#: display order always matches the order actions actually execute.
|
|
80
80
|
_KIND_ORDER: dict[PluginKind | None, int] = {kind: i for i, kind in enumerate(PHASE_ORDER)}
|
|
81
81
|
|
|
82
|
+
#: Mapping of resolved status label → stylesheet for dry-run badge styling.
|
|
83
|
+
_STATUS_STYLES: dict[str, str] = {
|
|
84
|
+
'Update available': ACTION_CARD_STATUS_UPDATE,
|
|
85
|
+
'Failed': ACTION_CARD_STATUS_FAILED,
|
|
86
|
+
'Pending': ACTION_CARD_STATUS_PENDING,
|
|
87
|
+
'Ready': ACTION_CARD_STATUS_SATISFIED,
|
|
88
|
+
'Needed': ACTION_CARD_STATUS_NEEDED,
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
|
|
83
92
|
def action_sort_key(action: SetupAction) -> int:
|
|
84
93
|
"""Return a sort key that groups cards by execution phase.
|
|
@@ -465,15 +474,6 @@ class ActionCard(QFrame):
|
|
|
465
474
|
|
|
466
475
|
self._stop_spinner()
|
|
467
476
|
|
|
468
|
-
# Status-to-style mapping
|
|
469
|
-
_STATUS_STYLES: dict[str, str] = {
|
|
470
|
-
'Update available': ACTION_CARD_STATUS_UPDATE,
|
|
471
|
-
'Failed': ACTION_CARD_STATUS_FAILED,
|
|
472
|
-
'Pending': ACTION_CARD_STATUS_PENDING,
|
|
473
|
-
'Ready': ACTION_CARD_STATUS_SATISFIED,
|
|
474
|
-
'Needed': ACTION_CARD_STATUS_NEEDED,
|
|
475
|
-
}
|
|
476
|
-
|
|
477
477
|
style = _STATUS_STYLES.get(status, ACTION_CARD_STATUS_SATISFIED)
|
|
478
478
|
display = status
|
|
479
479
|
|
|
@@ -50,7 +50,6 @@ from synodic_client.application.screen.schema import (
|
|
|
50
50
|
ActionState,
|
|
51
51
|
InstallCallbacks,
|
|
52
52
|
InstallConfig,
|
|
53
|
-
PreviewCallbacks,
|
|
54
53
|
PreviewConfig,
|
|
55
54
|
PreviewModel,
|
|
56
55
|
PreviewPhase,
|
|
@@ -499,12 +498,7 @@ class SetupPreviewWidget(QWidget):
|
|
|
499
498
|
project_directory=project_directory,
|
|
500
499
|
prerelease_packages=prerelease_packages,
|
|
501
500
|
),
|
|
502
|
-
|
|
503
|
-
on_manifest_parsed=self._on_manifest_parsed,
|
|
504
|
-
on_plugins_queried=self._on_plugins_queried,
|
|
505
|
-
on_preview_ready=self._on_preview_resolved,
|
|
506
|
-
on_action_checked=self._on_action_checked,
|
|
507
|
-
),
|
|
501
|
+
on_event=self._on_preview_event,
|
|
508
502
|
plugins=self._discovered_plugins,
|
|
509
503
|
)
|
|
510
504
|
self._on_preview_finished()
|
|
@@ -540,6 +534,26 @@ class SetupPreviewWidget(QWidget):
|
|
|
540
534
|
logger.exception('Install execution failed')
|
|
541
535
|
self._on_install_error(str(exc))
|
|
542
536
|
|
|
537
|
+
# --- Preview event dispatcher ---
|
|
538
|
+
|
|
539
|
+
def _on_preview_event(self, event: object) -> None:
|
|
540
|
+
"""Route a :data:`PreviewEvent` to the appropriate handler."""
|
|
541
|
+
from synodic_client.operations.schema import (
|
|
542
|
+
PreviewActionChecked,
|
|
543
|
+
PreviewManifestParsed,
|
|
544
|
+
PreviewPluginsQueried,
|
|
545
|
+
PreviewReady,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if isinstance(event, PreviewManifestParsed):
|
|
549
|
+
self._on_manifest_parsed(event.manifest, event.manifest_path, event.temp_dir)
|
|
550
|
+
elif isinstance(event, PreviewPluginsQueried):
|
|
551
|
+
self._on_plugins_queried(event.availability, event.capabilities)
|
|
552
|
+
elif isinstance(event, PreviewReady):
|
|
553
|
+
self._on_preview_resolved(event.manifest, event.manifest_path, event.temp_dir)
|
|
554
|
+
elif isinstance(event, PreviewActionChecked):
|
|
555
|
+
self._on_action_checked(event.index, event.result, event.status)
|
|
556
|
+
|
|
543
557
|
# --- Preview callbacks (wired by load()) ---
|
|
544
558
|
|
|
545
559
|
def _on_manifest_parsed(self, preview: SetupResults, manifest_path: str, temp_dir_path: str) -> None:
|
|
@@ -577,9 +591,6 @@ class SetupPreviewWidget(QWidget):
|
|
|
577
591
|
|
|
578
592
|
self._show_metadata(preview)
|
|
579
593
|
|
|
580
|
-
if preview.metadata:
|
|
581
|
-
self.metadata_ready.emit(preview)
|
|
582
|
-
|
|
583
594
|
if not preview.actions:
|
|
584
595
|
self._card_list.clear()
|
|
585
596
|
self._status_label.setText('No actions to perform — the manifest is empty.')
|
|
@@ -618,16 +629,13 @@ class SetupPreviewWidget(QWidget):
|
|
|
618
629
|
|
|
619
630
|
Called after ``MANIFEST_LOADED`` — cards are already visible
|
|
620
631
|
from the earlier ``_on_manifest_parsed`` handler. This updates
|
|
621
|
-
the temp-dir reference
|
|
632
|
+
the temp-dir reference.
|
|
622
633
|
"""
|
|
623
634
|
if self._model.preview is None:
|
|
624
635
|
return
|
|
625
636
|
|
|
626
637
|
self._model.temp_dir = temp_dir_path
|
|
627
638
|
|
|
628
|
-
if preview.metadata:
|
|
629
|
-
self.metadata_ready.emit(preview)
|
|
630
|
-
|
|
631
639
|
def _on_action_checked(self, row: int, result: SetupActionResult, status: str) -> None:
|
|
632
640
|
"""Update the model and action card with a dry-run result.
|
|
633
641
|
|
|
@@ -976,9 +984,7 @@ class SetupPreviewWidget(QWidget):
|
|
|
976
984
|
pre_skipped = len(m.install_plan.satisfied_indices) if m.install_plan else 0
|
|
977
985
|
|
|
978
986
|
# If we have stashed install results (auto-run path), use combined summary
|
|
979
|
-
install_results = (
|
|
980
|
-
list(self._install_results.results) if hasattr(self, '_install_results') and self._install_results else None
|
|
981
|
-
)
|
|
987
|
+
install_results = list(self._install_results.results) if self._install_results else None
|
|
982
988
|
summary = format_install_summary(
|
|
983
989
|
install_results=install_results,
|
|
984
990
|
post_sync_results=m.post_sync_results,
|
|
@@ -988,7 +994,12 @@ class SetupPreviewWidget(QWidget):
|
|
|
988
994
|
self._install_btn.setEnabled(False)
|
|
989
995
|
self._run_commands_btn.setEnabled(False)
|
|
990
996
|
self._close_btn.setEnabled(True)
|
|
991
|
-
|
|
997
|
+
|
|
998
|
+
# Only emit when coming from the auto-run path (install_finished
|
|
999
|
+
# was not yet emitted). On the manual "Run Commands" path the
|
|
1000
|
+
# signal was already emitted by _on_install_finished.
|
|
1001
|
+
if self._install_results is not None:
|
|
1002
|
+
self.install_finished.emit(results)
|
|
992
1003
|
|
|
993
1004
|
|
|
994
1005
|
# ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import logging
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
from porringer.api import API
|
|
@@ -17,9 +18,6 @@ from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
|
17
18
|
from porringer.schema import (
|
|
18
19
|
ActionCompletedEvent,
|
|
19
20
|
ActionStartedEvent,
|
|
20
|
-
ManifestLoadedEvent,
|
|
21
|
-
SetupAction,
|
|
22
|
-
SetupActionResult,
|
|
23
21
|
SetupResults,
|
|
24
22
|
SubActionProgressEvent,
|
|
25
23
|
)
|
|
@@ -27,16 +25,13 @@ from porringer.schema import (
|
|
|
27
25
|
from synodic_client.application.screen.schema import (
|
|
28
26
|
InstallCallbacks,
|
|
29
27
|
InstallConfig,
|
|
30
|
-
PreviewCallbacks,
|
|
31
28
|
PreviewConfig,
|
|
32
29
|
)
|
|
33
30
|
from synodic_client.application.uri import safe_rmtree
|
|
34
|
-
from synodic_client.operations.install import
|
|
31
|
+
from synodic_client.operations.install import collect_install, collect_post_sync, preview_manifest_stream
|
|
35
32
|
from synodic_client.operations.schema import (
|
|
36
|
-
|
|
33
|
+
PreviewEvent,
|
|
37
34
|
PreviewManifestParsed,
|
|
38
|
-
PreviewPluginsQueried,
|
|
39
|
-
PreviewReady,
|
|
40
35
|
)
|
|
41
36
|
|
|
42
37
|
logger = logging.getLogger(__name__)
|
|
@@ -58,16 +53,21 @@ async def run_install(
|
|
|
58
53
|
) -> SetupResults:
|
|
59
54
|
"""Execute setup actions via the operations layer and stream progress.
|
|
60
55
|
|
|
61
|
-
Delegates to :func:`~synodic_client.operations.install.
|
|
62
|
-
and routes
|
|
56
|
+
Delegates to :func:`~synodic_client.operations.install.collect_install`
|
|
57
|
+
and routes progress events to GUI callbacks.
|
|
63
58
|
"""
|
|
64
59
|
cfg = config or InstallConfig()
|
|
65
60
|
cb = callbacks or InstallCallbacks()
|
|
66
|
-
actions: list[SetupAction] = []
|
|
67
|
-
collected: list[SetupActionResult] = []
|
|
68
|
-
manifest_result: SetupResults | None = None
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
def _on_progress(stage: str, event: object) -> None:
|
|
63
|
+
if stage == 'action_started' and isinstance(event, ActionStartedEvent) and cb.on_action_started is not None:
|
|
64
|
+
cb.on_action_started(event.action)
|
|
65
|
+
elif stage == 'sub_progress' and isinstance(event, SubActionProgressEvent) and cb.on_sub_progress is not None:
|
|
66
|
+
cb.on_sub_progress(event.action, event.sub_action)
|
|
67
|
+
elif stage == 'action_completed' and isinstance(event, ActionCompletedEvent) and cb.on_progress is not None:
|
|
68
|
+
cb.on_progress(event.action, event.result)
|
|
69
|
+
|
|
70
|
+
return await collect_install(
|
|
71
71
|
porringer,
|
|
72
72
|
manifest_path,
|
|
73
73
|
project_directory=cfg.project_directory,
|
|
@@ -75,27 +75,7 @@ async def run_install(
|
|
|
75
75
|
prerelease_packages=cfg.prerelease_packages,
|
|
76
76
|
discovered=plugins,
|
|
77
77
|
exclude_post_sync=exclude_post_sync,
|
|
78
|
-
|
|
79
|
-
if stage == 'manifest_loaded' and isinstance(event, ManifestLoadedEvent):
|
|
80
|
-
manifest_result = event.manifest
|
|
81
|
-
actions = list(event.manifest.actions)
|
|
82
|
-
|
|
83
|
-
elif stage == 'action_started' and isinstance(event, ActionStartedEvent) and cb.on_action_started is not None:
|
|
84
|
-
cb.on_action_started(event.action)
|
|
85
|
-
|
|
86
|
-
elif stage == 'sub_progress' and isinstance(event, SubActionProgressEvent) and cb.on_sub_progress is not None:
|
|
87
|
-
cb.on_sub_progress(event.action, event.sub_action)
|
|
88
|
-
|
|
89
|
-
elif stage == 'action_completed' and isinstance(event, ActionCompletedEvent):
|
|
90
|
-
collected.append(event.result)
|
|
91
|
-
if cb.on_progress is not None:
|
|
92
|
-
cb.on_progress(event.action, event.result)
|
|
93
|
-
|
|
94
|
-
return SetupResults(
|
|
95
|
-
actions=actions,
|
|
96
|
-
results=collected,
|
|
97
|
-
manifest_path=manifest_result.manifest_path if manifest_result else None,
|
|
98
|
-
metadata=manifest_result.metadata if manifest_result else None,
|
|
78
|
+
on_progress=_on_progress,
|
|
99
79
|
)
|
|
100
80
|
|
|
101
81
|
|
|
@@ -114,40 +94,25 @@ async def run_post_sync(
|
|
|
114
94
|
) -> SetupResults:
|
|
115
95
|
"""Execute only the post-sync commands from a manifest.
|
|
116
96
|
|
|
117
|
-
Delegates to :func:`~synodic_client.operations.install.
|
|
118
|
-
and routes events to GUI callbacks.
|
|
97
|
+
Delegates to :func:`~synodic_client.operations.install.collect_post_sync`
|
|
98
|
+
and routes progress events to GUI callbacks.
|
|
119
99
|
"""
|
|
120
100
|
cb = callbacks or InstallCallbacks()
|
|
121
|
-
actions: list[SetupAction] = []
|
|
122
|
-
collected: list[SetupActionResult] = []
|
|
123
|
-
manifest_result: SetupResults | None = None
|
|
124
|
-
|
|
125
|
-
async for stage, event in execute_post_sync(
|
|
126
|
-
porringer,
|
|
127
|
-
manifest_path,
|
|
128
|
-
project_directory=project_directory,
|
|
129
|
-
discovered=plugins,
|
|
130
|
-
):
|
|
131
|
-
if stage == 'manifest_loaded' and isinstance(event, ManifestLoadedEvent):
|
|
132
|
-
manifest_result = event.manifest
|
|
133
|
-
actions = list(event.manifest.actions)
|
|
134
101
|
|
|
135
|
-
|
|
102
|
+
def _on_progress(stage: str, event: object) -> None:
|
|
103
|
+
if stage == 'action_started' and isinstance(event, ActionStartedEvent) and cb.on_action_started is not None:
|
|
136
104
|
cb.on_action_started(event.action)
|
|
137
|
-
|
|
138
105
|
elif stage == 'sub_progress' and isinstance(event, SubActionProgressEvent) and cb.on_sub_progress is not None:
|
|
139
106
|
cb.on_sub_progress(event.action, event.sub_action)
|
|
107
|
+
elif stage == 'action_completed' and isinstance(event, ActionCompletedEvent) and cb.on_progress is not None:
|
|
108
|
+
cb.on_progress(event.action, event.result)
|
|
140
109
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
actions=actions,
|
|
148
|
-
results=collected,
|
|
149
|
-
manifest_path=manifest_result.manifest_path if manifest_result else None,
|
|
150
|
-
metadata=manifest_result.metadata if manifest_result else None,
|
|
110
|
+
return await collect_post_sync(
|
|
111
|
+
porringer,
|
|
112
|
+
manifest_path,
|
|
113
|
+
project_directory=project_directory,
|
|
114
|
+
discovered=plugins,
|
|
115
|
+
on_progress=_on_progress,
|
|
151
116
|
)
|
|
152
117
|
|
|
153
118
|
|
|
@@ -161,17 +126,15 @@ async def run_preview(
|
|
|
161
126
|
url: str,
|
|
162
127
|
*,
|
|
163
128
|
config: PreviewConfig | None = None,
|
|
164
|
-
|
|
129
|
+
on_event: Callable[[PreviewEvent], object] | None = None,
|
|
165
130
|
plugins: DiscoveredPlugins | None = None,
|
|
166
131
|
) -> None:
|
|
167
132
|
"""Download a manifest and perform a dry-run preview.
|
|
168
133
|
|
|
169
134
|
Delegates to :func:`preview_manifest_stream` in the operations
|
|
170
|
-
layer, then
|
|
171
|
-
callback.
|
|
135
|
+
layer, then yields each :data:`PreviewEvent` to *on_event*.
|
|
172
136
|
"""
|
|
173
137
|
logger.info('run_preview starting for: %s', url)
|
|
174
|
-
cb = callbacks or PreviewCallbacks()
|
|
175
138
|
cfg = config or PreviewConfig()
|
|
176
139
|
temp_dir: str | None = None
|
|
177
140
|
try:
|
|
@@ -184,18 +147,9 @@ async def run_preview(
|
|
|
184
147
|
):
|
|
185
148
|
if isinstance(event, PreviewManifestParsed):
|
|
186
149
|
temp_dir = event.temp_dir or None
|
|
187
|
-
if cb.on_manifest_parsed is not None:
|
|
188
|
-
cb.on_manifest_parsed(event.manifest, event.manifest_path, event.temp_dir)
|
|
189
|
-
|
|
190
|
-
elif isinstance(event, PreviewPluginsQueried) and cb.on_plugins_queried is not None:
|
|
191
|
-
cb.on_plugins_queried(event.availability, event.capabilities)
|
|
192
|
-
|
|
193
|
-
elif isinstance(event, PreviewReady):
|
|
194
|
-
if cb.on_preview_ready is not None:
|
|
195
|
-
cb.on_preview_ready(event.manifest, event.manifest_path, event.temp_dir)
|
|
196
150
|
|
|
197
|
-
|
|
198
|
-
|
|
151
|
+
if on_event is not None:
|
|
152
|
+
on_event(event)
|
|
199
153
|
|
|
200
154
|
except asyncio.CancelledError:
|
|
201
155
|
if temp_dir:
|
|
@@ -7,6 +7,8 @@ package rows, project child rows, and filter chips.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
10
12
|
from porringer.schema import PluginInfo
|
|
11
13
|
from porringer.schema.plugin import PluginCapability, PluginKind
|
|
12
14
|
from PySide6.QtCore import Qt, QTimer, Signal
|
|
@@ -40,6 +42,7 @@ from synodic_client.application.theme import (
|
|
|
40
42
|
PLUGIN_ROW_PROJECT_TAG_TRANSITIVE_STYLE,
|
|
41
43
|
PLUGIN_ROW_REMOVE_STYLE,
|
|
42
44
|
PLUGIN_ROW_STATUS_MIN_WIDTH,
|
|
45
|
+
PLUGIN_ROW_STATUS_PENDING_STYLE,
|
|
43
46
|
PLUGIN_ROW_STATUS_STYLE,
|
|
44
47
|
PLUGIN_ROW_STYLE,
|
|
45
48
|
PLUGIN_ROW_TIMESTAMP_MIN_WIDTH,
|
|
@@ -59,6 +62,16 @@ from synodic_client.application.theme import (
|
|
|
59
62
|
PROJECT_CHILD_VERSION_STYLE,
|
|
60
63
|
)
|
|
61
64
|
|
|
65
|
+
|
|
66
|
+
class RowPhase(Enum):
|
|
67
|
+
"""Mutually exclusive visual state for a :class:`PluginRow`."""
|
|
68
|
+
|
|
69
|
+
IDLE = 'idle'
|
|
70
|
+
CHECKING = 'checking'
|
|
71
|
+
PENDING = 'pending'
|
|
72
|
+
UPDATING = 'updating'
|
|
73
|
+
|
|
74
|
+
|
|
62
75
|
# Row-spinner dimensions
|
|
63
76
|
_ROW_SPINNER_SIZE = 12
|
|
64
77
|
_ROW_SPINNER_PEN = 2
|
|
@@ -249,15 +262,18 @@ class PluginProviderHeader(QFrame):
|
|
|
249
262
|
update_btn.setToolTip('Not installed \u2014 cannot update')
|
|
250
263
|
|
|
251
264
|
def set_updating(self, updating: bool) -> None:
|
|
252
|
-
"""Toggle
|
|
253
|
-
if self._update_btn is None:
|
|
254
|
-
return
|
|
265
|
+
"""Toggle between *Updating…* (with spinner) and *Update* states."""
|
|
255
266
|
if updating:
|
|
256
|
-
self.
|
|
257
|
-
|
|
267
|
+
if self._checking_spinner is not None:
|
|
268
|
+
self._checking_spinner.start()
|
|
269
|
+
if self._update_btn is not None:
|
|
270
|
+
self._update_btn.hide()
|
|
258
271
|
else:
|
|
259
|
-
self.
|
|
260
|
-
|
|
272
|
+
if self._checking_spinner is not None:
|
|
273
|
+
self._checking_spinner.stop()
|
|
274
|
+
if self._update_btn is not None:
|
|
275
|
+
self._update_btn.setText('Update')
|
|
276
|
+
self._update_btn.setEnabled(True)
|
|
261
277
|
|
|
262
278
|
def set_checking(self, checking: bool) -> None:
|
|
263
279
|
"""Show or hide the inline checking spinner."""
|
|
@@ -331,7 +347,7 @@ class PluginRow(QFrame):
|
|
|
331
347
|
self._signal_key = f'{data.plugin_name}:{data.runtime_tag}' if data.runtime_tag else data.plugin_name
|
|
332
348
|
self._update_btn: QPushButton | None = None
|
|
333
349
|
self._remove_btn: QPushButton | None = None
|
|
334
|
-
self.
|
|
350
|
+
self._row_spinner: _RowSpinner | None = None
|
|
335
351
|
self._host_label: QLabel | None = None
|
|
336
352
|
self._project_paths: list[str] = list(data.project_paths)
|
|
337
353
|
self._project_labels: list[str] = [p.project_label for p in data.project_instances]
|
|
@@ -386,6 +402,11 @@ class PluginRow(QFrame):
|
|
|
386
402
|
if data.show_toggle:
|
|
387
403
|
self._build_toggle(layout, data)
|
|
388
404
|
|
|
405
|
+
# Inline spinner — always created so checking, pending, and
|
|
406
|
+
# updating flows can use it regardless of whether the toggle is shown.
|
|
407
|
+
self._row_spinner = _RowSpinner(self)
|
|
408
|
+
layout.addWidget(self._row_spinner)
|
|
409
|
+
|
|
389
410
|
# Update button — always created for alignment, hidden when no update
|
|
390
411
|
self._build_update_button(layout, data)
|
|
391
412
|
|
|
@@ -421,7 +442,7 @@ class PluginRow(QFrame):
|
|
|
421
442
|
self._build_remove_button(layout, data)
|
|
422
443
|
|
|
423
444
|
def _build_toggle(self, layout: QHBoxLayout, data: PluginRowData) -> None:
|
|
424
|
-
"""Add the auto-update toggle
|
|
445
|
+
"""Add the auto-update toggle button."""
|
|
425
446
|
toggle_btn = QPushButton('Auto')
|
|
426
447
|
toggle_btn.setCheckable(True)
|
|
427
448
|
toggle_btn.setChecked(data.auto_update)
|
|
@@ -436,9 +457,6 @@ class PluginRow(QFrame):
|
|
|
436
457
|
)
|
|
437
458
|
layout.addWidget(toggle_btn)
|
|
438
459
|
|
|
439
|
-
self._checking_spinner = _RowSpinner(self)
|
|
440
|
-
layout.addWidget(self._checking_spinner)
|
|
441
|
-
|
|
442
460
|
def _build_update_button(self, layout: QHBoxLayout, data: PluginRowData) -> None:
|
|
443
461
|
"""Add the per-package update button (always created, visibility toggled)."""
|
|
444
462
|
update_btn = QPushButton('Update')
|
|
@@ -483,27 +501,65 @@ class PluginRow(QFrame):
|
|
|
483
501
|
self._remove_btn = remove_btn
|
|
484
502
|
layout.addWidget(remove_btn)
|
|
485
503
|
|
|
486
|
-
def
|
|
487
|
-
"""
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
504
|
+
def set_phase(self, phase: RowPhase) -> None:
|
|
505
|
+
"""Transition the row to a mutually exclusive visual *phase*.
|
|
506
|
+
|
|
507
|
+
* ``IDLE`` — spinner stopped, status hidden, button restored.
|
|
508
|
+
* ``CHECKING`` — spinner running, button hidden.
|
|
509
|
+
* ``PENDING`` — spinner stopped, *Pending* text shown, button hidden.
|
|
510
|
+
* ``UPDATING`` — spinner running, status hidden, button hidden.
|
|
511
|
+
"""
|
|
512
|
+
self._apply_phase_reset()
|
|
513
|
+
|
|
514
|
+
if phase == RowPhase.IDLE:
|
|
515
|
+
self._apply_phase_idle()
|
|
516
|
+
elif phase == RowPhase.PENDING:
|
|
517
|
+
self._apply_phase_pending()
|
|
493
518
|
else:
|
|
519
|
+
# CHECKING and UPDATING both start spinner and hide update btn
|
|
520
|
+
self._apply_phase_spinner()
|
|
521
|
+
|
|
522
|
+
def _apply_phase_reset(self) -> None:
|
|
523
|
+
"""Stop spinner and hide status — common entry for every transition."""
|
|
524
|
+
if self._row_spinner is not None:
|
|
525
|
+
self._row_spinner.stop()
|
|
526
|
+
if self._update_status_label is not None:
|
|
527
|
+
self._update_status_label.hide()
|
|
528
|
+
|
|
529
|
+
def _apply_phase_idle(self) -> None:
|
|
530
|
+
"""Restore the update button to its default label and state."""
|
|
531
|
+
if self._update_btn is not None:
|
|
494
532
|
self._update_btn.setText('Update')
|
|
495
533
|
self._update_btn.setEnabled(True)
|
|
496
534
|
|
|
535
|
+
def _apply_phase_pending(self) -> None:
|
|
536
|
+
"""Show *Pending* text and hide the update button."""
|
|
537
|
+
if self._update_status_label is not None:
|
|
538
|
+
self._update_status_label.setText('Pending')
|
|
539
|
+
self._update_status_label.setStyleSheet(PLUGIN_ROW_STATUS_PENDING_STYLE)
|
|
540
|
+
self._update_status_label.show()
|
|
541
|
+
if self._update_btn is not None:
|
|
542
|
+
self._update_btn.hide()
|
|
543
|
+
|
|
544
|
+
def _apply_phase_spinner(self) -> None:
|
|
545
|
+
"""Start the spinner and hide the update button."""
|
|
546
|
+
if self._row_spinner is not None:
|
|
547
|
+
self._row_spinner.start()
|
|
548
|
+
if self._update_btn is not None:
|
|
549
|
+
self._update_btn.hide()
|
|
550
|
+
|
|
551
|
+
# Convenience aliases for backward-compatible call sites
|
|
552
|
+
def set_updating(self, updating: bool) -> None:
|
|
553
|
+
"""Toggle between updating and idle states."""
|
|
554
|
+
self.set_phase(RowPhase.UPDATING if updating else RowPhase.IDLE)
|
|
555
|
+
|
|
497
556
|
def set_checking(self, checking: bool) -> None:
|
|
498
|
-
"""
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
self._update_btn.hide()
|
|
505
|
-
else:
|
|
506
|
-
self._checking_spinner.stop()
|
|
557
|
+
"""Toggle between checking and idle states."""
|
|
558
|
+
self.set_phase(RowPhase.CHECKING if checking else RowPhase.IDLE)
|
|
559
|
+
|
|
560
|
+
def set_pending(self, pending: bool) -> None:
|
|
561
|
+
"""Toggle between pending and idle states."""
|
|
562
|
+
self.set_phase(RowPhase.PENDING if pending else RowPhase.IDLE)
|
|
507
563
|
|
|
508
564
|
def set_removing(self, removing: bool) -> None:
|
|
509
565
|
"""Toggle the remove button between *Removing…* and *×* states."""
|
|
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
|
|
|
9
9
|
|
|
10
10
|
from porringer.api import API
|
|
11
11
|
from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
12
|
+
from porringer.schema import DirectoryValidationResult, ManifestDirectory
|
|
12
13
|
from PySide6.QtCore import Qt, Signal
|
|
13
14
|
from PySide6.QtWidgets import (
|
|
14
15
|
QFileDialog,
|
|
@@ -138,15 +139,11 @@ class ProjectsView(QWidget):
|
|
|
138
139
|
# Convert ProjectInfo list to the same shape as validated_directories
|
|
139
140
|
results = []
|
|
140
141
|
for p in projects:
|
|
141
|
-
result =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
'exists': p.exists,
|
|
147
|
-
'has_manifest': p.has_manifest,
|
|
148
|
-
},
|
|
149
|
-
)()
|
|
142
|
+
result = DirectoryValidationResult(
|
|
143
|
+
directory=ManifestDirectory(path=Path(p.path), name=p.name),
|
|
144
|
+
exists=p.exists,
|
|
145
|
+
has_manifest=p.has_manifest,
|
|
146
|
+
)
|
|
150
147
|
results.append(result)
|
|
151
148
|
discovered = None
|
|
152
149
|
|
|
@@ -154,7 +151,7 @@ class ProjectsView(QWidget):
|
|
|
154
151
|
current_paths: set[Path] = set()
|
|
155
152
|
for result in results:
|
|
156
153
|
d = result.directory
|
|
157
|
-
valid = bool(result.exists and result.has_manifest
|
|
154
|
+
valid = bool(result.exists and result.has_manifest)
|
|
158
155
|
path = Path(d.path)
|
|
159
156
|
directories.append((path, d.name or '', valid))
|
|
160
157
|
current_paths.add(path)
|
|
@@ -238,8 +238,6 @@ class PreviewModel:
|
|
|
238
238
|
|
|
239
239
|
def __init__(self) -> None:
|
|
240
240
|
"""Initialise a blank preview model."""
|
|
241
|
-
self._normalize = normalize_manifest_key
|
|
242
|
-
|
|
243
241
|
self.phase: PreviewPhase = PreviewPhase.IDLE
|
|
244
242
|
self.preview: SetupResults | None = None
|
|
245
243
|
self.manifest_path: Path | None = None
|
|
@@ -295,7 +293,7 @@ class PreviewModel:
|
|
|
295
293
|
|
|
296
294
|
def has_same_manifest(self, key: str) -> bool:
|
|
297
295
|
"""Return ``True`` if *key* matches the current manifest key."""
|
|
298
|
-
return self.manifest_key is not None and self.manifest_key ==
|
|
296
|
+
return self.manifest_key is not None and self.manifest_key == normalize_manifest_key(key)
|
|
299
297
|
|
|
300
298
|
|
|
301
299
|
@dataclass(frozen=True, slots=True)
|
|
@@ -322,25 +320,29 @@ class InstallCallbacks:
|
|
|
322
320
|
|
|
323
321
|
|
|
324
322
|
@dataclass(frozen=True, slots=True)
|
|
325
|
-
class
|
|
326
|
-
"""
|
|
323
|
+
class PreviewConfig:
|
|
324
|
+
"""Optional execution parameters for :func:`run_preview`."""
|
|
327
325
|
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
project_directory: Path | None = None
|
|
327
|
+
prerelease_packages: set[str] | None = None
|
|
330
328
|
|
|
331
|
-
on_plugins_queried: Callable[[dict[str, bool], dict[str, frozenset[PluginCapability]]], None] | None = None
|
|
332
|
-
"""``(dict[str, bool], dict[str, frozenset[PluginCapability]])`` — plugin → installed + capabilities mappings."""
|
|
333
329
|
|
|
334
|
-
|
|
335
|
-
|
|
330
|
+
@dataclass(frozen=True, slots=True)
|
|
331
|
+
class UpdateTarget:
|
|
332
|
+
"""Identifies the scope of a manual tool update.
|
|
336
333
|
|
|
337
|
-
|
|
338
|
-
|
|
334
|
+
Passed to the shared completion handler so it can clear the correct
|
|
335
|
+
updating state and derive timestamp keys. ``None`` (the default in
|
|
336
|
+
the handler) means the update was periodic / automatic.
|
|
339
337
|
|
|
338
|
+
When *package* is empty the update targeted an entire plugin;
|
|
339
|
+
otherwise it targeted one specific package within the plugin.
|
|
340
|
+
*plugin* always carries the signal key (possibly composite
|
|
341
|
+
``"plugin:tag"``).
|
|
342
|
+
"""
|
|
340
343
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
"""Optional execution parameters for :func:`run_preview`."""
|
|
344
|
+
plugin: str
|
|
345
|
+
"""Signal key for the plugin (may be composite ``"name:tag"``)."""
|
|
344
346
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
package: str = ''
|
|
348
|
+
"""Package name, or empty when the whole plugin was updated."""
|