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 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(ssh_port=None, domain="", url="", custom_domains=[]),
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
- print(f" {UPGRADE_CMD}", file=sys.stderr)
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
@@ -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
- return [m.machine(v) for v in resp.vms]
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
- return [m.machine(v) for v in resp.vms]
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boxd
3
- Version: 0.2.6.dev355
3
+ Version: 0.2.6.dev398
4
4
  Summary: Python SDK for the boxd cloud VM platform
5
5
  Author: Azin
6
6
  License-Expression: MIT
@@ -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=8VqmwdNM1RqqGbn-1Ub1frKwg3hX9OBCKf3r69S2JBQ,12597
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=tWzZuKDmN4sFtJZvCitvkWtv1Vc_rTvPlmWGHMT3DMU,8293
7
+ boxd/_transport.py,sha256=pU9pY3V6UBj8prnO5suo7g32saadhL-EiffXi3Agtjs,8750
8
8
  boxd/_urls.py,sha256=pVwXTQGBmSieY-wwvXx5l6Be5lIrBcnjOdCCMMwW1Hs,3654
9
- boxd/_version_check.py,sha256=nhD6FazxaqGRjOPeAER3U0PBCEZPwNDuJjS3s1Ycpwo,4128
9
+ boxd/_version_check.py,sha256=XJD_BY1CH5xOn2ArrbO2ZQXI6xf41o-zwQIReohF8sg,5623
10
10
  boxd/errors.py,sha256=Vapb1US78lxMvnzqsQ6oyHl2r_rs417Hl8YucT5Lme8,3435
11
- boxd/models.py,sha256=wKehB8m1ydLt3jxSfHUg_u2Sbx8Xk8ZicN-NBCjZ4Yk,13960
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=NzqpjL22tVM95QMe-N-oDzH6lO1OoF0l6h1NLm8AzII,47746
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.dev355.dist-info/licenses/LICENSE,sha256=rD8_32iKSf_QarUkwB8tTujEORtC3uYs4KIggm6cFKg,1061
25
- boxd-0.2.6.dev355.dist-info/METADATA,sha256=TVmFv9lC1_ijqYLX0JMxk0wa6FRsmCHogZY0PuHEbZY,26582
26
- boxd-0.2.6.dev355.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
- boxd-0.2.6.dev355.dist-info/top_level.txt,sha256=lJFbtvnDUx23dMMfX5hqRWPjtUhgrLpNJ9qdCPwON30,5
28
- boxd-0.2.6.dev355.dist-info/RECORD,,
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,,