SparkleHelper 0.1.1__py3-none-win32.whl
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.
- sparklehelper-0.1.1.data/purelib/sparklehelper/__init__.py +62 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/__init__.py +54 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_macos/__init__.py +45 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_macos/_backend.py +377 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_macos/_delegates.py +656 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_macos/_loading.py +202 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_macos/_runtime.py +263 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_windows/__init__.py +20 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_windows/_backend.py +335 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_windows/_bindings.py +181 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/_windows/_loading.py +218 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_backend/base.py +330 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_framework.py +956 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_nuitka_plugin.py +139 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_pyinstaller/__init__.py +265 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/_pyinstaller/hook-sparklehelper.py +124 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/errors.py +68 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/licenses/WinSparkle-LICENSE.txt +25 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/sparklehelper.nuitka-package.config.yml +31 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/types.py +265 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/updater.py +474 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/winsparkle/arm64/WinSparkle.dll +0 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/winsparkle/winsparkle.h +663 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/winsparkle/x64/WinSparkle.dll +0 -0
- sparklehelper-0.1.1.data/purelib/sparklehelper/winsparkle/x86/WinSparkle.dll +0 -0
- sparklehelper-0.1.1.dist-info/METADATA +374 -0
- sparklehelper-0.1.1.dist-info/RECORD +31 -0
- sparklehelper-0.1.1.dist-info/WHEEL +5 -0
- sparklehelper-0.1.1.dist-info/entry_points.txt +5 -0
- sparklehelper-0.1.1.dist-info/licenses/LICENSE +21 -0
- sparklehelper-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""sparklehelper —— Sparkle / WinSparkle 原生更新框架的 Python 运行时接口。
|
|
2
|
+
|
|
3
|
+
快速上手::
|
|
4
|
+
|
|
5
|
+
from sparklehelper import Updater
|
|
6
|
+
|
|
7
|
+
updater = Updater() # 读 .app 的 Info.plist 配置
|
|
8
|
+
updater.check_for_updates() # 弹出平台原生更新窗口
|
|
9
|
+
|
|
10
|
+
详见 :class:`sparklehelper.updater.Updater`。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from . import errors, types
|
|
16
|
+
from ._backend._macos import Decision, UpdaterDelegate
|
|
17
|
+
from .errors import (
|
|
18
|
+
ConfigurationError,
|
|
19
|
+
NotABundleError,
|
|
20
|
+
SparkleError,
|
|
21
|
+
SparkleNotAvailableError,
|
|
22
|
+
UpdateCheckError,
|
|
23
|
+
WrongThreadError,
|
|
24
|
+
)
|
|
25
|
+
from .types import (
|
|
26
|
+
SystemProfileEntry,
|
|
27
|
+
UpdateCheckKind,
|
|
28
|
+
UpdateCheckResult,
|
|
29
|
+
UpdateInfo,
|
|
30
|
+
UserUpdateChoice,
|
|
31
|
+
UserUpdateStage,
|
|
32
|
+
UserUpdateState,
|
|
33
|
+
)
|
|
34
|
+
from .updater import Subscription, Updater, ensure_runnable
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.1"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Updater",
|
|
40
|
+
"UpdaterDelegate",
|
|
41
|
+
"Decision",
|
|
42
|
+
"Subscription",
|
|
43
|
+
"ensure_runnable",
|
|
44
|
+
"types",
|
|
45
|
+
"errors",
|
|
46
|
+
# dataclass 快捷导出
|
|
47
|
+
"UpdateInfo",
|
|
48
|
+
"SystemProfileEntry",
|
|
49
|
+
"UpdateCheckKind",
|
|
50
|
+
"UpdateCheckResult",
|
|
51
|
+
"UserUpdateChoice",
|
|
52
|
+
"UserUpdateStage",
|
|
53
|
+
"UserUpdateState",
|
|
54
|
+
# 异常快捷导出
|
|
55
|
+
"SparkleError",
|
|
56
|
+
"SparkleNotAvailableError",
|
|
57
|
+
"NotABundleError",
|
|
58
|
+
"ConfigurationError",
|
|
59
|
+
"UpdateCheckError",
|
|
60
|
+
"WrongThreadError",
|
|
61
|
+
"__version__",
|
|
62
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""平台后端选择器。
|
|
2
|
+
|
|
3
|
+
按 ``sys.platform`` 选择并返回一个 :class:`~sparklehelper._backend.base.UpdateBackend`
|
|
4
|
+
实现。后端类的 import 放在函数体内(惰性),保证:
|
|
5
|
+
|
|
6
|
+
- 非 darwin / 非 win32 平台 ``import sparklehelper._backend`` 本身不失败;
|
|
7
|
+
- 目标平台才 import 对应后端模块(macOS 不 import ctypes win32,Windows
|
|
8
|
+
不 import PyObjC),避免拉起无关的平台依赖。
|
|
9
|
+
|
|
10
|
+
后端类尚未全部实现时,调用 ``get_backend`` 在对应平台会抛
|
|
11
|
+
:class:`~sparklehelper.errors.SparkleNotAvailableError`,这是预期行为。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from ..errors import SparkleNotAvailableError
|
|
19
|
+
|
|
20
|
+
from .base import (
|
|
21
|
+
Callbacks,
|
|
22
|
+
SparkleExtras,
|
|
23
|
+
UpdateBackend,
|
|
24
|
+
UpdateConfig,
|
|
25
|
+
WinSparkleExtras,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"get_backend",
|
|
30
|
+
"UpdateBackend",
|
|
31
|
+
"UpdateConfig",
|
|
32
|
+
"Callbacks",
|
|
33
|
+
"SparkleExtras",
|
|
34
|
+
"WinSparkleExtras",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_backend() -> UpdateBackend:
|
|
39
|
+
"""返回当前平台的更新后端实例。
|
|
40
|
+
|
|
41
|
+
:raises SparkleNotAvailableError: 当前平台不支持,或对应后端模块缺失。
|
|
42
|
+
"""
|
|
43
|
+
platform = sys.platform
|
|
44
|
+
if platform == "darwin":
|
|
45
|
+
from ._macos import MacOSBackend
|
|
46
|
+
|
|
47
|
+
return MacOSBackend()
|
|
48
|
+
if platform == "win32":
|
|
49
|
+
from ._windows import WindowsBackend
|
|
50
|
+
|
|
51
|
+
return WindowsBackend()
|
|
52
|
+
raise SparkleNotAvailableError(
|
|
53
|
+
f"sparklehelper 不支持当前平台: {platform}(仅支持 macOS / Windows)"
|
|
54
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""macOS 后端包:Sparkle.framework 的加载与 ObjC 桥接。
|
|
2
|
+
|
|
3
|
+
把原 ``_objc`` / ``_bridge`` / ``delegates`` 三个模块的逻辑按功能边界拆分到
|
|
4
|
+
子模块,由 :class:`MacOSBackend` 组合,对外只暴露统一契约:
|
|
5
|
+
|
|
6
|
+
- :mod:`._loading`:Sparkle.framework 运行时加载(进程级缓存)
|
|
7
|
+
- :mod:`._runtime`:主线程约束 / bundle 解析 / KVO observer(进程级缓存)
|
|
8
|
+
- :mod:`._delegates`:``SPUUpdaterDelegate`` Python 抽象与 ObjC 适配器
|
|
9
|
+
- :mod:`._backend`::class:`MacOSBackend` 类(组合上述三者)
|
|
10
|
+
|
|
11
|
+
非 darwin 安全
|
|
12
|
+
--------------
|
|
13
|
+
本包顶层不 import PyObjC / Foundation。所有 ObjC import 延迟到函数体内
|
|
14
|
+
惰性执行,保证非 darwin 平台 ``import sparklehelper._backend._macos``
|
|
15
|
+
不会触发 ObjC 类注册或导入失败。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
# MacOSBackend + 公共符号(运行时函数重导出,供 Updater / 测试直接用)。
|
|
21
|
+
from ._backend import MacOSBackend
|
|
22
|
+
from ._delegates import Decision, UpdaterDelegate, make_delegate_adapter
|
|
23
|
+
from ._runtime import (
|
|
24
|
+
Subscription,
|
|
25
|
+
assert_main_thread,
|
|
26
|
+
bundle_info_plist,
|
|
27
|
+
get_kvo_observer_class,
|
|
28
|
+
host_bundle_path,
|
|
29
|
+
in_app_bundle,
|
|
30
|
+
on_main_thread,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"MacOSBackend",
|
|
35
|
+
"UpdaterDelegate",
|
|
36
|
+
"Decision",
|
|
37
|
+
"Subscription",
|
|
38
|
+
"make_delegate_adapter",
|
|
39
|
+
"assert_main_thread",
|
|
40
|
+
"on_main_thread",
|
|
41
|
+
"host_bundle_path",
|
|
42
|
+
"in_app_bundle",
|
|
43
|
+
"bundle_info_plist",
|
|
44
|
+
"get_kvo_observer_class",
|
|
45
|
+
]
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""macOS 更新后端:组合 Sparkle 加载、运行时基础设施与 delegate 桥接。
|
|
2
|
+
|
|
3
|
+
:class:`MacOSBackend` 实现
|
|
4
|
+
:class:`~sparklehelper._backend.base.UpdateBackend` 契约 + macOS 独有能力
|
|
5
|
+
(KVO / system_profile / 24+ delegate 方法)。它本身只持有 controller /
|
|
6
|
+
updater 实例状态与 ObjC selector 调用,底层逻辑由同包的
|
|
7
|
+
:mod:`._loading` / :mod:`._runtime` / :mod:`._delegates` 提供。
|
|
8
|
+
|
|
9
|
+
所有方法**必须在主线程**调用(``Updater`` Facade 统一断言)。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Callable, Optional
|
|
16
|
+
|
|
17
|
+
from ...errors import SparkleNotAvailableError
|
|
18
|
+
from ...types import SystemProfileEntry, from_system_profile
|
|
19
|
+
from ..base import Callbacks, UpdateConfig
|
|
20
|
+
from . import _delegates, _loading, _runtime
|
|
21
|
+
|
|
22
|
+
# Sparkle 2.9 明确声明为 KVO-compliant 的属性。
|
|
23
|
+
_KVO_PROPERTIES: dict[str, tuple[str, Callable[[Any], Any]]] = {
|
|
24
|
+
"can_check_for_updates": ("canCheckForUpdates", bool),
|
|
25
|
+
"automatically_checks_for_updates": ("automaticallyChecksForUpdates", bool),
|
|
26
|
+
"update_check_interval": ("updateCheckInterval", float),
|
|
27
|
+
"automatically_downloads_updates": ("automaticallyDownloadsUpdates", bool),
|
|
28
|
+
"allows_automatic_updates": ("allowsAutomaticUpdates", bool),
|
|
29
|
+
"sends_system_profile": ("sendsSystemProfile", bool),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MacOSBackend:
|
|
34
|
+
"""macOS 更新后端:Sparkle.framework 的加载与 ObjC 桥接。
|
|
35
|
+
|
|
36
|
+
实现 :class:`~sparklehelper._backend.base.UpdateBackend` 契约 +
|
|
37
|
+
macOS 独有能力(KVO / system_profile / 24+ delegate 方法)。
|
|
38
|
+
|
|
39
|
+
所有方法**必须在主线程**调用(``Updater`` Facade 统一断言)。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# 进程级缓存管理(类方法,供测试与诊断)
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def is_loaded(cls) -> bool:
|
|
48
|
+
"""Sparkle.framework 是否已成功加载。"""
|
|
49
|
+
return _loading.is_loaded()
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def loaded_path(cls) -> Optional[str]:
|
|
53
|
+
"""已加载 framework 的磁盘路径;未加载时为 None。"""
|
|
54
|
+
return _loading.loaded_path()
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _reset_for_test(cls) -> None:
|
|
58
|
+
"""仅供测试:清空所有进程级缓存。"""
|
|
59
|
+
_loading.reset_for_test()
|
|
60
|
+
|
|
61
|
+
# 透传给 _loading,便于测试 patch(与原 _objc 模块函数签名一致)。
|
|
62
|
+
_main_bundle_frameworks_path = staticmethod(_loading.main_bundle_frameworks_path)
|
|
63
|
+
_resolve_framework_path = staticmethod(_loading.resolve_framework_path)
|
|
64
|
+
load_sparkle = staticmethod(_loading.load_sparkle)
|
|
65
|
+
get_sparkle = staticmethod(_loading.get_sparkle)
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# ObjC 工具
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _look_up(sparkle_module: Any, class_name: str) -> Any:
|
|
73
|
+
"""从已加载的 Sparkle module 取出类,先查 globals 再 lookUpClass 兜底。"""
|
|
74
|
+
cls = getattr(sparkle_module, class_name, None)
|
|
75
|
+
if cls is not None:
|
|
76
|
+
return cls
|
|
77
|
+
try:
|
|
78
|
+
import objc
|
|
79
|
+
|
|
80
|
+
return objc.lookUpClass(class_name)
|
|
81
|
+
except Exception as exc: # noqa: BLE001
|
|
82
|
+
raise SparkleNotAvailableError(
|
|
83
|
+
f"Sparkle.framework loaded but cannot get class {class_name}: {exc}"
|
|
84
|
+
) from exc
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# 构造与生命周期(UpdateBackend Protocol)
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def __init__(self) -> None:
|
|
91
|
+
self._controller: Any = None
|
|
92
|
+
self._updater: Any = None
|
|
93
|
+
self._delegate: Any = None
|
|
94
|
+
self._delegate_adapter: Any = None
|
|
95
|
+
self._started: bool = False
|
|
96
|
+
|
|
97
|
+
def configure(self, config: UpdateConfig) -> None:
|
|
98
|
+
"""加载 Sparkle、创建 controller(复刻原 Updater.__init__ 创建链)。
|
|
99
|
+
|
|
100
|
+
顺序:load_sparkle → make_delegate_adapter → alloc().initWith...
|
|
101
|
+
selector 名 ``initWithStartingUpdater_updaterDelegate_userDriverDelegate_``
|
|
102
|
+
来自 framework introspection,不可改名。
|
|
103
|
+
|
|
104
|
+
``delegate`` 通过 :attr:`UpdateConfig.delegate` 传入。
|
|
105
|
+
"""
|
|
106
|
+
_runtime.assert_main_thread()
|
|
107
|
+
|
|
108
|
+
sparkle = self.get_sparkle()
|
|
109
|
+
SPUStandardUpdaterController = self._look_up(
|
|
110
|
+
sparkle, "SPUStandardUpdaterController"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
delegate = config.delegate
|
|
114
|
+
self._delegate = delegate
|
|
115
|
+
delegate_adapter = _delegates.make_delegate_adapter(delegate)
|
|
116
|
+
self._delegate_adapter = delegate_adapter
|
|
117
|
+
|
|
118
|
+
self._controller = (
|
|
119
|
+
SPUStandardUpdaterController.alloc()
|
|
120
|
+
.initWithStartingUpdater_updaterDelegate_userDriverDelegate_(
|
|
121
|
+
False, delegate_adapter, None
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
if self._controller is None:
|
|
125
|
+
raise SparkleNotAvailableError(
|
|
126
|
+
"SPUStandardUpdaterController 初始化失败(返回 nil)。"
|
|
127
|
+
"请检查 Info.plist 是否包含 SUFeedURL、SUPublicEDKey、"
|
|
128
|
+
"CFBundleVersion 等必需字段。"
|
|
129
|
+
)
|
|
130
|
+
self._updater = self._controller.updater()
|
|
131
|
+
|
|
132
|
+
def register_callbacks(self, callbacks: Callbacks) -> None:
|
|
133
|
+
"""注册跨平台回调集合。
|
|
134
|
+
|
|
135
|
+
macOS 后端通过 :class:`~._delegates.UpdaterDelegate` 体系接收回调。
|
|
136
|
+
粗粒度回调(error/found/no_update/cancelled)由 delegate 的对应方法
|
|
137
|
+
承载;此方法当前为 Protocol 完整性保留。
|
|
138
|
+
"""
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
def start(self) -> None:
|
|
142
|
+
"""启动更新检查调度器:``[controller startUpdater]``。幂等。"""
|
|
143
|
+
_runtime.assert_main_thread()
|
|
144
|
+
if self._controller is None:
|
|
145
|
+
raise SparkleNotAvailableError(
|
|
146
|
+
"后端尚未配置,请先调用 configure()。"
|
|
147
|
+
)
|
|
148
|
+
if self._started:
|
|
149
|
+
return
|
|
150
|
+
self._controller.startUpdater()
|
|
151
|
+
self._started = True
|
|
152
|
+
|
|
153
|
+
def cleanup(self) -> None:
|
|
154
|
+
"""释放资源。macOS 下 Sparkle 无显式清理 API,空操作。"""
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# 手动检查(UpdateBackend Protocol)
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def check_for_updates(self) -> None:
|
|
162
|
+
"""弹出标准更新窗口:``[controller checkForUpdates:nil]``。"""
|
|
163
|
+
_runtime.assert_main_thread()
|
|
164
|
+
self._controller.checkForUpdates_(None)
|
|
165
|
+
|
|
166
|
+
def check_for_updates_in_background(self) -> None:
|
|
167
|
+
"""后台静默检查:``[updater checkForUpdatesInBackground]``。"""
|
|
168
|
+
_runtime.assert_main_thread()
|
|
169
|
+
self._updater.checkForUpdatesInBackground()
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# macOS 独有检查方法
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def check_for_update_information(self) -> None:
|
|
176
|
+
"""仅拉取 appcast 信息,不触发起安装流程。"""
|
|
177
|
+
_runtime.assert_main_thread()
|
|
178
|
+
self._updater.checkForUpdateInformation()
|
|
179
|
+
|
|
180
|
+
def reset_update_cycle(self) -> None:
|
|
181
|
+
"""重置自动检查计时,立即开始新一轮调度。"""
|
|
182
|
+
_runtime.assert_main_thread()
|
|
183
|
+
self._updater.resetUpdateCycle()
|
|
184
|
+
|
|
185
|
+
def reset_update_cycle_after_short_delay(self) -> None:
|
|
186
|
+
"""短暂延迟后重置自动检查计时。"""
|
|
187
|
+
_runtime.assert_main_thread()
|
|
188
|
+
self._updater.resetUpdateCycleAfterShortDelay()
|
|
189
|
+
|
|
190
|
+
def clear_feed_url_from_user_defaults(self) -> Optional[str]:
|
|
191
|
+
"""清除此前持久化的 feed URL,返回被清除的值。"""
|
|
192
|
+
_runtime.assert_main_thread()
|
|
193
|
+
url = self._updater.clearFeedURLFromUserDefaults()
|
|
194
|
+
if url is None:
|
|
195
|
+
return None
|
|
196
|
+
absolute_string = getattr(url, "absoluteString", None)
|
|
197
|
+
if callable(absolute_string):
|
|
198
|
+
value = absolute_string()
|
|
199
|
+
return str(value) if value else None
|
|
200
|
+
return str(url) or None
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
# 只读状态(UpdateBackend Protocol + SparkleExtras)
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def can_check_for_updates(self) -> bool:
|
|
208
|
+
_runtime.assert_main_thread()
|
|
209
|
+
return bool(self._updater.canCheckForUpdates())
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def session_in_progress(self) -> bool:
|
|
213
|
+
_runtime.assert_main_thread()
|
|
214
|
+
return bool(self._updater.sessionInProgress())
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def feed_url(self) -> Optional[str]:
|
|
218
|
+
_runtime.assert_main_thread()
|
|
219
|
+
url = self._updater.feedURL()
|
|
220
|
+
if url is None:
|
|
221
|
+
return None
|
|
222
|
+
if hasattr(url, "absoluteString"):
|
|
223
|
+
abs_str = url.absoluteString()
|
|
224
|
+
return str(abs_str) if abs_str else None
|
|
225
|
+
return str(url) or None
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def host_bundle_path(self) -> str:
|
|
229
|
+
_runtime.assert_main_thread()
|
|
230
|
+
bundle = self._updater.hostBundle()
|
|
231
|
+
path = bundle.bundlePath() if bundle is not None else None
|
|
232
|
+
return str(path) if path else ""
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def last_update_check_date(self) -> Optional[datetime]:
|
|
236
|
+
_runtime.assert_main_thread()
|
|
237
|
+
date = self._updater.lastUpdateCheckDate()
|
|
238
|
+
if date is None:
|
|
239
|
+
return None
|
|
240
|
+
try:
|
|
241
|
+
ts = float(date.timeIntervalSince1970())
|
|
242
|
+
except (AttributeError, TypeError, ValueError):
|
|
243
|
+
return None
|
|
244
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def system_profile(self) -> list[SystemProfileEntry]:
|
|
248
|
+
_runtime.assert_main_thread()
|
|
249
|
+
entries = self._updater.systemProfileArray()
|
|
250
|
+
return from_system_profile(entries)
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def allows_automatic_updates(self) -> bool:
|
|
254
|
+
_runtime.assert_main_thread()
|
|
255
|
+
return bool(self._updater.allowsAutomaticUpdates())
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def automatically_downloads_updates(self) -> bool:
|
|
259
|
+
_runtime.assert_main_thread()
|
|
260
|
+
return bool(self._updater.automaticallyDownloadsUpdates())
|
|
261
|
+
|
|
262
|
+
@automatically_downloads_updates.setter
|
|
263
|
+
def automatically_downloads_updates(self, value: bool) -> None:
|
|
264
|
+
_runtime.assert_main_thread()
|
|
265
|
+
self._updater.setAutomaticallyDownloadsUpdates_(bool(value))
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# 可读写状态(UpdateBackend Protocol + SparkleExtras)
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def automatically_checks_for_updates(self) -> bool:
|
|
273
|
+
_runtime.assert_main_thread()
|
|
274
|
+
return bool(self._updater.automaticallyChecksForUpdates())
|
|
275
|
+
|
|
276
|
+
@automatically_checks_for_updates.setter
|
|
277
|
+
def automatically_checks_for_updates(self, value: bool) -> None:
|
|
278
|
+
_runtime.assert_main_thread()
|
|
279
|
+
self._updater.setAutomaticallyChecksForUpdates_(bool(value))
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def update_check_interval(self) -> float:
|
|
283
|
+
_runtime.assert_main_thread()
|
|
284
|
+
return float(self._updater.updateCheckInterval())
|
|
285
|
+
|
|
286
|
+
@update_check_interval.setter
|
|
287
|
+
def update_check_interval(self, seconds: float) -> None:
|
|
288
|
+
_runtime.assert_main_thread()
|
|
289
|
+
self._updater.setUpdateCheckInterval_(float(seconds))
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def http_headers(self) -> Optional[dict[str, str]]:
|
|
293
|
+
_runtime.assert_main_thread()
|
|
294
|
+
headers = self._updater.httpHeaders()
|
|
295
|
+
if headers is None:
|
|
296
|
+
return None
|
|
297
|
+
return {str(k): str(v) for k, v in headers.items()}
|
|
298
|
+
|
|
299
|
+
@http_headers.setter
|
|
300
|
+
def http_headers(self, headers: Optional[dict[str, str]]) -> None:
|
|
301
|
+
_runtime.assert_main_thread()
|
|
302
|
+
if headers is None:
|
|
303
|
+
self._updater.setHttpHeaders_(None)
|
|
304
|
+
return
|
|
305
|
+
from Foundation import NSDictionary
|
|
306
|
+
|
|
307
|
+
ns_dict = NSDictionary.dictionaryWithDictionary_(
|
|
308
|
+
{str(k): str(v) for k, v in headers.items()}
|
|
309
|
+
)
|
|
310
|
+
self._updater.setHttpHeaders_(ns_dict)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def user_agent_string(self) -> str:
|
|
314
|
+
_runtime.assert_main_thread()
|
|
315
|
+
value = self._updater.userAgentString()
|
|
316
|
+
return str(value) if value else ""
|
|
317
|
+
|
|
318
|
+
@user_agent_string.setter
|
|
319
|
+
def user_agent_string(self, value: str) -> None:
|
|
320
|
+
_runtime.assert_main_thread()
|
|
321
|
+
self._updater.setUserAgentString_(str(value))
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def sends_system_profile(self) -> bool:
|
|
325
|
+
_runtime.assert_main_thread()
|
|
326
|
+
return bool(self._updater.sendsSystemProfile())
|
|
327
|
+
|
|
328
|
+
@sends_system_profile.setter
|
|
329
|
+
def sends_system_profile(self, value: bool) -> None:
|
|
330
|
+
_runtime.assert_main_thread()
|
|
331
|
+
self._updater.setSendsSystemProfile_(bool(value))
|
|
332
|
+
|
|
333
|
+
# ------------------------------------------------------------------
|
|
334
|
+
# KVO 订阅
|
|
335
|
+
# ------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
def observe(
|
|
338
|
+
self, property_name: str, callback: Callable[[Any], None]
|
|
339
|
+
) -> "_runtime.Subscription":
|
|
340
|
+
"""订阅 Sparkle 公开的 KVO 属性。订阅建立后立即回调一次。"""
|
|
341
|
+
_runtime.assert_main_thread()
|
|
342
|
+
try:
|
|
343
|
+
key_path, converter = _KVO_PROPERTIES[property_name]
|
|
344
|
+
except KeyError as exc:
|
|
345
|
+
supported = ", ".join(sorted(_KVO_PROPERTIES))
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"unsupported observable property {property_name!r}; "
|
|
348
|
+
f"available: {supported}"
|
|
349
|
+
) from exc
|
|
350
|
+
|
|
351
|
+
target = self._updater
|
|
352
|
+
observer_cls = _runtime.get_kvo_observer_class()
|
|
353
|
+
|
|
354
|
+
def wrapped(new_value: Any) -> None:
|
|
355
|
+
if new_value is not None:
|
|
356
|
+
callback(converter(new_value))
|
|
357
|
+
|
|
358
|
+
observer = observer_cls.alloc().initWithCallback_target_keyPath_(
|
|
359
|
+
wrapped, target, key_path
|
|
360
|
+
)
|
|
361
|
+
from Foundation import (
|
|
362
|
+
NSKeyValueObservingOptionInitial,
|
|
363
|
+
NSKeyValueObservingOptionNew,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
|
|
367
|
+
target.addObserver_forKeyPath_options_context_(observer, key_path, options, 0)
|
|
368
|
+
return _runtime.Subscription(observer=observer, target=target, key_path=key_path)
|
|
369
|
+
|
|
370
|
+
def observe_can_check_for_updates(
|
|
371
|
+
self, callback: Callable[[bool], None]
|
|
372
|
+
) -> "_runtime.Subscription":
|
|
373
|
+
"""订阅 ``canCheckForUpdates`` 变化(便捷封装)。"""
|
|
374
|
+
return self.observe("can_check_for_updates", callback)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
__all__ = ["MacOSBackend"]
|