synodic-client 0.0.1.dev38__tar.gz → 0.0.1.dev40__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.dev38 → synodic_client-0.0.1.dev40}/PKG-INFO +2 -2
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/pyproject.toml +5 -5
- synodic_client-0.0.1.dev40/synodic_client/_version.py +1 -0
- synodic_client-0.0.1.dev40/synodic_client/application/data.py +192 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/__init__.py +1 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/install.py +324 -255
- synodic_client-0.0.1.dev40/synodic_client/application/screen/screen.py +2115 -0
- synodic_client-0.0.1.dev40/synodic_client/application/screen/sidebar.py +330 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/spinner.py +35 -8
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/tray.py +260 -43
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/theme.py +251 -14
- synodic_client-0.0.1.dev40/synodic_client/application/workers.py +164 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/client.py +0 -11
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/config.py +12 -4
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/resolution.py +90 -2
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/updater.py +24 -53
- synodic_client-0.0.1.dev40/tests/unit/qt/test_gather_packages.py +755 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/test_install_preview.py +198 -161
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/test_log_panel.py +44 -28
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/test_logging.py +2 -2
- synodic_client-0.0.1.dev40/tests/unit/qt/test_sidebar.py +308 -0
- synodic_client-0.0.1.dev40/tests/unit/qt/test_tray_window_show.py +69 -0
- synodic_client-0.0.1.dev40/tests/unit/qt/test_update_feedback.py +393 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_client_updater.py +0 -7
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_config.py +12 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_resolution.py +86 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_updater.py +34 -24
- synodic_client-0.0.1.dev40/tests/unit/test_workers.py +55 -0
- synodic_client-0.0.1.dev38/synodic_client/_version.py +0 -1
- synodic_client-0.0.1.dev38/synodic_client/application/screen/screen.py +0 -843
- synodic_client-0.0.1.dev38/synodic_client/application/workers.py +0 -112
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/README.md +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev38 → synodic_client-0.0.1.dev40}/tests/unit/windows/test_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: synodic_client
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev40
|
|
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,7 +8,7 @@ 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.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev71
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
13
|
Requires-Dist: velopack>=0.0.1444.dev49733
|
|
14
14
|
Requires-Dist: typer>=0.24.1
|
|
@@ -10,12 +10,12 @@ 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.
|
|
13
|
+
"porringer>=0.2.1.dev71",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
15
|
"velopack>=0.0.1444.dev49733",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev40"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -35,8 +35,8 @@ build = [
|
|
|
35
35
|
"pyinstaller>=6.19.0",
|
|
36
36
|
]
|
|
37
37
|
lint = [
|
|
38
|
-
"ruff>=0.15.
|
|
39
|
-
"pyrefly>=0.
|
|
38
|
+
"ruff>=0.15.4",
|
|
39
|
+
"pyrefly>=0.55.0",
|
|
40
40
|
]
|
|
41
41
|
test = [
|
|
42
42
|
"pytest>=9.0.2",
|
|
@@ -109,7 +109,7 @@ write_template = "__version__ = '{}'\n"
|
|
|
109
109
|
allow-prereleases = true
|
|
110
110
|
|
|
111
111
|
[tool.pdm.scripts]
|
|
112
|
-
analyze = "ruff check
|
|
112
|
+
analyze = "ruff check"
|
|
113
113
|
format = "ruff format"
|
|
114
114
|
test = "pytest --cov=synodic_client --verbose tests"
|
|
115
115
|
type-check = "pyrefly check"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.1.dev40'
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Shared data coordinator for the Synodic Client application.
|
|
2
|
+
|
|
3
|
+
Centralises porringer API calls so that plugin discovery, directory
|
|
4
|
+
listing, and runtime context resolution happen once per refresh cycle
|
|
5
|
+
and the results are reused by every consumer (ToolsView, ProjectsView,
|
|
6
|
+
TrayScreen, install workers).
|
|
7
|
+
|
|
8
|
+
The coordinator follows an *invalidate-on-mutation* strategy: callers
|
|
9
|
+
that modify state (install, uninstall, add/remove directory) call
|
|
10
|
+
:meth:`invalidate` to force the next :meth:`refresh` to re-fetch.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
from porringer.api import API
|
|
20
|
+
from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
21
|
+
from porringer.core.plugin_schema.plugin_manager import PluginManager
|
|
22
|
+
from porringer.schema import (
|
|
23
|
+
CheckParameters,
|
|
24
|
+
CheckResult,
|
|
25
|
+
DirectoryValidationResult,
|
|
26
|
+
ManifestDirectory,
|
|
27
|
+
PluginInfo,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class Snapshot:
|
|
35
|
+
"""Immutable bundle of data produced by a single refresh cycle.
|
|
36
|
+
|
|
37
|
+
All fields are populated by :meth:`DataCoordinator.refresh` and
|
|
38
|
+
remain stable until the next refresh.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
plugins: list[PluginInfo] = field(default_factory=list)
|
|
42
|
+
"""All discovered plugins with install status and version info."""
|
|
43
|
+
|
|
44
|
+
directories: list[ManifestDirectory] = field(default_factory=list)
|
|
45
|
+
"""Cached project directories (un-validated)."""
|
|
46
|
+
|
|
47
|
+
validated_directories: list[DirectoryValidationResult] = field(default_factory=list)
|
|
48
|
+
"""Cached directories with ``exists`` / ``has_manifest`` validation."""
|
|
49
|
+
|
|
50
|
+
discovered: DiscoveredPlugins | None = None
|
|
51
|
+
"""Full plugin discovery result including runtime context."""
|
|
52
|
+
|
|
53
|
+
plugin_managers: dict[str, PluginManager] = field(default_factory=dict)
|
|
54
|
+
"""Project-environment plugins implementing the ``PluginManager`` protocol."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DataCoordinator:
|
|
58
|
+
"""Single source of truth for porringer data across the application.
|
|
59
|
+
|
|
60
|
+
Usage::
|
|
61
|
+
|
|
62
|
+
coordinator = DataCoordinator(porringer)
|
|
63
|
+
snapshot = await coordinator.refresh() # first load
|
|
64
|
+
# … later, after an install …
|
|
65
|
+
coordinator.invalidate()
|
|
66
|
+
snapshot = await coordinator.refresh() # re-fetches everything
|
|
67
|
+
|
|
68
|
+
The coordinator caches the most recent :class:`Snapshot` so that
|
|
69
|
+
synchronous property access (``coordinator.snapshot``) is available
|
|
70
|
+
between refresh cycles.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, porringer: API) -> None:
|
|
74
|
+
"""Initialize the coordinator with a porringer API instance."""
|
|
75
|
+
self._porringer = porringer
|
|
76
|
+
self._snapshot: Snapshot = Snapshot()
|
|
77
|
+
self._stale = True
|
|
78
|
+
self._refresh_lock = asyncio.Lock()
|
|
79
|
+
|
|
80
|
+
# -- Public API --------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def snapshot(self) -> Snapshot:
|
|
84
|
+
"""Return the most recent snapshot (may be empty before first refresh)."""
|
|
85
|
+
return self._snapshot
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def discovered_plugins(self) -> DiscoveredPlugins | None:
|
|
89
|
+
"""Shortcut to the current ``DiscoveredPlugins`` instance."""
|
|
90
|
+
return self._snapshot.discovered
|
|
91
|
+
|
|
92
|
+
def invalidate(self) -> None:
|
|
93
|
+
"""Mark the cached data as stale.
|
|
94
|
+
|
|
95
|
+
The next call to :meth:`refresh` will re-fetch everything from
|
|
96
|
+
porringer. This is a lightweight O(1) flag-flip.
|
|
97
|
+
"""
|
|
98
|
+
self._stale = True
|
|
99
|
+
|
|
100
|
+
async def refresh(self, *, force: bool = False) -> Snapshot:
|
|
101
|
+
"""Fetch fresh data from porringer if stale (or *force* is set).
|
|
102
|
+
|
|
103
|
+
Multiple concurrent callers are coalesced via an ``asyncio.Lock``
|
|
104
|
+
so that only one discovery + listing round-trip runs at a time.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The populated :class:`Snapshot`.
|
|
108
|
+
"""
|
|
109
|
+
if not self._stale and not force:
|
|
110
|
+
return self._snapshot
|
|
111
|
+
|
|
112
|
+
async with self._refresh_lock:
|
|
113
|
+
# Double-check after acquiring the lock — another coroutine
|
|
114
|
+
# may have already refreshed while we were waiting.
|
|
115
|
+
if not self._stale and not force:
|
|
116
|
+
return self._snapshot
|
|
117
|
+
|
|
118
|
+
self._snapshot = await self._fetch()
|
|
119
|
+
self._stale = False
|
|
120
|
+
return self._snapshot
|
|
121
|
+
|
|
122
|
+
async def check_updates(
|
|
123
|
+
self,
|
|
124
|
+
plugins: list[str] | None = None,
|
|
125
|
+
) -> list[CheckResult]:
|
|
126
|
+
"""Run update detection using the cached ``DiscoveredPlugins``.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
plugins: Optional include-set of plugin names. ``None``
|
|
130
|
+
means all plugins.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A list of :class:`CheckResult` per plugin.
|
|
134
|
+
"""
|
|
135
|
+
params = CheckParameters(plugins=plugins)
|
|
136
|
+
return await self._porringer.sync.check_updates(
|
|
137
|
+
params,
|
|
138
|
+
plugins=self._snapshot.discovered,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# -- Internals ---------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
async def _fetch(self) -> Snapshot:
|
|
144
|
+
"""Run the full discovery + listing pipeline.
|
|
145
|
+
|
|
146
|
+
1. ``API.discover_plugins()`` — plugin entry-points + runtime
|
|
147
|
+
context in one shot.
|
|
148
|
+
2. ``PluginCommands.list()`` — installed status + versions,
|
|
149
|
+
passing the already-discovered ``DiscoveredPlugins``.
|
|
150
|
+
3. ``cache.list_directories(validate=True, check_manifest=True)``
|
|
151
|
+
— directory listing with validation baked in.
|
|
152
|
+
4. Filter ``project_environments`` for ``PluginManager`` instances.
|
|
153
|
+
|
|
154
|
+
All blocking calls are dispatched via ``asyncio.to_thread``.
|
|
155
|
+
"""
|
|
156
|
+
loop = asyncio.get_running_loop()
|
|
157
|
+
|
|
158
|
+
# Step 1: discover all plugins + resolve runtime context
|
|
159
|
+
discovered = await API.discover_plugins()
|
|
160
|
+
|
|
161
|
+
# Step 2 + 3 in parallel: plugin list + validated directories
|
|
162
|
+
plugins_task = asyncio.create_task(
|
|
163
|
+
self._porringer.plugin.list(plugins=discovered),
|
|
164
|
+
)
|
|
165
|
+
dirs_future = loop.run_in_executor(
|
|
166
|
+
None,
|
|
167
|
+
lambda: self._porringer.cache.list_directories(
|
|
168
|
+
validate=True,
|
|
169
|
+
check_manifest=True,
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
plugins = await plugins_task
|
|
174
|
+
validated = await dirs_future
|
|
175
|
+
|
|
176
|
+
# Step 4: extract PluginManager instances from project_environments
|
|
177
|
+
managers: dict[str, PluginManager] = {}
|
|
178
|
+
for _name, env in discovered.project_environments.items():
|
|
179
|
+
if isinstance(env, PluginManager) and env.is_available():
|
|
180
|
+
managers[env.tool_name()] = env
|
|
181
|
+
|
|
182
|
+
# Derive the un-validated directory list for callers that only
|
|
183
|
+
# need path + name (e.g. _gather_packages).
|
|
184
|
+
directories = [r.directory for r in validated]
|
|
185
|
+
|
|
186
|
+
return Snapshot(
|
|
187
|
+
plugins=plugins,
|
|
188
|
+
directories=directories,
|
|
189
|
+
validated_directories=validated,
|
|
190
|
+
discovered=discovered,
|
|
191
|
+
plugin_managers=managers,
|
|
192
|
+
)
|
|
@@ -43,6 +43,7 @@ def plugin_kind_group_label(kind: PluginKind) -> str:
|
|
|
43
43
|
|
|
44
44
|
SKIP_REASON_LABELS: dict[SkipReason, str] = {
|
|
45
45
|
SkipReason.ALREADY_INSTALLED: 'Already installed',
|
|
46
|
+
SkipReason.NOT_INSTALLED: 'Not installed',
|
|
46
47
|
SkipReason.ALREADY_LATEST: 'Already latest',
|
|
47
48
|
SkipReason.NO_PROJECT_DIRECTORY: 'No project directory',
|
|
48
49
|
SkipReason.UPDATE_AVAILABLE: 'Update available',
|