boxd 0.2.6.dev355__tar.gz → 0.2.6.dev399__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.
- {boxd-0.2.6.dev355/src/boxd.egg-info → boxd-0.2.6.dev399}/PKG-INFO +2 -2
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/README.md +1 -1
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/pyproject.toml +1 -1
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_client.py +1 -1
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_mappers.py +10 -2
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_transport.py +12 -1
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_urls.py +4 -4
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_version_check.py +37 -1
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/models.py +5 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/machines.py +8 -4
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399/src/boxd.egg-info}/PKG-INFO +2 -2
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_mappers.py +12 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_namespaces.py +1 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_transport.py +1 -1
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_version_check.py +47 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/LICENSE +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/setup.cfg +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/__init__.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_credentials.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_requests.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/errors.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/domains.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd/resources/vars.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd.egg-info/SOURCES.txt +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_credentials.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_requests.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_streaming.py +0 -0
- {boxd-0.2.6.dev355 → boxd-0.2.6.dev399}/tests/test_urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.2.6.
|
|
3
|
+
Version: 0.2.6.dev399
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -77,7 +77,7 @@ Every argument is keyword-only.
|
|
|
77
77
|
|---|---|---|
|
|
78
78
|
| `api_key` | `BOXD_API_KEY` | — |
|
|
79
79
|
| `token` | `BOXD_TOKEN` | — |
|
|
80
|
-
| `base_url` | `BOXD_BASE_URL` (or the deprecated `BOXD_API_URL`) | `
|
|
80
|
+
| `base_url` | `BOXD_BASE_URL` (or the deprecated `BOXD_API_URL`) | `https://boxd.sh:9443` |
|
|
81
81
|
| `timeout` | — | 60.0 seconds |
|
|
82
82
|
| `max_retries` | — | 2 |
|
|
83
83
|
|
|
@@ -45,7 +45,7 @@ Every argument is keyword-only.
|
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| `api_key` | `BOXD_API_KEY` | — |
|
|
47
47
|
| `token` | `BOXD_TOKEN` | — |
|
|
48
|
-
| `base_url` | `BOXD_BASE_URL` (or the deprecated `BOXD_API_URL`) | `
|
|
48
|
+
| `base_url` | `BOXD_BASE_URL` (or the deprecated `BOXD_API_URL`) | `https://boxd.sh:9443` |
|
|
49
49
|
| `timeout` | — | 60.0 seconds |
|
|
50
50
|
| `max_retries` | — | 2 |
|
|
51
51
|
|
|
@@ -30,7 +30,7 @@ class Boxd:
|
|
|
30
30
|
|
|
31
31
|
boxd = Boxd() # production
|
|
32
32
|
boxd = Boxd(api_key="bxd_...")
|
|
33
|
-
boxd = Boxd(base_url="
|
|
33
|
+
boxd = Boxd(base_url="https://boxd-stg.sh:9443") # any other cluster
|
|
34
34
|
|
|
35
35
|
Args:
|
|
36
36
|
api_key: A `bxd_` API key. Defaults to `BOXD_API_KEY`.
|
|
@@ -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,
|
|
@@ -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:
|
|
@@ -11,10 +11,10 @@ import ipaddress
|
|
|
11
11
|
import os
|
|
12
12
|
from typing import NamedTuple
|
|
13
13
|
|
|
14
|
-
# gRPC front door.
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
DEFAULT_BASE_URL = "
|
|
14
|
+
# gRPC front door, over TLS. The proxy still accepts plain HTTP/2 on the same
|
|
15
|
+
# port for older clients, so an explicit http:// base_url keeps working against
|
|
16
|
+
# any cluster (see `crates/boxd-proxy/src/api_transport.rs`).
|
|
17
|
+
DEFAULT_BASE_URL = "https://boxd.sh:9443"
|
|
18
18
|
|
|
19
19
|
EXCHANGE_PATH = "/api/v1/auth/token"
|
|
20
20
|
|
|
@@ -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+)(.*)$")
|
|
@@ -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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.2.6.
|
|
3
|
+
Version: 0.2.6.dev399
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -77,7 +77,7 @@ Every argument is keyword-only.
|
|
|
77
77
|
|---|---|---|
|
|
78
78
|
| `api_key` | `BOXD_API_KEY` | — |
|
|
79
79
|
| `token` | `BOXD_TOKEN` | — |
|
|
80
|
-
| `base_url` | `BOXD_BASE_URL` (or the deprecated `BOXD_API_URL`) | `
|
|
80
|
+
| `base_url` | `BOXD_BASE_URL` (or the deprecated `BOXD_API_URL`) | `https://boxd.sh:9443` |
|
|
81
81
|
| `timeout` | — | 60.0 seconds |
|
|
82
82
|
| `max_retries` | — | 2 |
|
|
83
83
|
|
|
@@ -518,3 +518,15 @@ def test_backup_dedup_pointer_and_checksum_are_absent_on_a_dedup_row():
|
|
|
518
518
|
assert backup.status == "dedup"
|
|
519
519
|
assert backup.checksum is None
|
|
520
520
|
assert backup.points_to_backup_id == "bkp-1"
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def test_desktop_url_is_the_console_origin_plus_the_name():
|
|
524
|
+
"""Built client-side like `url`, and empty rather than half-built when the
|
|
525
|
+
client has no endpoint to derive a console origin from."""
|
|
526
|
+
from boxd import _mappers as m
|
|
527
|
+
|
|
528
|
+
vm = a_vm(name="box")
|
|
529
|
+
assert m.machine(vm, "https://app.boxd.sh").access.desktop_url == (
|
|
530
|
+
"https://app.boxd.sh/desktop/box"
|
|
531
|
+
)
|
|
532
|
+
assert m.machine(vm).access.desktop_url == ""
|
|
@@ -767,6 +767,7 @@ def test_machines_list_returns_a_plain_list():
|
|
|
767
767
|
machines = Machines(transport).list()
|
|
768
768
|
assert isinstance(machines, list)
|
|
769
769
|
assert [x.name for x in machines] == ["alpha", "beta"]
|
|
770
|
+
assert machines[0].access.desktop_url == "https://app.boxd.sh/desktop/alpha"
|
|
770
771
|
|
|
771
772
|
|
|
772
773
|
def test_machines_list_is_empty_when_the_account_has_none():
|
|
@@ -192,7 +192,7 @@ def test_production_is_the_last_resort():
|
|
|
192
192
|
t = SyncTransport(
|
|
193
193
|
base_url=None, credentials=FakeCredentials(), timeout=1.0, max_retries=2
|
|
194
194
|
)
|
|
195
|
-
assert t.base_url == "
|
|
195
|
+
assert t.base_url == "https://boxd.sh:9443"
|
|
196
196
|
|
|
197
197
|
|
|
198
198
|
# --- error mapping ---
|
|
@@ -13,6 +13,16 @@ from boxd._version_check import (
|
|
|
13
13
|
request_metadata,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def _stderr_is_a_terminal(monkeypatch):
|
|
19
|
+
"""The nag is for a person at a terminal, and pytest is not one.
|
|
20
|
+
|
|
21
|
+
Every test here assumes a TTY unless it says otherwise; the one that
|
|
22
|
+
checks the non-terminal case overrides this.
|
|
23
|
+
"""
|
|
24
|
+
monkeypatch.setattr(_version_check, "_stderr_is_tty", lambda: True)
|
|
25
|
+
|
|
16
26
|
# Picked to be unambiguously newer/older than any real shipped SDK so tests
|
|
17
27
|
# don't silently flip meaning when the package version bumps.
|
|
18
28
|
NEWER = "99.0.0"
|
|
@@ -119,3 +129,40 @@ def test_header_lookup_is_case_insensitive(stub_current, capsys):
|
|
|
119
129
|
def test_header_value_is_stripped(stub_current, capsys):
|
|
120
130
|
note_metadata([(RESP_HEADER, f" {NEWER} ")])
|
|
121
131
|
assert NEWER in capsys.readouterr().err
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_silent_when_stderr_is_not_a_terminal(monkeypatch, capsys):
|
|
135
|
+
"""CI, or an automation job whose stderr IS its log file: noise on every run."""
|
|
136
|
+
monkeypatch.setattr(_version_check, "_notified", False)
|
|
137
|
+
monkeypatch.setattr(_version_check, "_stderr_is_tty", lambda: False)
|
|
138
|
+
_version_check.note_metadata([(_version_check.RESP_HEADER, "99.0.0")])
|
|
139
|
+
assert capsys.readouterr().err == ""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_silent_when_latest_is_a_prerelease_and_we_are_on_a_release(monkeypatch, capsys):
|
|
143
|
+
"""A pre-release is opt-in (`pip install --pre`), and `--upgrade` alone
|
|
144
|
+
cannot install it — so the nag would repeat forever."""
|
|
145
|
+
monkeypatch.setattr(_version_check, "_notified", False)
|
|
146
|
+
monkeypatch.setattr(_version_check, "_current_version", lambda: "0.2.4")
|
|
147
|
+
_version_check.note_metadata([(_version_check.RESP_HEADER, "0.2.6.dev356")])
|
|
148
|
+
assert capsys.readouterr().err == ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_prerelease_user_is_told_the_prerelease_command(monkeypatch, capsys):
|
|
152
|
+
monkeypatch.setattr(_version_check, "_notified", False)
|
|
153
|
+
monkeypatch.setattr(_version_check, "_current_version", lambda: "0.2.6.dev1")
|
|
154
|
+
_version_check.note_metadata([(_version_check.RESP_HEADER, "0.2.6.dev356")])
|
|
155
|
+
assert "--pre" in capsys.readouterr().err
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_is_prerelease():
|
|
159
|
+
assert _version_check._is_prerelease("0.2.6.dev356")
|
|
160
|
+
assert _version_check._is_prerelease("0.2.6-dev.356")
|
|
161
|
+
assert not _version_check._is_prerelease("0.2.6")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_silent_when_the_opt_out_env_var_is_set(monkeypatch, capsys):
|
|
165
|
+
monkeypatch.setattr(_version_check, "_notified", False)
|
|
166
|
+
monkeypatch.setenv("BOXD_NO_UPDATE_NOTICE", "1")
|
|
167
|
+
_version_check.note_metadata([(_version_check.RESP_HEADER, "99.0.0")])
|
|
168
|
+
assert capsys.readouterr().err == ""
|
|
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
|