dexverse-cli 0.1.4__tar.gz → 0.1.4.post1__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.
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/PKG-INFO +10 -3
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/README.md +9 -2
- dexverse_cli-0.1.4.post1/src/dexverse_cli/__init__.py +1 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/update/gate.py +34 -3
- dexverse_cli-0.1.4.post1/src/dexverse_cli/update/helper.py +56 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/update/upgrade.py +151 -4
- dexverse_cli-0.1.4/src/dexverse_cli/__init__.py +0 -1
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/.gitignore +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/.python-version +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/pyproject.toml +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/cli.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/common/__init__.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/common/hashes.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/common/state.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/download/__init__.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/download/batch.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/download/filesystem.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/download/http.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/download/manifest.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/download/runner.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/download/transfer.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/progress/__init__.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/progress/bar.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/progress/formatting.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/progress/reporter.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/progress/view.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/__init__.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/alignment.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/batch.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/instruction.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/layout.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/legacy.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/media.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/publish.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/runner.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/schema.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/session.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/restore/versioned.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/update/__init__.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/update/check.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/src/dexverse_cli/update/observation.py +0 -0
- {dexverse_cli-0.1.4 → dexverse_cli-0.1.4.post1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dexverse-cli
|
|
3
|
-
Version: 0.1.4
|
|
3
|
+
Version: 0.1.4.post1
|
|
4
4
|
Summary: Download DexVerse dataset manifests and unpack HDF5 sessions
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: av<17,>=14
|
|
@@ -39,8 +39,15 @@ falls back to the source this machine's pip is configured with (`pip.conf`,
|
|
|
39
39
|
`python -m pip install --upgrade dexverse-cli` from the source that answered,
|
|
40
40
|
and then exits with status **20**. Run the same command again; the rerun uses
|
|
41
41
|
the new version.
|
|
42
|
-
- The check times out, pip is missing, no source answers,
|
|
43
|
-
one line is printed and the
|
|
42
|
+
- The check times out, pip is missing, no source answers, the environment is not
|
|
43
|
+
writable (a system Python), or the upgrade fails: one line is printed and the
|
|
44
|
+
requested command still runs. A directory pip cannot write is refused before
|
|
45
|
+
pip starts, so a failed upgrade never leaves a half-installed package behind.
|
|
46
|
+
- On Windows, when the command was started from the installed `dexverse-dl.exe`,
|
|
47
|
+
the upgrade runs in the background: pip cannot replace that file while it is
|
|
48
|
+
the running program, so the CLI starts a helper that waits for this process to
|
|
49
|
+
exit and then upgrades. Run the command again shortly. Linux and macOS upgrade
|
|
50
|
+
in place, before the command continues.
|
|
44
51
|
|
|
45
52
|
The switch is on by default. Turn the check off for one command, or permanently:
|
|
46
53
|
|
|
@@ -24,8 +24,15 @@ falls back to the source this machine's pip is configured with (`pip.conf`,
|
|
|
24
24
|
`python -m pip install --upgrade dexverse-cli` from the source that answered,
|
|
25
25
|
and then exits with status **20**. Run the same command again; the rerun uses
|
|
26
26
|
the new version.
|
|
27
|
-
- The check times out, pip is missing, no source answers,
|
|
28
|
-
one line is printed and the
|
|
27
|
+
- The check times out, pip is missing, no source answers, the environment is not
|
|
28
|
+
writable (a system Python), or the upgrade fails: one line is printed and the
|
|
29
|
+
requested command still runs. A directory pip cannot write is refused before
|
|
30
|
+
pip starts, so a failed upgrade never leaves a half-installed package behind.
|
|
31
|
+
- On Windows, when the command was started from the installed `dexverse-dl.exe`,
|
|
32
|
+
the upgrade runs in the background: pip cannot replace that file while it is
|
|
33
|
+
the running program, so the CLI starts a helper that waits for this process to
|
|
34
|
+
exit and then upgrades. Run the command again shortly. Linux and macOS upgrade
|
|
35
|
+
in place, before the command continues.
|
|
29
36
|
|
|
30
37
|
The switch is on by default. Turn the check off for one command, or permanently:
|
|
31
38
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.4.post1"
|
|
@@ -55,18 +55,46 @@ def upgrade_failure_message(result: upgrade.UpgradeOutcome) -> str:
|
|
|
55
55
|
"`python -m pip install --force-reinstall dexverse-cli`. Continuing with the requested command."
|
|
56
56
|
)
|
|
57
57
|
return (
|
|
58
|
-
f"Automatic upgrade to {result.version} failed ({result.
|
|
59
|
-
"
|
|
58
|
+
f"Automatic upgrade to {result.version} failed ({result.detail}); "
|
|
59
|
+
"if the next command reports a missing dexverse_cli module, repair it with "
|
|
60
|
+
"`python -m pip install --force-reinstall --upgrade dexverse-cli`. "
|
|
60
61
|
"Continuing with the requested command."
|
|
61
62
|
)
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
def background_upgrade(observed: check.CheckOutcome) -> None:
|
|
66
|
+
"""@brief Upgrade from a detached helper when this process locks its own launcher.
|
|
67
|
+
@param observed Release observation that selected the target version.
|
|
68
|
+
@throws SystemExit With 20 once the helper owns the upgrade.
|
|
69
|
+
"""
|
|
70
|
+
if upgrade.upgrade_pending():
|
|
71
|
+
print("An upgrade is already running in the background; run the command again shortly.", file=sys.stderr)
|
|
72
|
+
raise SystemExit(RERUN_EXIT_CODE)
|
|
73
|
+
target = str(observed.latest)
|
|
74
|
+
with contextlib.suppress(OSError, ValueError):
|
|
75
|
+
upgrade.mark_pending(target)
|
|
76
|
+
if upgrade.start_detached(target, check.prerelease_allowed(), observed.source):
|
|
77
|
+
print(
|
|
78
|
+
f"dexverse-cli {target} is available; upgrading in the background because the running launcher "
|
|
79
|
+
"cannot be replaced while it runs. Run the command again shortly.",
|
|
80
|
+
file=sys.stderr,
|
|
81
|
+
)
|
|
82
|
+
raise SystemExit(RERUN_EXIT_CODE)
|
|
83
|
+
upgrade.clear_pending()
|
|
84
|
+
print(
|
|
85
|
+
"Automatic upgrade needs this launcher to exit first; run "
|
|
86
|
+
"`python -m pip install --upgrade dexverse-cli` yourself. Continuing with the requested command.",
|
|
87
|
+
file=sys.stderr,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
64
91
|
def update_gate(*, check_enabled: bool = True, index_url: str | None = None) -> None:
|
|
65
92
|
"""@brief Run before any business import, stdin read, or file access.
|
|
66
93
|
@param check_enabled False when the caller passed --no-update-check.
|
|
67
94
|
@param index_url Optional release index for this command; publishing it as the
|
|
68
95
|
environment variable keeps the check and the upgrade on one source.
|
|
69
|
-
@throws SystemExit With 20
|
|
96
|
+
@throws SystemExit With 20 after a verified upgrade, or once a background
|
|
97
|
+
upgrade owns the environment.
|
|
70
98
|
"""
|
|
71
99
|
if not check_enabled:
|
|
72
100
|
return
|
|
@@ -74,6 +102,9 @@ def update_gate(*, check_enabled: bool = True, index_url: str | None = None) ->
|
|
|
74
102
|
os.environ[check.INDEX_ENVIRONMENT] = index_url
|
|
75
103
|
observed = check.check_for_update()
|
|
76
104
|
if observed.status == check.STATUS_OUTDATED:
|
|
105
|
+
if upgrade.console_script_locked():
|
|
106
|
+
background_upgrade(observed)
|
|
107
|
+
return
|
|
77
108
|
print(
|
|
78
109
|
f"dexverse-cli {observed.latest} is available (running {observed.installed}); upgrading...",
|
|
79
110
|
file=sys.stderr,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Background upgrade helper: replaces the package once the caller has exited."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from dexverse_cli import __version__
|
|
12
|
+
from dexverse_cli.update import check, observation, upgrade
|
|
13
|
+
|
|
14
|
+
STARTUP_GRACE_S = 3.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def record(outcome: upgrade.UpgradeOutcome, target: str) -> None:
|
|
18
|
+
"""@brief Persist what the background upgrade actually did.
|
|
19
|
+
@param outcome Result of the retrying upgrade.
|
|
20
|
+
@param target Version the helper was asked to install.
|
|
21
|
+
"""
|
|
22
|
+
succeeded = outcome.status == upgrade.STATUS_UPGRADED
|
|
23
|
+
operation = observation.UpdateOperation(
|
|
24
|
+
str(uuid4()),
|
|
25
|
+
"succeeded" if succeeded else "failed",
|
|
26
|
+
"upgraded" if succeeded else "upgrading",
|
|
27
|
+
__version__,
|
|
28
|
+
target,
|
|
29
|
+
datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
30
|
+
"" if succeeded else outcome.status,
|
|
31
|
+
)
|
|
32
|
+
with contextlib.suppress(OSError, ValueError):
|
|
33
|
+
observation.record_operation(check.install_mode(), __version__, operation)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(arguments: list[str] | None = None) -> int:
|
|
37
|
+
"""@brief Upgrade this environment after the caller released its launcher.
|
|
38
|
+
@param arguments Optional argument vector for tests; defaults to the command line.
|
|
39
|
+
@return Zero when an upgrade was verified, otherwise one.
|
|
40
|
+
"""
|
|
41
|
+
values = sys.argv[1:] if arguments is None else arguments
|
|
42
|
+
if len(values) < 2:
|
|
43
|
+
return 2
|
|
44
|
+
version, flag = values[0], values[1]
|
|
45
|
+
source = tuple(values[2:])
|
|
46
|
+
# The caller must be gone before pip may delete its launcher, and everything
|
|
47
|
+
# this process needs is already imported, so the sleep is safe here.
|
|
48
|
+
time.sleep(STARTUP_GRACE_S)
|
|
49
|
+
outcome = upgrade.retry_upgrade(version, flag == "1", source=source)
|
|
50
|
+
upgrade.clear_pending()
|
|
51
|
+
record(outcome, version)
|
|
52
|
+
return 0 if outcome.status == upgrade.STATUS_UPGRADED else 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
raise SystemExit(main())
|
|
@@ -7,16 +7,28 @@ import importlib.metadata
|
|
|
7
7
|
import os
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
10
12
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
11
13
|
from dataclasses import dataclass
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
|
|
16
|
+
import dexverse_cli
|
|
14
17
|
from dexverse_cli.update.check import PACKAGE, source_arguments
|
|
15
18
|
from dexverse_cli.update.observation import update_state_path
|
|
16
19
|
|
|
17
20
|
UPGRADE_TIMEOUT_S = 300.0
|
|
18
21
|
LOCK_NAME = "upgrade.lock"
|
|
19
|
-
|
|
22
|
+
PENDING_NAME = "upgrade.pending.json"
|
|
23
|
+
PENDING_TTL_S = 300.0
|
|
24
|
+
RETRY_ATTEMPTS = 10
|
|
25
|
+
RETRY_DELAY_S = 2.0
|
|
26
|
+
MAX_DETAIL_CHARS = 320
|
|
27
|
+
DETAIL_TAIL_LINES = 3
|
|
28
|
+
|
|
29
|
+
# Failure reasons that may only mean the launcher was still running, so one more
|
|
30
|
+
# attempt after the caller exits can still succeed.
|
|
31
|
+
LOCK_REASONS = ("denied", "permission", "--user", "did not change")
|
|
20
32
|
|
|
21
33
|
STATUS_UPGRADED = "upgraded"
|
|
22
34
|
STATUS_BUSY = "busy"
|
|
@@ -161,15 +173,42 @@ def installed_version() -> str | None:
|
|
|
161
173
|
|
|
162
174
|
|
|
163
175
|
def failure_detail(completed: subprocess.CompletedProcess[str]) -> str:
|
|
164
|
-
"""@brief Summarize pip output into one bounded line
|
|
176
|
+
"""@brief Summarize pip output into one bounded line that names the real blocker.
|
|
165
177
|
@param completed Finished pip process.
|
|
166
|
-
@return
|
|
178
|
+
@return Tail of pip's output, truncated; pip puts its reason before its advice.
|
|
167
179
|
"""
|
|
168
180
|
text = (completed.stderr or "").strip() or (completed.stdout or "").strip()
|
|
169
181
|
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
170
182
|
if not lines:
|
|
171
183
|
return f"pip exited with {completed.returncode}"
|
|
172
|
-
|
|
184
|
+
# pip ends a failure with generic advice ("Consider using the --user option"),
|
|
185
|
+
# so keep the lines before it: they name the file or the reason.
|
|
186
|
+
return " | ".join(lines[-DETAIL_TAIL_LINES:])[:MAX_DETAIL_CHARS]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def install_target() -> Path | None:
|
|
190
|
+
"""@brief Locate the directory pip has to write to replace this package.
|
|
191
|
+
@return Package parent directory, or None when it cannot be determined.
|
|
192
|
+
"""
|
|
193
|
+
module = getattr(dexverse_cli, "__file__", None)
|
|
194
|
+
if not module:
|
|
195
|
+
return None
|
|
196
|
+
return Path(module).resolve().parent.parent
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def writable_target() -> bool:
|
|
200
|
+
"""@brief Prove pip can write there before it is allowed to touch anything.
|
|
201
|
+
@return True when a probe file could be created and removed.
|
|
202
|
+
"""
|
|
203
|
+
target = install_target()
|
|
204
|
+
if target is None or not target.is_dir():
|
|
205
|
+
return False
|
|
206
|
+
try:
|
|
207
|
+
with tempfile.NamedTemporaryFile(dir=target, prefix=".dexverse-write-probe-", delete=True):
|
|
208
|
+
pass
|
|
209
|
+
except OSError:
|
|
210
|
+
return False
|
|
211
|
+
return True
|
|
173
212
|
|
|
174
213
|
|
|
175
214
|
def run_upgrade(
|
|
@@ -188,6 +227,14 @@ def run_upgrade(
|
|
|
188
227
|
with update_lock() as owned:
|
|
189
228
|
if not owned:
|
|
190
229
|
return UpgradeOutcome(STATUS_BUSY, version, "another dexverse-cli process is upgrading")
|
|
230
|
+
# A pip run that cannot finish still removes the old files first, which
|
|
231
|
+
# leaves a half-installed package behind. Refuse it up front instead.
|
|
232
|
+
if not writable_target():
|
|
233
|
+
return UpgradeOutcome(
|
|
234
|
+
STATUS_UNAVAILABLE,
|
|
235
|
+
version,
|
|
236
|
+
"installation directory is not writable; run `python -m pip install --user --upgrade dexverse-cli`",
|
|
237
|
+
)
|
|
191
238
|
try:
|
|
192
239
|
completed = (runner or run_pip)(upgrade_arguments(prerelease, source), UPGRADE_TIMEOUT_S)
|
|
193
240
|
except subprocess.TimeoutExpired:
|
|
@@ -201,3 +248,103 @@ def run_upgrade(
|
|
|
201
248
|
if installed_version() != version:
|
|
202
249
|
return UpgradeOutcome(STATUS_FAILED, version, "installed version did not change")
|
|
203
250
|
return UpgradeOutcome(STATUS_UPGRADED, version)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def console_script_locked(platform: str | None = None, launched: str | None = None) -> bool:
|
|
254
|
+
"""@brief Detect the Windows case where pip cannot replace the running launcher.
|
|
255
|
+
@param platform Optional platform name for deterministic tests.
|
|
256
|
+
@param launched Optional launched path for deterministic tests.
|
|
257
|
+
@return True when this process is its own environment's console script.
|
|
258
|
+
"""
|
|
259
|
+
if (platform or os.name) != "nt":
|
|
260
|
+
return False
|
|
261
|
+
executable = Path(launched if launched is not None else (sys.argv[0] or "")).resolve()
|
|
262
|
+
if executable.suffix.lower() != ".exe":
|
|
263
|
+
return False
|
|
264
|
+
return executable.parent == (Path(sys.prefix) / "Scripts").resolve()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def pending_path() -> Path:
|
|
268
|
+
"""@brief Resolve the marker that fences a background upgrade.
|
|
269
|
+
@return Absolute marker path.
|
|
270
|
+
@throws ValueError When the configured state directory is not absolute.
|
|
271
|
+
"""
|
|
272
|
+
return update_state_path().with_name(PENDING_NAME)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def upgrade_pending() -> bool:
|
|
276
|
+
"""@brief Tell whether a background upgrade is still expected to settle.
|
|
277
|
+
@return True while a fresh marker exists.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
path = pending_path()
|
|
281
|
+
return time.time() - path.stat().st_mtime < PENDING_TTL_S
|
|
282
|
+
except (OSError, ValueError):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def mark_pending(target: str) -> None:
|
|
287
|
+
"""@brief Publish the marker before a background helper is started.
|
|
288
|
+
@param target Version the helper is expected to install.
|
|
289
|
+
@throws OSError When the marker cannot be written.
|
|
290
|
+
"""
|
|
291
|
+
path = pending_path()
|
|
292
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
293
|
+
path.write_text(target, encoding="utf-8")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def clear_pending() -> None:
|
|
297
|
+
"""@brief Remove the marker once a background upgrade settled."""
|
|
298
|
+
with contextlib.suppress(OSError, ValueError):
|
|
299
|
+
pending_path().unlink(missing_ok=True)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def start_detached(version: str, prerelease: bool, source: Sequence[str] | None = None) -> bool:
|
|
303
|
+
"""@brief Start the helper outside this process so the launcher is released.
|
|
304
|
+
@param version Version the helper must install.
|
|
305
|
+
@param prerelease Whether pre-releases are allowed.
|
|
306
|
+
@param source Pip source arguments answered by the check.
|
|
307
|
+
@return True when the helper process was started.
|
|
308
|
+
"""
|
|
309
|
+
command = [sys.executable, "-m", "dexverse_cli.update.helper", version, "1" if prerelease else "0"]
|
|
310
|
+
command += list(source or ())
|
|
311
|
+
flags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
|
312
|
+
try:
|
|
313
|
+
subprocess.Popen(
|
|
314
|
+
command,
|
|
315
|
+
stdin=subprocess.DEVNULL,
|
|
316
|
+
stdout=subprocess.DEVNULL,
|
|
317
|
+
stderr=subprocess.DEVNULL,
|
|
318
|
+
creationflags=flags,
|
|
319
|
+
)
|
|
320
|
+
except OSError:
|
|
321
|
+
return False
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def retry_upgrade(
|
|
326
|
+
version: str,
|
|
327
|
+
prerelease: bool,
|
|
328
|
+
source: Sequence[str] | None = None,
|
|
329
|
+
runner: Runner | None = None,
|
|
330
|
+
attempts: int = RETRY_ATTEMPTS,
|
|
331
|
+
delay: float = RETRY_DELAY_S,
|
|
332
|
+
) -> UpgradeOutcome:
|
|
333
|
+
"""@brief Retry an upgrade that a still-exiting launcher blocked.
|
|
334
|
+
@param version Version to install.
|
|
335
|
+
@param prerelease Whether pre-releases are allowed.
|
|
336
|
+
@param source Pip source arguments answered by the check.
|
|
337
|
+
@param runner Optional process runner for deterministic tests.
|
|
338
|
+
@param attempts Maximum number of pip runs.
|
|
339
|
+
@param delay Seconds to wait between attempts.
|
|
340
|
+
@return The last outcome.
|
|
341
|
+
"""
|
|
342
|
+
outcome = run_upgrade(version, prerelease, runner=runner, source=source)
|
|
343
|
+
for _ in range(max(attempts - 1, 0)):
|
|
344
|
+
if outcome.status == STATUS_UPGRADED:
|
|
345
|
+
return outcome
|
|
346
|
+
if not any(reason in outcome.detail.lower() for reason in LOCK_REASONS):
|
|
347
|
+
return outcome
|
|
348
|
+
time.sleep(delay)
|
|
349
|
+
outcome = run_upgrade(version, prerelease, runner=runner, source=source)
|
|
350
|
+
return outcome
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.4"
|
|
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
|
|
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
|