boxd 0.2.6.dev355__py3-none-any.whl → 0.2.6.dev398__py3-none-any.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.
- boxd/_mappers.py +10 -2
- boxd/_transport.py +12 -1
- boxd/_version_check.py +37 -1
- boxd/models.py +5 -0
- boxd/resources/machines.py +8 -4
- {boxd-0.2.6.dev355.dist-info → boxd-0.2.6.dev398.dist-info}/METADATA +1 -1
- {boxd-0.2.6.dev355.dist-info → boxd-0.2.6.dev398.dist-info}/RECORD +10 -10
- {boxd-0.2.6.dev355.dist-info → boxd-0.2.6.dev398.dist-info}/WHEEL +0 -0
- {boxd-0.2.6.dev355.dist-info → boxd-0.2.6.dev398.dist-info}/licenses/LICENSE +0 -0
- {boxd-0.2.6.dev355.dist-info → boxd-0.2.6.dev398.dist-info}/top_level.txt +0 -0
boxd/_mappers.py
CHANGED
|
@@ -80,6 +80,11 @@ def _epoch_ms_always(millis: int) -> datetime:
|
|
|
80
80
|
# ── Machines ─────────────────────────────────────────────────────────
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def _desktop_url(name: str, console_origin: str) -> str:
|
|
84
|
+
"""`<console>/desktop/<name>`, or "" when the console origin is unknown."""
|
|
85
|
+
return f"{console_origin}/desktop/{name}" if console_origin else ""
|
|
86
|
+
|
|
87
|
+
|
|
83
88
|
def _machine_url(name: str, domain: str) -> str:
|
|
84
89
|
"""`https://<name>.<domain>`, or "" when the domain is not known yet."""
|
|
85
90
|
return f"https://{name}.{domain}" if domain else ""
|
|
@@ -99,7 +104,7 @@ def machine_source(msg) -> MachineSource | None:
|
|
|
99
104
|
)
|
|
100
105
|
|
|
101
106
|
|
|
102
|
-
def machine(msg) -> Machine:
|
|
107
|
+
def machine(msg, console_origin: str = "") -> Machine:
|
|
103
108
|
# `billing_org`/`billing_org_id` name the machine's org whether or not it
|
|
104
109
|
# is shared. `org`/`org_id` are the same org, blanked when it is private —
|
|
105
110
|
# so they answer "is it shared", and nothing else.
|
|
@@ -131,6 +136,7 @@ def machine(msg) -> Machine:
|
|
|
131
136
|
ssh_port=msg.ssh_port or None,
|
|
132
137
|
domain=msg.access_domain,
|
|
133
138
|
url=_machine_url(msg.name, msg.access_domain),
|
|
139
|
+
desktop_url=_desktop_url(msg.name, console_origin),
|
|
134
140
|
custom_domains=[custom_domain(d) for d in msg.custom_domains],
|
|
135
141
|
),
|
|
136
142
|
# 0 is MEANINGFUL here — disabled, not unset. Do not null it.
|
|
@@ -167,7 +173,9 @@ def machine_from_created(msg) -> Machine:
|
|
|
167
173
|
# Not reported by these responses — `create`/`fork` re-read the
|
|
168
174
|
# machine, so this fallback only shows when that read failed.
|
|
169
175
|
networking=MachineNetworking(networks=[], isolated=False),
|
|
170
|
-
access=MachineAccess(
|
|
176
|
+
access=MachineAccess(
|
|
177
|
+
ssh_port=None, domain="", url="", desktop_url="", custom_domains=[]
|
|
178
|
+
),
|
|
171
179
|
idle=MachineIdle(suspend_after=0, hibernate_after=0, destroy_after=0),
|
|
172
180
|
source=None,
|
|
173
181
|
hibernated_at=None,
|
boxd/_transport.py
CHANGED
|
@@ -17,7 +17,7 @@ import grpc.aio
|
|
|
17
17
|
from . import _version_check
|
|
18
18
|
from ._credentials import AsyncCredentials, SyncCredentials
|
|
19
19
|
from ._generated import api_pb2_grpc
|
|
20
|
-
from ._urls import DEFAULT_BASE_URL, resolve_endpoint
|
|
20
|
+
from ._urls import DEFAULT_BASE_URL, console_base_url, resolve_endpoint
|
|
21
21
|
from .errors import BoxdError, from_grpc_error, is_retryable
|
|
22
22
|
|
|
23
23
|
# First backoff, doubled per attempt. Two retries by default ⇒ 0.5s then 1.0s.
|
|
@@ -49,6 +49,17 @@ class _TransportBase:
|
|
|
49
49
|
or DEFAULT_BASE_URL
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
+
@property
|
|
53
|
+
def console_origin(self) -> str:
|
|
54
|
+
"""The web console's origin for whichever cluster this client talks to.
|
|
55
|
+
|
|
56
|
+
Derived from the same `base_url` the channel dials, so the two can
|
|
57
|
+
never disagree. Note it cannot come from a machine's own
|
|
58
|
+
`access.domain`: that may be the ORG's wildcard domain, and the
|
|
59
|
+
console is never under it.
|
|
60
|
+
"""
|
|
61
|
+
return console_base_url(self.base_url)
|
|
62
|
+
|
|
52
63
|
def _build_channel(self, factory_secure, factory_insecure):
|
|
53
64
|
endpoint = resolve_endpoint(self.base_url)
|
|
54
65
|
if endpoint.use_tls:
|
boxd/_version_check.py
CHANGED
|
@@ -11,6 +11,7 @@ produced; the comparison and the nag are shared.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import os
|
|
14
15
|
import re
|
|
15
16
|
import sys
|
|
16
17
|
from collections.abc import Iterable
|
|
@@ -18,6 +19,7 @@ from collections.abc import Iterable
|
|
|
18
19
|
REQ_HEADER = "x-boxd-py-sdk-version"
|
|
19
20
|
RESP_HEADER = "x-boxd-py-sdk-latest"
|
|
20
21
|
UPGRADE_CMD = "pip install --upgrade boxd"
|
|
22
|
+
PRERELEASE_UPGRADE_CMD = "pip install --upgrade --pre boxd"
|
|
21
23
|
|
|
22
24
|
_notified = False
|
|
23
25
|
|
|
@@ -40,6 +42,15 @@ def note_metadata(md: Iterable[tuple[str, str]] | None) -> None:
|
|
|
40
42
|
global _notified
|
|
41
43
|
if _notified or md is None:
|
|
42
44
|
return
|
|
45
|
+
# Only for a person at a terminal: in CI, or in an automation job whose
|
|
46
|
+
# stderr is its log file, this is noise on every run and nothing acts on
|
|
47
|
+
# it.
|
|
48
|
+
# An explicit opt-out beats every heuristic: set BOXD_NO_UPDATE_NOTICE to
|
|
49
|
+
# any non-empty value and this is never printed.
|
|
50
|
+
if os.environ.get("BOXD_NO_UPDATE_NOTICE"):
|
|
51
|
+
return
|
|
52
|
+
if not _stderr_is_tty():
|
|
53
|
+
return
|
|
43
54
|
latest = None
|
|
44
55
|
for key, value in md:
|
|
45
56
|
if key.lower() == RESP_HEADER:
|
|
@@ -50,13 +61,38 @@ def note_metadata(md: Iterable[tuple[str, str]] | None) -> None:
|
|
|
50
61
|
current = _current_version()
|
|
51
62
|
if _compare_semver(latest, current) <= 0:
|
|
52
63
|
return
|
|
64
|
+
# A pre-release is opt-in (`pip install --pre`), so someone on a release
|
|
65
|
+
# build is not behind on anything they asked for — and the command below
|
|
66
|
+
# would not install it anyway, leaving the nag to repeat forever. Staging
|
|
67
|
+
# advertises its own pre-releases, so this is the normal case there.
|
|
68
|
+
if _is_prerelease(latest) and not _is_prerelease(current):
|
|
69
|
+
return
|
|
53
70
|
_notified = True
|
|
54
71
|
print(file=sys.stderr)
|
|
55
72
|
print(
|
|
56
73
|
f" A new version of boxd is available (v{latest}, you have v{current}). Update with:",
|
|
57
74
|
file=sys.stderr,
|
|
58
75
|
)
|
|
59
|
-
|
|
76
|
+
cmd = PRERELEASE_UPGRADE_CMD if _is_prerelease(latest) else UPGRADE_CMD
|
|
77
|
+
print(f" {cmd}", file=sys.stderr)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _stderr_is_tty() -> bool:
|
|
81
|
+
"""Is stderr a terminal? Indirected through a function so tests can say.
|
|
82
|
+
|
|
83
|
+
(A capture fixture replaces ``sys.stderr`` wholesale, so patching the
|
|
84
|
+
stream's ``isatty`` is not something a test can rely on.)
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
return bool(sys.stderr.isatty())
|
|
88
|
+
except Exception: # closed or exotic stream: not a terminal
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_prerelease(v: str) -> bool:
|
|
93
|
+
"""Does this version carry a pre-release suffix ("0.2.6.dev356")?"""
|
|
94
|
+
m = _SEMVER_RE.match(v.strip())
|
|
95
|
+
return bool(m and m.group(4))
|
|
60
96
|
|
|
61
97
|
|
|
62
98
|
_SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(.*)$")
|
boxd/models.py
CHANGED
|
@@ -68,6 +68,11 @@ class MachineAccess(BaseModel):
|
|
|
68
68
|
"""The domain the machine is addressed under."""
|
|
69
69
|
url: str
|
|
70
70
|
"""`https://<name>.<domain>` — built for you, not sent by the server."""
|
|
71
|
+
desktop_url: str = ""
|
|
72
|
+
"""The machine's live Desktop view in the console —
|
|
73
|
+
`https://app.<zone>/desktop/<name>`. Built for you, like `url`. Empty when
|
|
74
|
+
the client has no endpoint to derive the console origin from. Opening it
|
|
75
|
+
needs a signed-in browser; the machine is woken if it is asleep."""
|
|
71
76
|
custom_domains: list[CustomDomain]
|
|
72
77
|
"""Per-VM custom domains bound to this machine (BOX-30), e.g.
|
|
73
78
|
`app.example.com`. Distinct from `domain` above, which is the org's
|
boxd/resources/machines.py
CHANGED
|
@@ -259,7 +259,8 @@ class Machines(Namespace):
|
|
|
259
259
|
def get(self, machine_id: str) -> Machine:
|
|
260
260
|
"""Fetch one machine by id or name."""
|
|
261
261
|
return m.machine(
|
|
262
|
-
self._transport.call("GetVm", api_pb2.GetVmRequest(vm_id=machine_id))
|
|
262
|
+
self._transport.call("GetVm", api_pb2.GetVmRequest(vm_id=machine_id)),
|
|
263
|
+
self._transport.console_origin,
|
|
263
264
|
)
|
|
264
265
|
|
|
265
266
|
def list(self, *, org: str | None = None, all_contexts: bool = False) -> list[Machine]:
|
|
@@ -271,7 +272,8 @@ class Machines(Namespace):
|
|
|
271
272
|
resp = self._transport.call(
|
|
272
273
|
"ListVms", req.list_vms(org=org, all_contexts=all_contexts)
|
|
273
274
|
)
|
|
274
|
-
|
|
275
|
+
console_origin = self._transport.console_origin
|
|
276
|
+
return [m.machine(v, console_origin) for v in resp.vms]
|
|
275
277
|
|
|
276
278
|
def delete(self, machine_id: str) -> None:
|
|
277
279
|
"""Destroy a machine. Terminal."""
|
|
@@ -828,7 +830,8 @@ class AsyncMachines(Namespace):
|
|
|
828
830
|
async def get(self, machine_id: str) -> Machine:
|
|
829
831
|
"""Fetch one machine by id or name."""
|
|
830
832
|
return m.machine(
|
|
831
|
-
await self._transport.call("GetVm", api_pb2.GetVmRequest(vm_id=machine_id))
|
|
833
|
+
await self._transport.call("GetVm", api_pb2.GetVmRequest(vm_id=machine_id)),
|
|
834
|
+
self._transport.console_origin,
|
|
832
835
|
)
|
|
833
836
|
|
|
834
837
|
async def list(
|
|
@@ -838,7 +841,8 @@ class AsyncMachines(Namespace):
|
|
|
838
841
|
resp = await self._transport.call(
|
|
839
842
|
"ListVms", req.list_vms(org=org, all_contexts=all_contexts)
|
|
840
843
|
)
|
|
841
|
-
|
|
844
|
+
console_origin = self._transport.console_origin
|
|
845
|
+
return [m.machine(v, console_origin) for v in resp.vms]
|
|
842
846
|
|
|
843
847
|
async def delete(self, machine_id: str) -> None:
|
|
844
848
|
"""Destroy a machine. Terminal."""
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
boxd/__init__.py,sha256=4fGbOYgQ_32_GQCLf7Nv4CTWfuSvdomx5tr57QA-zys,2868
|
|
2
2
|
boxd/_client.py,sha256=oqJ_XtuzmmKW1jgzQ57na-ndT7PzAd8-QAxoYNbLpY0,5161
|
|
3
3
|
boxd/_credentials.py,sha256=Z7sjbl9Y_tcKBLk6PA5YOSNews8GrYr391phFpnrW3E,11492
|
|
4
|
-
boxd/_mappers.py,sha256=
|
|
4
|
+
boxd/_mappers.py,sha256=kHnqVWUyixHS-XsEkNALlvXhGqp1RL-pOicfrLntv5U,12936
|
|
5
5
|
boxd/_requests.py,sha256=s3d0IKxJjyX0klJwNaufYRWt4lwlQdxQgjAD6aNL6q4,9734
|
|
6
6
|
boxd/_streaming.py,sha256=MEhPUhgdnwGnk0H8xUKx8kx2z-3Mcmhx4iS-yO-ifvI,6222
|
|
7
|
-
boxd/_transport.py,sha256=
|
|
7
|
+
boxd/_transport.py,sha256=pU9pY3V6UBj8prnO5suo7g32saadhL-EiffXi3Agtjs,8750
|
|
8
8
|
boxd/_urls.py,sha256=pVwXTQGBmSieY-wwvXx5l6Be5lIrBcnjOdCCMMwW1Hs,3654
|
|
9
|
-
boxd/_version_check.py,sha256=
|
|
9
|
+
boxd/_version_check.py,sha256=XJD_BY1CH5xOn2ArrbO2ZQXI6xf41o-zwQIReohF8sg,5623
|
|
10
10
|
boxd/errors.py,sha256=Vapb1US78lxMvnzqsQ6oyHl2r_rs417Hl8YucT5Lme8,3435
|
|
11
|
-
boxd/models.py,sha256=
|
|
11
|
+
boxd/models.py,sha256=Mi0zRgJL3nkkeUiSU7dtT7ggc9NJi_dTHL3XBAcNxF4,14272
|
|
12
12
|
boxd/_generated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
boxd/_generated/api_pb2.py,sha256=iSLbWoV1wOuF0fv-ylNl3cRnnCdubqyfYuh3N3cmAAY,55350
|
|
14
14
|
boxd/_generated/api_pb2_grpc.py,sha256=9k6x_bxyEi7S3SLlGXiFdZwsRJIOSwsv8NCgvI0XoSc,148445
|
|
@@ -17,12 +17,12 @@ boxd/resources/account.py,sha256=9I9iHHjmEOxvrJSP773uKVMHORgPvaZBKCWkhuaNNSo,286
|
|
|
17
17
|
boxd/resources/credentials.py,sha256=sFl2jFnyu7D_a-vZMfBWfnMGu31VzO-h2-JkFe0DEmA,3365
|
|
18
18
|
boxd/resources/disks.py,sha256=lqlAJxt1mMvmsQCfBabsTtDMAbPkfz3xD9W_aVvCLqg,3785
|
|
19
19
|
boxd/resources/domains.py,sha256=e5cY6f4iTVny2m_BRVrrKf4KR452i9FAdS64_RvzhIk,2967
|
|
20
|
-
boxd/resources/machines.py,sha256=
|
|
20
|
+
boxd/resources/machines.py,sha256=MDyw5HphuI7YNFKPbTkjG0QFWWnQxq7F2RloM8-59xg,47980
|
|
21
21
|
boxd/resources/orgs.py,sha256=KMG5MDZiZWbFgtwBR_F6tpwr1Kzjvnr6S5HOMPndd_0,7702
|
|
22
22
|
boxd/resources/snapshots.py,sha256=xzVvBqcjimeor0pcWMyrGePxBJ-aV8NPbKYuIvu6epU,3181
|
|
23
23
|
boxd/resources/vars.py,sha256=SuiK6h15Ols9TLA28WIRP_a86PfAnoDIDsi2JH1ZVVs,8301
|
|
24
|
-
boxd-0.2.6.
|
|
25
|
-
boxd-0.2.6.
|
|
26
|
-
boxd-0.2.6.
|
|
27
|
-
boxd-0.2.6.
|
|
28
|
-
boxd-0.2.6.
|
|
24
|
+
boxd-0.2.6.dev398.dist-info/licenses/LICENSE,sha256=rD8_32iKSf_QarUkwB8tTujEORtC3uYs4KIggm6cFKg,1061
|
|
25
|
+
boxd-0.2.6.dev398.dist-info/METADATA,sha256=_019etJ5MicQQwdEoCTAdYdU4BxhX4SjnjT1nfztMBI,26582
|
|
26
|
+
boxd-0.2.6.dev398.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
27
|
+
boxd-0.2.6.dev398.dist-info/top_level.txt,sha256=lJFbtvnDUx23dMMfX5hqRWPjtUhgrLpNJ9qdCPwON30,5
|
|
28
|
+
boxd-0.2.6.dev398.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|