lange-python 0.3.5__tar.gz → 0.3.7__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.
- {lange_python-0.3.5 → lange_python-0.3.7}/PKG-INFO +23 -2
- {lange_python-0.3.5 → lange_python-0.3.7}/README.md +18 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/distribution/_client.py +124 -3
- lange_python-0.3.7/lange/distribution/_update_macos.py +161 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/distribution/_util.py +20 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/pyproject.toml +2 -2
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/__main__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/_util/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/_util/_base_client.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/_util/_key_handling.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/build/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/build/_command.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/build/_discovery.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/build/_docker.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/build/_poetry.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/build/_types.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/code/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/code/_stats.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/distribution/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/cli/distribution/_command.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/distribution/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/tunnel/__init__.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/tunnel/_client.py +0 -0
- {lange_python-0.3.5 → lange_python-0.3.7}/lange/tunnel/_util.py +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lange-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: A bundeld set of tools, clients for the lange-suite of tools and more.
|
|
5
5
|
Author: contact@robertlange.me
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
10
13
|
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
@@ -30,6 +33,24 @@ lange distribution publish \
|
|
|
30
33
|
--os macos
|
|
31
34
|
```
|
|
32
35
|
|
|
36
|
+
Apply a macOS app update after downloading the published zip artifact:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
from lange.distribution import DistributionClient
|
|
42
|
+
|
|
43
|
+
client = DistributionClient(distribution_name="desktop-app", api_key="your-api-key")
|
|
44
|
+
|
|
45
|
+
update_metadata = client.update(
|
|
46
|
+
current_version="1.2.3",
|
|
47
|
+
installed_app_path=Path("/Applications/Desktop App.app"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
print(update_metadata["version"])
|
|
51
|
+
# The caller should now shut down so the detached helper can replace the app bundle.
|
|
52
|
+
```
|
|
53
|
+
|
|
33
54
|
## Tunnel worker
|
|
34
55
|
|
|
35
56
|
```python
|
|
@@ -15,6 +15,24 @@ lange distribution publish \
|
|
|
15
15
|
--os macos
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Apply a macOS app update after downloading the published zip artifact:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from lange.distribution import DistributionClient
|
|
24
|
+
|
|
25
|
+
client = DistributionClient(distribution_name="desktop-app", api_key="your-api-key")
|
|
26
|
+
|
|
27
|
+
update_metadata = client.update(
|
|
28
|
+
current_version="1.2.3",
|
|
29
|
+
installed_app_path=Path("/Applications/Desktop App.app"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
print(update_metadata["version"])
|
|
33
|
+
# The caller should now shut down so the detached helper can replace the app bundle.
|
|
34
|
+
```
|
|
35
|
+
|
|
18
36
|
## Tunnel worker
|
|
19
37
|
|
|
20
38
|
```python
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import mimetypes
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
8
9
|
|
|
9
|
-
from .
|
|
10
|
+
from ._update_macos import extract_macos_app_from_zip, spawn_macos_update_process
|
|
11
|
+
from ._util import (
|
|
12
|
+
_compare_versions,
|
|
13
|
+
detect_distribution_os,
|
|
14
|
+
parse_distribution_os,
|
|
15
|
+
validate_installed_macos_app_path,
|
|
16
|
+
)
|
|
10
17
|
from .._util import BaseLangeLabsClient
|
|
11
18
|
|
|
12
19
|
|
|
@@ -158,7 +165,87 @@ class DistributionClient(BaseLangeLabsClient):
|
|
|
158
165
|
if newest_match is None:
|
|
159
166
|
raise ValueError(f"No downloadable versions available for operating system: {current_os}.")
|
|
160
167
|
|
|
161
|
-
version,
|
|
168
|
+
version, _artifact = newest_match
|
|
169
|
+
|
|
170
|
+
return self._download_version_artifact_to_temp(version=version, os_name=current_os)
|
|
171
|
+
|
|
172
|
+
def update(
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
current_version: str,
|
|
176
|
+
installed_app_path: str | Path,
|
|
177
|
+
target_version: str | None = None,
|
|
178
|
+
current_process_id: int | None = None,
|
|
179
|
+
wait_for_exit_timeout: float = 30.0,
|
|
180
|
+
) -> dict[str, str]:
|
|
181
|
+
"""
|
|
182
|
+
Download and stage a macOS update, then spawn a detached helper to apply it.
|
|
183
|
+
|
|
184
|
+
:param current_version: Current installed version string.
|
|
185
|
+
:param installed_app_path: Path to the installed macOS ``.app`` bundle to replace.
|
|
186
|
+
:param target_version: Explicit target version to install. When omitted, the newest newer version is used.
|
|
187
|
+
:param current_process_id: PID that must exit before replacement begins. Defaults to the current process.
|
|
188
|
+
:param wait_for_exit_timeout: Maximum seconds the helper waits for the current process to exit.
|
|
189
|
+
:returns: Metadata describing the staged update and spawned helper process.
|
|
190
|
+
:raises ValueError: If the platform, app path, or artifact layout is invalid.
|
|
191
|
+
:raises RuntimeError: If the detached helper process could not be launched.
|
|
192
|
+
"""
|
|
193
|
+
current_os = detect_distribution_os()
|
|
194
|
+
if current_os != "macos":
|
|
195
|
+
raise ValueError(f"macOS updates are not implemented for operating system: {current_os}.")
|
|
196
|
+
|
|
197
|
+
normalized_current_version = current_version.strip()
|
|
198
|
+
if not normalized_current_version:
|
|
199
|
+
raise ValueError("current_version is required.")
|
|
200
|
+
|
|
201
|
+
normalized_installed_app_path = validate_installed_macos_app_path(installed_app_path)
|
|
202
|
+
resolved_target_version = target_version.strip() if target_version is not None else self.search_for_update(
|
|
203
|
+
normalized_current_version
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if not resolved_target_version:
|
|
207
|
+
raise ValueError("No newer update is available for the current macOS installation.")
|
|
208
|
+
|
|
209
|
+
artifact = self._get_version_artifact(version=resolved_target_version, os_name="macos")
|
|
210
|
+
if artifact is None:
|
|
211
|
+
raise ValueError(f"No macos artifact is available for version {resolved_target_version}.")
|
|
212
|
+
|
|
213
|
+
artifact_path = self._download_version_artifact_to_temp(version=resolved_target_version, os_name="macos")
|
|
214
|
+
extracted_app_path = extract_macos_app_from_zip(artifact_path)
|
|
215
|
+
helper_process = spawn_macos_update_process(
|
|
216
|
+
source_app_path=extracted_app_path,
|
|
217
|
+
installed_app_path=normalized_installed_app_path,
|
|
218
|
+
current_process_id=os.getpid() if current_process_id is None else current_process_id,
|
|
219
|
+
workspace_path=artifact_path.parent,
|
|
220
|
+
wait_for_exit_timeout=wait_for_exit_timeout,
|
|
221
|
+
)
|
|
222
|
+
process_id = helper_process.get("process_id", helper_process.get("pid", ""))
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"version": resolved_target_version,
|
|
226
|
+
"artifact_path": str(artifact_path),
|
|
227
|
+
"installed_app_path": str(normalized_installed_app_path),
|
|
228
|
+
"extracted_app_path": str(extracted_app_path),
|
|
229
|
+
"script_path": helper_process["script_path"],
|
|
230
|
+
"log_path": helper_process["log_path"],
|
|
231
|
+
"process_id": str(process_id),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def _download_version_artifact_to_temp(self, version: str, os_name: str) -> Path:
|
|
235
|
+
"""
|
|
236
|
+
Download one explicit version artifact for one operating system into ``/tmp``.
|
|
237
|
+
|
|
238
|
+
:param version: Distribution version to download.
|
|
239
|
+
:param os_name: Operating-system label accepted by the app service.
|
|
240
|
+
:returns: Absolute path to the downloaded artifact.
|
|
241
|
+
:raises ValueError: If the artifact metadata is incomplete or missing.
|
|
242
|
+
:raises httpx.HTTPError: If the metadata or artifact download fails.
|
|
243
|
+
"""
|
|
244
|
+
artifact = self._get_version_artifact(version=version, os_name=os_name)
|
|
245
|
+
|
|
246
|
+
if artifact is None:
|
|
247
|
+
raise ValueError(f"No {os_name} artifact is available for version {version}.")
|
|
248
|
+
|
|
162
249
|
filename = str(artifact.get("filename", "")).strip()
|
|
163
250
|
|
|
164
251
|
if not filename:
|
|
@@ -172,7 +259,7 @@ class DistributionClient(BaseLangeLabsClient):
|
|
|
172
259
|
|
|
173
260
|
try:
|
|
174
261
|
response = client.get(
|
|
175
|
-
self._build_artifact_download_url(version,
|
|
262
|
+
self._build_artifact_download_url(version, os_name),
|
|
176
263
|
headers=self._build_connection_headers(),
|
|
177
264
|
)
|
|
178
265
|
response.raise_for_status()
|
|
@@ -182,6 +269,40 @@ class DistributionClient(BaseLangeLabsClient):
|
|
|
182
269
|
|
|
183
270
|
return destination_path
|
|
184
271
|
|
|
272
|
+
def _get_version_artifact(self, version: str, os_name: str) -> dict[str, Any] | None:
|
|
273
|
+
"""
|
|
274
|
+
Resolve the downloadable artifact payload for one version and operating system.
|
|
275
|
+
|
|
276
|
+
:param version: Distribution version to inspect.
|
|
277
|
+
:param os_name: Operating-system label accepted by the app service.
|
|
278
|
+
:returns: Artifact metadata for the version and OS, otherwise ``None``.
|
|
279
|
+
:raises ValueError: If the version is missing.
|
|
280
|
+
:raises httpx.HTTPError: If the metadata request fails.
|
|
281
|
+
"""
|
|
282
|
+
normalized_version = version.strip()
|
|
283
|
+
normalized_os_name = parse_distribution_os(os_name)
|
|
284
|
+
|
|
285
|
+
if not normalized_version:
|
|
286
|
+
raise ValueError("version is required.")
|
|
287
|
+
|
|
288
|
+
payload = self._get_distribution_payload()
|
|
289
|
+
versions = payload.get("distribution", {}).get("versions", [])
|
|
290
|
+
|
|
291
|
+
for version_entry in versions:
|
|
292
|
+
if not isinstance(version_entry, dict):
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
if str(version_entry.get("version", "")).strip() != normalized_version:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
artifacts = version_entry.get("artifacts", {})
|
|
299
|
+
artifact = artifacts.get(normalized_os_name) if isinstance(artifacts, dict) else None
|
|
300
|
+
if isinstance(artifact, dict):
|
|
301
|
+
return artifact
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
return None
|
|
305
|
+
|
|
185
306
|
def upload(self, path: str | Path, version_name: str, os_name: str) -> dict[str, Any]:
|
|
186
307
|
"""
|
|
187
308
|
Upload one OS-specific distribution artifact for a version.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""macOS-specific helpers for the distribution update workflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import stat
|
|
7
|
+
import subprocess
|
|
8
|
+
import zipfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from shlex import quote
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_macos_app_from_zip(archive_path: str | Path) -> Path:
|
|
14
|
+
"""
|
|
15
|
+
Extract a macOS update archive and return the only top-level ``.app`` bundle.
|
|
16
|
+
|
|
17
|
+
:param archive_path: Path to the downloaded zip archive.
|
|
18
|
+
:returns: Absolute path to the extracted ``.app`` bundle.
|
|
19
|
+
:raises ValueError: If the archive is not a zip or does not contain exactly one top-level app bundle.
|
|
20
|
+
"""
|
|
21
|
+
normalized_archive_path = Path(archive_path).expanduser().resolve()
|
|
22
|
+
|
|
23
|
+
if normalized_archive_path.suffix.lower() != ".zip":
|
|
24
|
+
raise ValueError("macOS update artifacts must be .zip files.")
|
|
25
|
+
|
|
26
|
+
destination_path = normalized_archive_path.parent / "extracted"
|
|
27
|
+
if destination_path.exists():
|
|
28
|
+
shutil.rmtree(destination_path)
|
|
29
|
+
destination_path.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
with zipfile.ZipFile(normalized_archive_path) as archive:
|
|
32
|
+
archive.extractall(destination_path)
|
|
33
|
+
|
|
34
|
+
app_bundles = [
|
|
35
|
+
child.resolve()
|
|
36
|
+
for child in destination_path.iterdir()
|
|
37
|
+
if child.is_dir() and child.suffix == ".app"
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
if len(app_bundles) != 1:
|
|
41
|
+
raise ValueError("macOS update archives must contain exactly one top-level .app bundle.")
|
|
42
|
+
|
|
43
|
+
return app_bundles[0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def spawn_macos_update_process(
|
|
47
|
+
*,
|
|
48
|
+
source_app_path: str | Path,
|
|
49
|
+
installed_app_path: str | Path,
|
|
50
|
+
current_process_id: int,
|
|
51
|
+
workspace_path: str | Path,
|
|
52
|
+
wait_for_exit_timeout: float,
|
|
53
|
+
) -> dict[str, str]:
|
|
54
|
+
"""
|
|
55
|
+
Generate and launch the detached macOS helper process that applies the update.
|
|
56
|
+
|
|
57
|
+
:param source_app_path: Extracted replacement app bundle.
|
|
58
|
+
:param installed_app_path: Installed app bundle that should be replaced.
|
|
59
|
+
:param current_process_id: PID that must exit before replacement begins.
|
|
60
|
+
:param workspace_path: Temporary workspace for helper files and logs.
|
|
61
|
+
:param wait_for_exit_timeout: Maximum number of seconds to wait for the PID to exit.
|
|
62
|
+
:returns: Metadata about the spawned helper process and generated files.
|
|
63
|
+
:raises RuntimeError: If the helper process could not be launched.
|
|
64
|
+
"""
|
|
65
|
+
normalized_source_app_path = Path(source_app_path).expanduser().resolve()
|
|
66
|
+
normalized_installed_app_path = Path(installed_app_path).expanduser().resolve()
|
|
67
|
+
normalized_workspace_path = Path(workspace_path).expanduser().resolve()
|
|
68
|
+
|
|
69
|
+
normalized_workspace_path.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
backup_app_path = normalized_installed_app_path.parent / f"{normalized_installed_app_path.name}.previous"
|
|
72
|
+
script_path = normalized_workspace_path / "apply-update.sh"
|
|
73
|
+
log_path = normalized_workspace_path / "apply-update.log"
|
|
74
|
+
script_contents = _build_macos_update_script(
|
|
75
|
+
source_app_path=normalized_source_app_path,
|
|
76
|
+
installed_app_path=normalized_installed_app_path,
|
|
77
|
+
backup_app_path=backup_app_path,
|
|
78
|
+
current_process_id=current_process_id,
|
|
79
|
+
wait_for_exit_timeout=wait_for_exit_timeout,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
script_path.write_text(script_contents, encoding="utf-8")
|
|
83
|
+
current_mode = script_path.stat().st_mode
|
|
84
|
+
script_path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
85
|
+
|
|
86
|
+
with log_path.open("ab") as log_file:
|
|
87
|
+
try:
|
|
88
|
+
process = subprocess.Popen(
|
|
89
|
+
["/bin/bash", str(script_path)],
|
|
90
|
+
stdout=log_file,
|
|
91
|
+
stderr=subprocess.STDOUT,
|
|
92
|
+
stdin=subprocess.DEVNULL,
|
|
93
|
+
start_new_session=True,
|
|
94
|
+
)
|
|
95
|
+
except OSError as error:
|
|
96
|
+
raise RuntimeError(f"Failed to launch the macOS updater helper process: {error}") from error
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"script_path": str(script_path),
|
|
100
|
+
"log_path": str(log_path),
|
|
101
|
+
"process_id": str(process.pid),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_macos_update_script(
|
|
106
|
+
*,
|
|
107
|
+
source_app_path: Path,
|
|
108
|
+
installed_app_path: Path,
|
|
109
|
+
backup_app_path: Path,
|
|
110
|
+
current_process_id: int,
|
|
111
|
+
wait_for_exit_timeout: float,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Build the shell script that applies the macOS app replacement with rollback.
|
|
115
|
+
|
|
116
|
+
:param source_app_path: Extracted replacement app bundle.
|
|
117
|
+
:param installed_app_path: Installed app bundle that should be replaced.
|
|
118
|
+
:param backup_app_path: Temporary backup location used for rollback.
|
|
119
|
+
:param current_process_id: PID that must exit before replacement begins.
|
|
120
|
+
:param wait_for_exit_timeout: Maximum number of seconds to wait for the PID to exit.
|
|
121
|
+
:returns: Shell script source code.
|
|
122
|
+
"""
|
|
123
|
+
timeout_seconds = max(int(wait_for_exit_timeout), 0)
|
|
124
|
+
|
|
125
|
+
return f"""#!/bin/bash
|
|
126
|
+
set -euo pipefail
|
|
127
|
+
|
|
128
|
+
SOURCE_APP={quote(str(source_app_path))}
|
|
129
|
+
TARGET_APP={quote(str(installed_app_path))}
|
|
130
|
+
BACKUP_APP={quote(str(backup_app_path))}
|
|
131
|
+
TARGET_PID={int(current_process_id)}
|
|
132
|
+
TIMEOUT_SECONDS={timeout_seconds}
|
|
133
|
+
WAITED_SECONDS=0
|
|
134
|
+
|
|
135
|
+
restore_backup() {{
|
|
136
|
+
if [ -d "$BACKUP_APP" ]; then
|
|
137
|
+
rm -rf "$TARGET_APP"
|
|
138
|
+
mv "$BACKUP_APP" "$TARGET_APP"
|
|
139
|
+
fi
|
|
140
|
+
}}
|
|
141
|
+
|
|
142
|
+
while kill -0 "$TARGET_PID" 2>/dev/null; do
|
|
143
|
+
if [ "$WAITED_SECONDS" -ge "$TIMEOUT_SECONDS" ]; then
|
|
144
|
+
echo "Timed out waiting for process $TARGET_PID to exit."
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
sleep 1
|
|
148
|
+
WAITED_SECONDS=$((WAITED_SECONDS + 1))
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
rm -rf "$BACKUP_APP"
|
|
152
|
+
|
|
153
|
+
trap 'restore_backup' ERR
|
|
154
|
+
|
|
155
|
+
if [ -d "$TARGET_APP" ]; then
|
|
156
|
+
mv "$TARGET_APP" "$BACKUP_APP"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
/usr/bin/ditto "$SOURCE_APP" "$TARGET_APP"
|
|
160
|
+
rm -rf "$BACKUP_APP"
|
|
161
|
+
"""
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import platform
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def detect_distribution_os() -> str:
|
|
@@ -89,3 +90,22 @@ def _normalize_version_parts(version: str) -> tuple[int, ...]:
|
|
|
89
90
|
normalized_parts.append(int(part))
|
|
90
91
|
|
|
91
92
|
return tuple(normalized_parts)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def validate_installed_macos_app_path(installed_app_path: str | Path) -> Path:
|
|
96
|
+
"""
|
|
97
|
+
Validate and normalize the macOS app bundle path that should be replaced.
|
|
98
|
+
|
|
99
|
+
:param installed_app_path: Untrusted path to the installed ``.app`` bundle.
|
|
100
|
+
:returns: Absolute path to the installed ``.app`` bundle.
|
|
101
|
+
:raises ValueError: If the path is missing, does not exist, or is not a ``.app`` directory.
|
|
102
|
+
"""
|
|
103
|
+
normalized_path = Path(installed_app_path).expanduser().resolve()
|
|
104
|
+
|
|
105
|
+
if not normalized_path.exists():
|
|
106
|
+
raise ValueError("installed_app_path must point to an existing .app bundle.")
|
|
107
|
+
|
|
108
|
+
if not normalized_path.is_dir() or normalized_path.suffix != ".app":
|
|
109
|
+
raise ValueError("installed_app_path must point to an existing .app bundle.")
|
|
110
|
+
|
|
111
|
+
return normalized_path
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lange-python"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.7"
|
|
4
4
|
description = "A bundeld set of tools, clients for the lange-suite of tools and more."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "contact@robertlange.me"}
|
|
7
7
|
]
|
|
8
8
|
readme = "README.md"
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"httpx (>=0.28.1,<0.29.0)",
|
|
12
12
|
"websockets (>=12.0,<20.0)",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|