secflow-cli 0.2.5__tar.gz → 0.3.0__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.
Files changed (47) hide show
  1. {secflow_cli-0.2.5/src/secflow_cli.egg-info → secflow_cli-0.3.0}/PKG-INFO +11 -10
  2. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/USAGE.md +9 -9
  3. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/pyproject.toml +3 -2
  4. secflow_cli-0.3.0/src/secflow_cli/__init__.py +1 -0
  5. secflow_cli-0.3.0/src/secflow_cli/__main__.py +3 -0
  6. secflow_cli-0.3.0/src/secflow_cli/bootstrap.py +37 -0
  7. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/main.py +3 -2
  8. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/output.py +4 -0
  9. secflow_cli-0.3.0/src/secflow_cli/update_install.py +322 -0
  10. secflow_cli-0.3.0/src/secflow_cli/update_runtime.py +511 -0
  11. secflow_cli-0.3.0/src/secflow_cli/update_security.py +170 -0
  12. secflow_cli-0.3.0/src/secflow_cli/update_state.py +195 -0
  13. {secflow_cli-0.2.5 → secflow_cli-0.3.0/src/secflow_cli.egg-info}/PKG-INFO +11 -10
  14. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/SOURCES.txt +5 -0
  15. secflow_cli-0.3.0/src/secflow_cli.egg-info/entry_points.txt +2 -0
  16. secflow_cli-0.2.5/src/secflow_cli/__init__.py +0 -0
  17. secflow_cli-0.2.5/src/secflow_cli/__main__.py +0 -3
  18. secflow_cli-0.2.5/src/secflow_cli.egg-info/entry_points.txt +0 -2
  19. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/LICENSE +0 -0
  20. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/setup.cfg +0 -0
  21. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/access_profile_commands.py +0 -0
  22. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/account_commands.py +0 -0
  23. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/browser_login.py +0 -0
  24. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/command_support.py +0 -0
  25. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/completion.py +0 -0
  26. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/config.py +0 -0
  27. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/credentials.py +0 -0
  28. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/doctor_commands.py +0 -0
  29. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/errors.py +0 -0
  30. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/help.py +0 -0
  31. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/logkit/__init__.py +0 -0
  32. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/logkit/test_logkit.py +0 -0
  33. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/manifest_commands.py +0 -0
  34. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/namespace_commands.py +0 -0
  35. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/operator_commands.py +0 -0
  36. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/paging.py +0 -0
  37. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/policy_audit_commands.py +0 -0
  38. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/reference_commands.py +0 -0
  39. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/report_commands.py +0 -0
  40. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/role_commands.py +0 -0
  41. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/runtime_commands.py +0 -0
  42. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/secret_commands.py +0 -0
  43. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/session_commands.py +0 -0
  44. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli/token_commands.py +0 -0
  45. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/dependency_links.txt +0 -0
  46. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/requires.txt +0 -0
  47. {secflow_cli-0.2.5 → secflow_cli-0.3.0}/src/secflow_cli.egg-info/top_level.txt +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secflow-cli
3
- Version: 0.2.5
3
+ Version: 0.3.0
4
4
  Summary: SecFlow operator CLI
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/yeon3724/secflow
7
7
  Project-URL: Issues, https://github.com/yeon3724/secflow/issues
8
+ Keywords: intflow-cli-update-protocol-1
8
9
  Requires-Python: >=3.10
9
10
  Description-Content-Type: text/markdown
10
11
  License-File: LICENSE
@@ -14,11 +15,11 @@ Dynamic: license-file
14
15
 
15
16
  # SecFlow CLI
16
17
 
17
- Use Intflow browser login or an existing Admin session token. Both distributions provide the same `secflowctl` commands.
18
+ Use Intflow browser login or an Admin session token.
18
19
 
19
20
  ## Install and update
20
21
 
21
- Choose one distribution per machine; both own the same `secflowctl` command and share profiles and the OS credential store.
22
+ Choose npm or Python; both own `secflowctl` and share profiles and OS credentials.
22
23
 
23
24
  ```sh
24
25
  pipx install secflow-cli
@@ -30,19 +31,19 @@ npm install --global @intflow/secflow-cli
30
31
  secflowctl --version
31
32
  ```
32
33
 
33
- The Python distribution requires Python 3.10+. Installation automatically includes the matching Admin-capable `secflow-sdk` dependency.
34
+ Python 3.10+ is required; installation includes the pinned Admin-capable SDK.
34
35
 
35
- The npm distribution includes Python and the CLI dependencies in OS-specific native packages. Targets: Windows x64, macOS Intel/Apple Silicon, and Linux x64 with glibc 2.35+ (Ubuntu 22.04+). Alpine/musl and Linux ARM are not npm targets; use the Python distribution there. Keep npm optional dependencies enabled. Installation has no postinstall script or runtime download. Native binaries are not platform-signed or notarized.
36
+ The npm distribution bundles Python and dependencies for Windows x64, macOS Intel/Apple Silicon, and Linux x64 glibc 2.35+. Alpine/musl and Linux ARM use the Python distribution. Keep optional dependencies enabled; installation has no postinstall script. Releases require all four same-version native packages. Binaries are not platform-signed or notarized.
36
37
 
37
- The bundled interpreter includes CA certificates for HTTPS. An explicit `SSL_CERT_FILE` or `SSL_CERT_DIR` takes precedence for organization-managed trust; TLS certificate verification stays enabled.
38
+ The bundle includes HTTPS CAs; `SSL_CERT_FILE`/`SSL_CERT_DIR` overrides trust without disabling TLS verification.
38
39
 
39
40
  ```sh
40
- pipx upgrade secflow-cli
41
- # Or use the same installer as the initial installation:
42
- python -m pip install --upgrade secflow-cli
43
- npm install --global @intflow/secflow-cli@latest
41
+ secflowctl update --check
42
+ secflowctl update --status
44
43
  ```
45
44
 
45
+ Commands check within 3s and stage protocol-1 stable runtimes in a private cache/venv, then run argv once; offline failures keep the last good runtime. TTY/JSON agree; help/version/check/status never install. `update --apply` installs; `--auto on|off` persists policy; `--pin VERSION` freezes an exact runtime; `--unpin` releases only policy. `INTFLOW_CLI_AUTO_UPDATE=0` stops automatic installs, allowing explicit apply/pin. Pins outrank off and fail closed. Internal native binaries update through npm.
46
+
46
47
  ## Connect
47
48
 
48
49
  ```sh
@@ -1,10 +1,10 @@
1
1
  # SecFlow CLI
2
2
 
3
- Use Intflow browser login or an existing Admin session token. Both distributions provide the same `secflowctl` commands.
3
+ Use Intflow browser login or an Admin session token.
4
4
 
5
5
  ## Install and update
6
6
 
7
- Choose one distribution per machine; both own the same `secflowctl` command and share profiles and the OS credential store.
7
+ Choose npm or Python; both own `secflowctl` and share profiles and OS credentials.
8
8
 
9
9
  ```sh
10
10
  pipx install secflow-cli
@@ -16,19 +16,19 @@ npm install --global @intflow/secflow-cli
16
16
  secflowctl --version
17
17
  ```
18
18
 
19
- The Python distribution requires Python 3.10+. Installation automatically includes the matching Admin-capable `secflow-sdk` dependency.
19
+ Python 3.10+ is required; installation includes the pinned Admin-capable SDK.
20
20
 
21
- The npm distribution includes Python and the CLI dependencies in OS-specific native packages. Targets: Windows x64, macOS Intel/Apple Silicon, and Linux x64 with glibc 2.35+ (Ubuntu 22.04+). Alpine/musl and Linux ARM are not npm targets; use the Python distribution there. Keep npm optional dependencies enabled. Installation has no postinstall script or runtime download. Native binaries are not platform-signed or notarized.
21
+ The npm distribution bundles Python and dependencies for Windows x64, macOS Intel/Apple Silicon, and Linux x64 glibc 2.35+. Alpine/musl and Linux ARM use the Python distribution. Keep optional dependencies enabled; installation has no postinstall script. Releases require all four same-version native packages. Binaries are not platform-signed or notarized.
22
22
 
23
- The bundled interpreter includes CA certificates for HTTPS. An explicit `SSL_CERT_FILE` or `SSL_CERT_DIR` takes precedence for organization-managed trust; TLS certificate verification stays enabled.
23
+ The bundle includes HTTPS CAs; `SSL_CERT_FILE`/`SSL_CERT_DIR` overrides trust without disabling TLS verification.
24
24
 
25
25
  ```sh
26
- pipx upgrade secflow-cli
27
- # Or use the same installer as the initial installation:
28
- python -m pip install --upgrade secflow-cli
29
- npm install --global @intflow/secflow-cli@latest
26
+ secflowctl update --check
27
+ secflowctl update --status
30
28
  ```
31
29
 
30
+ Commands check within 3s and stage protocol-1 stable runtimes in a private cache/venv, then run argv once; offline failures keep the last good runtime. TTY/JSON agree; help/version/check/status never install. `update --apply` installs; `--auto on|off` persists policy; `--pin VERSION` freezes an exact runtime; `--unpin` releases only policy. `INTFLOW_CLI_AUTO_UPDATE=0` stops automatic installs, allowing explicit apply/pin. Pins outrank off and fail closed. Internal native binaries update through npm.
31
+
32
32
  ## Connect
33
33
 
34
34
  ```sh
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "secflow-cli"
7
- version = "0.2.5"
7
+ version = "0.3.0"
8
8
  description = "SecFlow operator CLI"
9
9
  readme = "USAGE.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
12
+ keywords = ["intflow-cli-update-protocol-1"]
12
13
  dependencies = ["secflow-sdk==0.1.10", "keyring>=25,<26"]
13
14
 
14
15
  [project.scripts]
15
- secflowctl = "secflow_cli.main:main"
16
+ secflowctl = "secflow_cli.bootstrap:main"
16
17
 
17
18
  [project.urls]
18
19
  Repository = "https://github.com/yeon3724/secflow"
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,3 @@
1
+ from secflow_cli.bootstrap import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Any
5
+
6
+ from . import __version__
7
+ from .update_runtime import bootstrap
8
+ from .update_state import Configuration, default_root
9
+
10
+
11
+ def execute(arguments: list[str]) -> int:
12
+ from .main import main
13
+
14
+ return main(arguments, configure=True)
15
+
16
+
17
+ def output(value: dict[str, Any]) -> None:
18
+ from .output import update_result
19
+
20
+ update_result(value)
21
+
22
+
23
+ def main(argv: list[str] | None = None) -> int:
24
+ config = Configuration(
25
+ "secflow-cli", "secflow_cli", __version__, default_root("secflow-cli")
26
+ )
27
+ return bootstrap(
28
+ config,
29
+ list(sys.argv[1:] if argv is None else argv),
30
+ execute,
31
+ output,
32
+ frozen=bool(getattr(sys, "frozen", False)),
33
+ )
34
+
35
+
36
+ if __name__ == "__main__":
37
+ raise SystemExit(main())
@@ -53,6 +53,7 @@ class VersionAction(argparse.Action):
53
53
 
54
54
  def parser():
55
55
  result = Parser(prog="secflowctl", description="SecFlow operator CLI (independent of the service CLI).")
56
+ result.epilog = "CLI updates: update [--check|--apply|--status|--auto on|off|--pin VERSION|--unpin]."
56
57
  result.add_argument("--version", action=VersionAction, nargs=0, help="Show the CLI version and exit.")
57
58
  result.add_argument("--profile")
58
59
  result.add_argument("--base-url")
@@ -496,8 +497,8 @@ def dispatch(arguments, run):
496
497
  return run_configured(args, run, quick_login)
497
498
 
498
499
 
499
- def main(argv=None):
500
- if argv is None:
500
+ def main(argv=None, *, configure=False):
501
+ if argv is None or configure:
501
502
  logkit.configure("secflowctl", kind="cli")
502
503
  run = Invocation("table" if sys.stdout.isatty() else "json")
503
504
  try:
@@ -14,6 +14,10 @@ TOKEN_FIELDS = ("token_id", "account_id", "scope_type", "scope", "issued_at", "e
14
14
  ACCOUNT_ROLES = ("org_admin", "namespace_admin", "secret_writer", "secret_reader")
15
15
 
16
16
 
17
+ def update_result(value):
18
+ print(json.dumps(value, ensure_ascii=True, separators=(",", ":")))
19
+
20
+
17
21
  def safe_fields(value, fields):
18
22
  if not isinstance(value, Mapping):
19
23
  raise CliError(7, "INVALID_RESPONSE", "The server returned an invalid metadata response.")
@@ -0,0 +1,322 @@
1
+ from __future__ import annotations
2
+
3
+ import http.client
4
+ import json
5
+ import os
6
+ import queue
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ import urllib.parse
13
+ import uuid
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from .update_security import directory, windows_tree
18
+ from .update_state import (
19
+ CHILD,
20
+ MARKER,
21
+ PROTOCOL,
22
+ Configuration,
23
+ UpdateError,
24
+ checked,
25
+ digest,
26
+ executable,
27
+ private,
28
+ read,
29
+ release_path,
30
+ remove_release,
31
+ save,
32
+ version,
33
+ )
34
+
35
+
36
+ def compatible(specification: Any) -> bool:
37
+ if not isinstance(specification, str) or not specification:
38
+ return False
39
+ current = sys.version_info[:3]
40
+ operations = {
41
+ ">=": lambda wanted: current >= wanted,
42
+ ">": lambda wanted: current > wanted,
43
+ "<=": lambda wanted: current <= wanted,
44
+ "<": lambda wanted: current < wanted,
45
+ "==": lambda wanted: current == wanted,
46
+ "!=": lambda wanted: current != wanted,
47
+ }
48
+ for term in specification.split(","):
49
+ match = re.fullmatch(
50
+ r"\s*(>=|<=|==|!=|>|<)\s*(\d+)\.(\d+)(?:\.(\d+))?\s*", term
51
+ )
52
+ if match is None:
53
+ return False
54
+ wanted = tuple(int(value or 0) for value in match.groups()[1:])
55
+ if not operations[match[1]](wanted):
56
+ return False
57
+ return True
58
+
59
+
60
+ def registry_bytes(url: str) -> bytes:
61
+ target = urllib.parse.urlsplit(url)
62
+ if (
63
+ target.scheme != "https"
64
+ or target.netloc != "pypi.org"
65
+ or target.query
66
+ or target.fragment
67
+ ):
68
+ raise UpdateError("INVALID_REGISTRY_RESPONSE")
69
+ connection = http.client.HTTPSConnection("pypi.org", timeout=3)
70
+ try:
71
+ connection.request("GET", target.path, headers={"Accept": "application/json"})
72
+ response = connection.getresponse()
73
+ if response.status != 200:
74
+ raise UpdateError("REGISTRY_UNAVAILABLE")
75
+ content = response.read(1048577)
76
+ finally:
77
+ connection.close()
78
+ if len(content) > 1048576:
79
+ raise UpdateError("INVALID_REGISTRY_RESPONSE")
80
+ return content
81
+
82
+
83
+ def registry_document(url: str) -> dict[str, Any]:
84
+ result: queue.Queue[bytes | Exception] = queue.Queue(maxsize=1)
85
+
86
+ def fetch() -> None:
87
+ try:
88
+ result.put(registry_bytes(url))
89
+ except (OSError, ValueError, http.client.HTTPException, UpdateError) as error:
90
+ result.put(error)
91
+
92
+ threading.Thread(target=fetch, daemon=True).start()
93
+ try:
94
+ content = result.get(timeout=3)
95
+ except queue.Empty:
96
+ raise UpdateError("REGISTRY_TIMEOUT") from None
97
+ if isinstance(content, Exception):
98
+ raise UpdateError("REGISTRY_UNAVAILABLE") from None
99
+ value = json.loads(content)
100
+ if not isinstance(value, dict) or not isinstance(value.get("info"), dict):
101
+ raise UpdateError("INVALID_REGISTRY_RESPONSE")
102
+ return value
103
+
104
+
105
+ def metadata(config: Configuration, target: str | None = None) -> dict[str, Any]:
106
+ if target is not None:
107
+ version(target)
108
+ suffix = "/" + target if target else ""
109
+ url = (
110
+ "https://pypi.org/pypi/"
111
+ + urllib.parse.quote(config.package, safe="")
112
+ + suffix
113
+ + "/json"
114
+ )
115
+ value = registry_document(url)
116
+ info = value.get("info", {})
117
+ actual = info.get("version")
118
+ version(actual)
119
+ normalized = re.sub(r"[-_.]+", "-", str(info.get("name", ""))).lower()
120
+ if normalized != config.package or (target is not None and actual != target):
121
+ raise UpdateError("INVALID_REGISTRY_RESPONSE")
122
+ keywords = info.get("keywords")
123
+ if not isinstance(keywords, str) or MARKER not in re.split(r"[\s,]+", keywords):
124
+ raise UpdateError("UNSUPPORTED_PROTOCOL", actual)
125
+ if not compatible(info.get("requires_python")):
126
+ raise UpdateError("INCOMPATIBLE_RUNTIME", actual)
127
+ wheels = [
128
+ row
129
+ for row in value.get("urls", [])
130
+ if isinstance(row, dict)
131
+ and row.get("packagetype") == "bdist_wheel"
132
+ and row.get("yanked") is False
133
+ ]
134
+ if not wheels:
135
+ raise UpdateError("WHEEL_UNAVAILABLE", actual)
136
+ return {"version": actual, "package": config.package, "protocol": PROTOCOL}
137
+
138
+
139
+ def installer_environment() -> dict[str, str]:
140
+ allowed = {
141
+ "PATH",
142
+ "PATHEXT",
143
+ "SYSTEMROOT",
144
+ "WINDIR",
145
+ "COMSPEC",
146
+ "HOME",
147
+ "USERPROFILE",
148
+ "LOCALAPPDATA",
149
+ "APPDATA",
150
+ "TEMP",
151
+ "TMP",
152
+ "TMPDIR",
153
+ "LANG",
154
+ "LC_ALL",
155
+ }
156
+ return {
157
+ **{key: value for key, value in os.environ.items() if key.upper() in allowed},
158
+ CHILD: "1",
159
+ "INTFLOW_CLI_AUTO_UPDATE": "0",
160
+ "PYTHONUTF8": "1",
161
+ "PYTHONDONTWRITEBYTECODE": "1",
162
+ "PIP_CONFIG_FILE": os.devnull,
163
+ "NETRC": os.devnull,
164
+ "PIP_KEYRING_PROVIDER": "disabled",
165
+ }
166
+
167
+
168
+ def execute(
169
+ command: list[str], folder: Path, deadline: float, capture: bool = False
170
+ ) -> str:
171
+ remaining = deadline - time.monotonic()
172
+ if remaining <= 0:
173
+ raise UpdateError("INSTALL_TIMEOUT")
174
+ result = subprocess.run(
175
+ command,
176
+ cwd=folder,
177
+ stdin=subprocess.DEVNULL,
178
+ stdout=subprocess.PIPE if capture else subprocess.DEVNULL,
179
+ stderr=subprocess.DEVNULL,
180
+ env=installer_environment(),
181
+ timeout=remaining,
182
+ check=False,
183
+ )
184
+ if result.returncode != 0:
185
+ raise UpdateError("VALIDATION_FAILED" if capture else "INSTALL_FAILED")
186
+ if capture:
187
+ if len(result.stdout) > 4096:
188
+ raise UpdateError("INVALID_PROBE")
189
+ return result.stdout.decode("utf-8").strip()
190
+ return ""
191
+
192
+
193
+ def site_packages(release: Path) -> Path:
194
+ return release / (
195
+ "Lib/site-packages"
196
+ if os.name == "nt"
197
+ else f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages"
198
+ )
199
+
200
+
201
+ def seal_files(release: Path) -> dict[str, str]:
202
+ files = [executable(release), release / "pyvenv.cfg"]
203
+ site = checked(site_packages(release))
204
+ for path in site.rglob("*"):
205
+ checked(path)
206
+ if path.is_file():
207
+ files.append(path)
208
+ if len(files) > 8192 or sum(path.stat().st_size for path in files) > 134217728:
209
+ raise UpdateError("RELEASE_TOO_LARGE")
210
+ return {path.relative_to(release).as_posix(): digest(path) for path in files}
211
+
212
+
213
+ def validate(config: Configuration, pointer: dict[str, Any]) -> Path:
214
+ directory(config.root)
215
+ version(pointer.get("version"))
216
+ release = release_path(config.root, pointer.get("release"))
217
+ directory(release)
218
+ windows_tree(release)
219
+ seal = read(release, "release.json", 1048576)
220
+ if (
221
+ seal.get("package") != config.package
222
+ or seal.get("version") != pointer["version"]
223
+ or seal.get("protocol") != PROTOCOL
224
+ ):
225
+ raise UpdateError("INVALID_RELEASE")
226
+ if seal.get("files") != seal_files(release):
227
+ raise UpdateError("RELEASE_TAMPERED")
228
+ probe(config, release, pointer["version"], time.monotonic() + 10)
229
+ return executable(release)
230
+
231
+
232
+ def probe(config: Configuration, release: Path, target: str, deadline: float) -> None:
233
+ python = str(executable(release))
234
+ query = (
235
+ "import importlib.metadata,json,sys; m=importlib.metadata.metadata(sys.argv[1]);"
236
+ "print(json.dumps({k:m.get(k) for k in "
237
+ "['Name','Version','Requires-Python','Keywords']}))"
238
+ )
239
+ installed = json.loads(
240
+ execute(
241
+ [python, "-I", "-B", "-c", query, config.package], release, deadline, True
242
+ )
243
+ )
244
+ normalized = re.sub(r"[-_.]+", "-", str(installed.get("Name", ""))).lower()
245
+ if normalized != config.package or installed.get("Version") != target:
246
+ raise UpdateError("VERSION_MISMATCH")
247
+ if MARKER not in re.split(
248
+ r"[\s,]+", str(installed.get("Keywords", ""))
249
+ ) or not compatible(installed.get("Requires-Python")):
250
+ raise UpdateError("UNSUPPORTED_PROTOCOL")
251
+ payload = execute(
252
+ [python, "-I", "-B", "-m", config.module + ".bootstrap", "--runtime-probe"],
253
+ release,
254
+ deadline,
255
+ True,
256
+ )
257
+ if json.loads(payload) != {
258
+ "protocol": PROTOCOL,
259
+ "package": config.package,
260
+ "version": target,
261
+ "runtime": "python",
262
+ }:
263
+ raise UpdateError("INVALID_PROBE")
264
+ displayed = execute(
265
+ [python, "-I", "-B", "-m", config.module + ".bootstrap", "--version"],
266
+ release,
267
+ deadline,
268
+ True,
269
+ )
270
+ if target not in displayed.split():
271
+ raise UpdateError("VERSION_MISMATCH")
272
+
273
+
274
+ def install(config: Configuration, target: str) -> dict[str, Any]:
275
+ version(target)
276
+ private(config.root)
277
+ release = config.root / ("release-" + str(uuid.uuid4()))
278
+ private(release)
279
+ deadline = time.monotonic() + 180
280
+ complete = False
281
+ try:
282
+ execute(
283
+ [sys.executable, "-I", "-m", "venv", "--copies", str(release)],
284
+ release,
285
+ deadline,
286
+ )
287
+ execute(
288
+ [
289
+ str(executable(release)),
290
+ "-I",
291
+ "-m",
292
+ "pip",
293
+ "install",
294
+ "--disable-pip-version-check",
295
+ "--no-input",
296
+ "--no-cache-dir",
297
+ "--no-compile",
298
+ "--index-url",
299
+ "https://pypi.org/simple",
300
+ "--only-binary",
301
+ ":all:",
302
+ config.package + "==" + target,
303
+ ],
304
+ release,
305
+ deadline,
306
+ )
307
+ probe(config, release, target, deadline)
308
+ save(
309
+ release,
310
+ "release.json",
311
+ {
312
+ "package": config.package,
313
+ "version": target,
314
+ "protocol": PROTOCOL,
315
+ "files": seal_files(release),
316
+ },
317
+ )
318
+ complete = True
319
+ return {"version": target, "release": release.name}
320
+ finally:
321
+ if not complete:
322
+ remove_release(config.root, release)