devnomads-cli 0.11.1__tar.gz → 0.11.2__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.
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/PKG-INFO +10 -6
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/README.md +9 -5
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/devnomads_cli.egg-info/PKG-INFO +10 -6
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/dncli.py +150 -14
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/pyproject.toml +1 -1
- devnomads_cli-0.11.2/tests/test_versions.py +271 -0
- devnomads_cli-0.11.1/tests/test_versions.py +0 -157
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/LICENSE +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/devnomads_cli.egg-info/SOURCES.txt +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/devnomads_cli.egg-info/dependency_links.txt +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/devnomads_cli.egg-info/entry_points.txt +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/devnomads_cli.egg-info/requires.txt +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/devnomads_cli.egg-info/top_level.txt +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/setup.cfg +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_cert.py +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_cli.py +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_config.py +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_generate.py +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_generated_cli.py +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_helpers.py +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_hook.py +0 -0
- {devnomads_cli-0.11.1 → devnomads_cli-0.11.2}/tests/test_transfer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devnomads-cli
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Manage your DevNomads services from the command line
|
|
5
5
|
Author-email: DevNomads <support@devnomads.nl>
|
|
6
6
|
License: MIT
|
|
@@ -46,16 +46,20 @@ devnomads-cli`) work as well.
|
|
|
46
46
|
|
|
47
47
|
### Updates
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Upgrade with:
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
|
-
|
|
52
|
+
dncli upgrade
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
It uses the tool you installed dncli with (uv, pipx, or pip in a
|
|
56
|
+
virtualenv); where it can't, such as a system-wide pip install or on
|
|
57
|
+
Windows, it prints the command to run instead. `dncli upgrade --dry-run`
|
|
58
|
+
shows what it would do.
|
|
59
|
+
|
|
55
60
|
dncli checks PyPI at most once a day and tells you when a new version
|
|
56
|
-
is out
|
|
57
|
-
|
|
58
|
-
`DN_NO_UPDATE_CHECK=1` to turn it off.
|
|
61
|
+
is out. It only does so on an interactive terminal, never in pipes,
|
|
62
|
+
scripts or CI; set `DN_NO_UPDATE_CHECK=1` to turn it off.
|
|
59
63
|
|
|
60
64
|
dncli also warns when the DevNomads API reports a major version it was
|
|
61
65
|
not built for, and tells you when the API no longer supports your
|
|
@@ -30,16 +30,20 @@ devnomads-cli`) work as well.
|
|
|
30
30
|
|
|
31
31
|
### Updates
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Upgrade with:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
|
|
36
|
+
dncli upgrade
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
It uses the tool you installed dncli with (uv, pipx, or pip in a
|
|
40
|
+
virtualenv); where it can't, such as a system-wide pip install or on
|
|
41
|
+
Windows, it prints the command to run instead. `dncli upgrade --dry-run`
|
|
42
|
+
shows what it would do.
|
|
43
|
+
|
|
39
44
|
dncli checks PyPI at most once a day and tells you when a new version
|
|
40
|
-
is out
|
|
41
|
-
|
|
42
|
-
`DN_NO_UPDATE_CHECK=1` to turn it off.
|
|
45
|
+
is out. It only does so on an interactive terminal, never in pipes,
|
|
46
|
+
scripts or CI; set `DN_NO_UPDATE_CHECK=1` to turn it off.
|
|
43
47
|
|
|
44
48
|
dncli also warns when the DevNomads API reports a major version it was
|
|
45
49
|
not built for, and tells you when the API no longer supports your
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devnomads-cli
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Manage your DevNomads services from the command line
|
|
5
5
|
Author-email: DevNomads <support@devnomads.nl>
|
|
6
6
|
License: MIT
|
|
@@ -46,16 +46,20 @@ devnomads-cli`) work as well.
|
|
|
46
46
|
|
|
47
47
|
### Updates
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Upgrade with:
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
|
-
|
|
52
|
+
dncli upgrade
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
It uses the tool you installed dncli with (uv, pipx, or pip in a
|
|
56
|
+
virtualenv); where it can't, such as a system-wide pip install or on
|
|
57
|
+
Windows, it prints the command to run instead. `dncli upgrade --dry-run`
|
|
58
|
+
shows what it would do.
|
|
59
|
+
|
|
55
60
|
dncli checks PyPI at most once a day and tells you when a new version
|
|
56
|
-
is out
|
|
57
|
-
|
|
58
|
-
`DN_NO_UPDATE_CHECK=1` to turn it off.
|
|
61
|
+
is out. It only does so on an interactive terminal, never in pipes,
|
|
62
|
+
scripts or CI; set `DN_NO_UPDATE_CHECK=1` to turn it off.
|
|
59
63
|
|
|
60
64
|
dncli also warns when the DevNomads API reports a major version it was
|
|
61
65
|
not built for, and tells you when the API no longer supports your
|
|
@@ -31,6 +31,7 @@ import re
|
|
|
31
31
|
import secrets
|
|
32
32
|
import shutil
|
|
33
33
|
import stat
|
|
34
|
+
import subprocess # nosec B404 - only for `dncli upgrade`
|
|
34
35
|
import sys
|
|
35
36
|
import threading
|
|
36
37
|
import time
|
|
@@ -63,7 +64,7 @@ from rich.markup import escape
|
|
|
63
64
|
from rich.table import Table
|
|
64
65
|
from typer.core import TyperGroup
|
|
65
66
|
|
|
66
|
-
__version__ = "0.11.
|
|
67
|
+
__version__ = "0.11.2"
|
|
67
68
|
|
|
68
69
|
# Distribution names, for reporting versions via `dncli version`.
|
|
69
70
|
CLI_PACKAGE = "devnomads-cli"
|
|
@@ -112,17 +113,70 @@ def _version_tuple(text: str | None) -> tuple[int, int, int] | None:
|
|
|
112
113
|
return major, minor, patch
|
|
113
114
|
|
|
114
115
|
|
|
116
|
+
def install_method() -> str:
|
|
117
|
+
"""How this copy of dncli was installed, from the marker files each
|
|
118
|
+
installer leaves in the environment it creates:
|
|
119
|
+
|
|
120
|
+
uv-tool (uv tool install), pipx, uvx (uv's throwaway environments),
|
|
121
|
+
venv (pip in a virtualenv), system (pip into a system or user Python)
|
|
122
|
+
or script (the single-file dncli.py)."""
|
|
123
|
+
|
|
124
|
+
prefix = Path(sys.prefix)
|
|
125
|
+
if _package_version(CLI_PACKAGE, "") == "":
|
|
126
|
+
return "script"
|
|
127
|
+
if (prefix / "uv-receipt.toml").is_file():
|
|
128
|
+
return "uv-tool"
|
|
129
|
+
if (prefix / "pipx_metadata.json").is_file():
|
|
130
|
+
return "pipx"
|
|
131
|
+
if any(part.startswith("archive-v") for part in prefix.parts):
|
|
132
|
+
return "uvx"
|
|
133
|
+
if sys.prefix != sys.base_prefix:
|
|
134
|
+
return "venv"
|
|
135
|
+
return "system"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def upgrade_argv(method: str) -> list[str] | None:
|
|
139
|
+
"""The command that upgrades dncli in place, or None when dncli cannot
|
|
140
|
+
or should not run it itself. On Windows the running dncli.exe cannot be
|
|
141
|
+
replaced, so the upgrade is left to the user there."""
|
|
142
|
+
|
|
143
|
+
if sys.platform == "win32":
|
|
144
|
+
return None
|
|
145
|
+
if method == "uv-tool" and (uv := shutil.which("uv")):
|
|
146
|
+
return [uv, "tool", "upgrade", CLI_PACKAGE]
|
|
147
|
+
if method == "pipx" and (pipx := shutil.which("pipx")):
|
|
148
|
+
return [pipx, "upgrade", CLI_PACKAGE]
|
|
149
|
+
if method == "venv":
|
|
150
|
+
from importlib.util import find_spec
|
|
151
|
+
|
|
152
|
+
if find_spec("pip") is not None:
|
|
153
|
+
return [sys.executable, "-m", "pip", "install", "-U", CLI_PACKAGE]
|
|
154
|
+
if uv := shutil.which("uv"): # uv-made virtualenvs have no pip
|
|
155
|
+
return [uv, "pip", "install", "--python", sys.executable, "-U", CLI_PACKAGE]
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def manual_upgrade(method: str) -> str:
|
|
160
|
+
"""How to upgrade by hand, for messages and when dncli cannot do it."""
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"uv-tool": f"uv tool upgrade {CLI_PACKAGE}",
|
|
164
|
+
"pipx": f"pipx upgrade {CLI_PACKAGE}",
|
|
165
|
+
"uvx": f"uvx --from {CLI_PACKAGE}@latest dncli (uvx caches the version "
|
|
166
|
+
"it ran last)",
|
|
167
|
+
"venv": f"pip install -U {CLI_PACKAGE}",
|
|
168
|
+
"system": f"pip install -U {CLI_PACKAGE} (with --user or sudo, as you "
|
|
169
|
+
"installed it)",
|
|
170
|
+
"script": "download the latest dncli.py",
|
|
171
|
+
}[method]
|
|
172
|
+
|
|
173
|
+
|
|
115
174
|
def upgrade_command() -> str:
|
|
116
|
-
"""How to upgrade dncli,
|
|
175
|
+
"""How to upgrade dncli, for notices: `dncli upgrade` when it can do the
|
|
176
|
+
job itself, otherwise the command to run by hand."""
|
|
117
177
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return f"uv tool upgrade {CLI_PACKAGE}"
|
|
121
|
-
if "/pipx/venvs/" in prefix:
|
|
122
|
-
return f"pipx upgrade {CLI_PACKAGE}"
|
|
123
|
-
if _package_version(CLI_PACKAGE, "") == "": # the single-file script
|
|
124
|
-
return "download the latest dncli.py"
|
|
125
|
-
return f"pip install -U {CLI_PACKAGE}"
|
|
178
|
+
method = install_method()
|
|
179
|
+
return "dncli upgrade" if upgrade_argv(method) else manual_upgrade(method)
|
|
126
180
|
|
|
127
181
|
|
|
128
182
|
# ---------------------------------------------------------------------------
|
|
@@ -1382,6 +1436,80 @@ def version_cmd(
|
|
|
1382
1436
|
render(state_from(ctx, output, columns), _versions())
|
|
1383
1437
|
|
|
1384
1438
|
|
|
1439
|
+
@app.command("upgrade")
|
|
1440
|
+
def upgrade_cmd(
|
|
1441
|
+
dry_run: Annotated[
|
|
1442
|
+
bool,
|
|
1443
|
+
typer.Option(
|
|
1444
|
+
"--dry-run", help="Show how dncli would upgrade, without doing it."
|
|
1445
|
+
),
|
|
1446
|
+
] = False,
|
|
1447
|
+
) -> None:
|
|
1448
|
+
"""Upgrade dncli to the latest release.
|
|
1449
|
+
|
|
1450
|
+
Uses the tool dncli was installed with (uv, pipx or pip in a
|
|
1451
|
+
virtualenv). Where dncli cannot do that itself, it prints the command
|
|
1452
|
+
to run."""
|
|
1453
|
+
|
|
1454
|
+
method = install_method()
|
|
1455
|
+
argv = upgrade_argv(method)
|
|
1456
|
+
latest = latest_version()
|
|
1457
|
+
latest_t, current_t = _version_tuple(latest), _version_tuple(__version__)
|
|
1458
|
+
newer = latest_t is not None and latest_t > (current_t or (0, 0, 0))
|
|
1459
|
+
status = (
|
|
1460
|
+
f"dncli {__version__}; latest release {latest}"
|
|
1461
|
+
if latest
|
|
1462
|
+
else f"dncli {__version__}; could not reach PyPI for the latest release"
|
|
1463
|
+
)
|
|
1464
|
+
err_console.print(status)
|
|
1465
|
+
if dry_run:
|
|
1466
|
+
err_console.print(f"installed with: {method}")
|
|
1467
|
+
command = " ".join(argv) if argv else manual_upgrade(method)
|
|
1468
|
+
err_console.print(
|
|
1469
|
+
f"would run: {command}" if argv else f"upgrade by hand: {command}"
|
|
1470
|
+
)
|
|
1471
|
+
return
|
|
1472
|
+
if latest is not None and not newer:
|
|
1473
|
+
err_console.print("already up to date")
|
|
1474
|
+
return
|
|
1475
|
+
if argv is None:
|
|
1476
|
+
raise CliError(
|
|
1477
|
+
f"dncli cannot upgrade this installation ({method}) itself; "
|
|
1478
|
+
f"run: {manual_upgrade(method)}"
|
|
1479
|
+
)
|
|
1480
|
+
err_console.print(f"running: {' '.join(argv)}")
|
|
1481
|
+
result = subprocess.run(argv, check=False) # nosec B603 - fixed argv, no shell
|
|
1482
|
+
if result.returncode != 0:
|
|
1483
|
+
raise CliError(f"upgrade failed (exit code {result.returncode})")
|
|
1484
|
+
installed = _installed_version()
|
|
1485
|
+
if installed and installed != __version__:
|
|
1486
|
+
err_console.print(f"dncli upgraded from {__version__} to {installed}")
|
|
1487
|
+
else:
|
|
1488
|
+
err_console.print("dncli is up to date")
|
|
1489
|
+
state = _read_update_state()
|
|
1490
|
+
state.update(latest=installed or latest or __version__, checked_at=time.time())
|
|
1491
|
+
_write_update_state(state)
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def _installed_version() -> str | None:
|
|
1495
|
+
"""The dncli version now installed in this environment, read in a fresh
|
|
1496
|
+
interpreter because this process still has the old one loaded."""
|
|
1497
|
+
|
|
1498
|
+
result = subprocess.run( # nosec B603 - fixed argv, no shell
|
|
1499
|
+
[
|
|
1500
|
+
sys.executable,
|
|
1501
|
+
"-c",
|
|
1502
|
+
f"from importlib.metadata import version; print(version({CLI_PACKAGE!r}))",
|
|
1503
|
+
],
|
|
1504
|
+
capture_output=True,
|
|
1505
|
+
text=True,
|
|
1506
|
+
check=False,
|
|
1507
|
+
)
|
|
1508
|
+
if result.returncode != 0:
|
|
1509
|
+
return None
|
|
1510
|
+
return result.stdout.strip() or None
|
|
1511
|
+
|
|
1512
|
+
|
|
1385
1513
|
# --- configure -------------------------------------------------------------
|
|
1386
1514
|
|
|
1387
1515
|
|
|
@@ -9127,9 +9255,8 @@ def _write_update_state(state: dict[str, Any]) -> None:
|
|
|
9127
9255
|
pass
|
|
9128
9256
|
|
|
9129
9257
|
|
|
9130
|
-
def
|
|
9131
|
-
"""
|
|
9132
|
-
ignored: the check must never get in the way of a command."""
|
|
9258
|
+
def latest_version() -> str | None:
|
|
9259
|
+
"""The latest dncli release on PyPI, or None when PyPI cannot be reached."""
|
|
9133
9260
|
|
|
9134
9261
|
try:
|
|
9135
9262
|
response = httpx.get(
|
|
@@ -9137,8 +9264,17 @@ def _fetch_latest_version() -> None:
|
|
|
9137
9264
|
timeout=UPDATE_CHECK_TIMEOUT,
|
|
9138
9265
|
headers={"User-Agent": user_agent()},
|
|
9139
9266
|
)
|
|
9140
|
-
|
|
9267
|
+
return str(response.json()["info"]["version"])
|
|
9141
9268
|
except Exception: # best effort: offline or a PyPI hiccup is fine
|
|
9269
|
+
return None
|
|
9270
|
+
|
|
9271
|
+
|
|
9272
|
+
def _fetch_latest_version() -> None:
|
|
9273
|
+
"""Ask PyPI for the latest release and remember it. Any failure is
|
|
9274
|
+
ignored: the check must never get in the way of a command."""
|
|
9275
|
+
|
|
9276
|
+
latest = latest_version()
|
|
9277
|
+
if latest is None:
|
|
9142
9278
|
return
|
|
9143
9279
|
state = _read_update_state()
|
|
9144
9280
|
state.update(latest=str(latest), checked_at=time.time())
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Update notice, API version check and the user agent."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import respx
|
|
7
|
+
from httpx import Response
|
|
8
|
+
|
|
9
|
+
import dncli
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def interactive(monkeypatch):
|
|
14
|
+
monkeypatch.delenv(dncli.ENV_NO_UPDATE_CHECK, raising=False)
|
|
15
|
+
monkeypatch.delenv("CI", raising=False)
|
|
16
|
+
monkeypatch.setattr(dncli.err_console, "_force_terminal", True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def write_state(**state):
|
|
20
|
+
path = dncli._update_state_path()
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
path.write_text(json.dumps(state))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_version_tuple():
|
|
26
|
+
assert dncli._version_tuple("0.10.0") == (0, 10, 0)
|
|
27
|
+
assert dncli._version_tuple("2.1") == (2, 1, 0)
|
|
28
|
+
assert dncli._version_tuple("1.0.0rc1") == (1, 0, 0)
|
|
29
|
+
assert dncli._version_tuple("") is None
|
|
30
|
+
assert dncli._version_tuple(None) is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_update_check_is_off_outside_a_terminal(monkeypatch):
|
|
34
|
+
monkeypatch.setattr(dncli.err_console, "_force_terminal", False)
|
|
35
|
+
assert not dncli._update_check_enabled()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_update_check_can_be_disabled(interactive, monkeypatch):
|
|
39
|
+
assert dncli._update_check_enabled()
|
|
40
|
+
monkeypatch.setenv(dncli.ENV_NO_UPDATE_CHECK, "1")
|
|
41
|
+
assert not dncli._update_check_enabled()
|
|
42
|
+
monkeypatch.delenv(dncli.ENV_NO_UPDATE_CHECK)
|
|
43
|
+
monkeypatch.setenv("CI", "true")
|
|
44
|
+
assert not dncli._update_check_enabled()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_notice_when_a_newer_version_is_out(interactive, capsys):
|
|
48
|
+
write_state(latest="99.0.0", checked_at=0)
|
|
49
|
+
dncli.notify_update()
|
|
50
|
+
assert "dncli 99.0.0 is available" in capsys.readouterr().err
|
|
51
|
+
# shown at most once a day
|
|
52
|
+
dncli.notify_update()
|
|
53
|
+
assert capsys.readouterr().err == ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_no_notice_when_up_to_date(interactive, capsys):
|
|
57
|
+
write_state(latest=dncli.__version__, checked_at=0)
|
|
58
|
+
dncli.notify_update()
|
|
59
|
+
assert capsys.readouterr().err == ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@respx.mock
|
|
63
|
+
def test_fetch_stores_the_latest_version():
|
|
64
|
+
respx.get(dncli.PYPI_URL).mock(
|
|
65
|
+
return_value=Response(200, json={"info": {"version": "1.2.3"}})
|
|
66
|
+
)
|
|
67
|
+
dncli._fetch_latest_version()
|
|
68
|
+
assert dncli._read_update_state()["latest"] == "1.2.3"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@respx.mock
|
|
72
|
+
def test_fetch_failures_are_ignored():
|
|
73
|
+
respx.get(dncli.PYPI_URL).mock(return_value=Response(500, text="down"))
|
|
74
|
+
dncli._fetch_latest_version()
|
|
75
|
+
assert dncli._read_update_state() == {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_fresh_state_skips_the_background_check(interactive, monkeypatch):
|
|
79
|
+
started = []
|
|
80
|
+
monkeypatch.setattr(
|
|
81
|
+
dncli.threading, "Thread", lambda **kw: started.append(kw) or _Dummy()
|
|
82
|
+
)
|
|
83
|
+
write_state(latest="0.0.1", checked_at=dncli.time.time())
|
|
84
|
+
dncli.start_update_check()
|
|
85
|
+
assert started == []
|
|
86
|
+
write_state(latest="0.0.1", checked_at=0)
|
|
87
|
+
dncli.start_update_check()
|
|
88
|
+
assert len(started) == 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _Dummy:
|
|
92
|
+
def start(self):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture
|
|
97
|
+
def api_warning(monkeypatch):
|
|
98
|
+
monkeypatch.setattr(dncli, "_api_version_warned", False)
|
|
99
|
+
monkeypatch.setattr(dncli, "API_VERSION", "1.4.0")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_same_api_major_is_silent(api_warning, capsys):
|
|
103
|
+
dncli.check_api_version("1.9.2")
|
|
104
|
+
dncli.check_api_version(None)
|
|
105
|
+
assert capsys.readouterr().err == ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_newer_api_major_warns_once(api_warning, capsys):
|
|
109
|
+
dncli.check_api_version("2.0.0")
|
|
110
|
+
dncli.check_api_version("2.0.0")
|
|
111
|
+
err = capsys.readouterr().err
|
|
112
|
+
assert err.count("warning:") == 1
|
|
113
|
+
assert "built for API 1.4.0" in err and "update dncli" in err
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_older_api_major_warns(api_warning, capsys):
|
|
117
|
+
dncli.check_api_version("0.9.0")
|
|
118
|
+
assert "newer than the API" in capsys.readouterr().err
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_upgrade_required_message():
|
|
122
|
+
message = dncli._error_message(426, "Upgrade Required")
|
|
123
|
+
assert "no longer supported" in message
|
|
124
|
+
assert dncli.upgrade_command() in message
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.fixture
|
|
128
|
+
def env(tmp_path, monkeypatch):
|
|
129
|
+
"""A fake installation prefix; returns a function that sets it up."""
|
|
130
|
+
|
|
131
|
+
def make(marker=None, base=None, parts=("tools", "devnomads-cli")):
|
|
132
|
+
prefix = tmp_path.joinpath(*parts)
|
|
133
|
+
prefix.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
if marker:
|
|
135
|
+
(prefix / marker).write_text("")
|
|
136
|
+
monkeypatch.setattr(dncli.sys, "prefix", str(prefix))
|
|
137
|
+
monkeypatch.setattr(dncli.sys, "base_prefix", base or "/usr")
|
|
138
|
+
monkeypatch.setattr(dncli.sys, "platform", "linux")
|
|
139
|
+
return prefix
|
|
140
|
+
|
|
141
|
+
return make
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_install_method_from_marker_files(env):
|
|
145
|
+
env("uv-receipt.toml")
|
|
146
|
+
assert dncli.install_method() == "uv-tool"
|
|
147
|
+
env("pipx_metadata.json", parts=("pipx", "venvs", "devnomads-cli"))
|
|
148
|
+
assert dncli.install_method() == "pipx"
|
|
149
|
+
env(parts=("cache", "uv", "archive-v0", "abc"))
|
|
150
|
+
assert dncli.install_method() == "uvx"
|
|
151
|
+
env(parts=("project", ".venv"))
|
|
152
|
+
assert dncli.install_method() == "venv"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_install_method_system_and_script(env, monkeypatch):
|
|
156
|
+
prefix = env()
|
|
157
|
+
monkeypatch.setattr(dncli.sys, "base_prefix", str(prefix))
|
|
158
|
+
assert dncli.install_method() == "system"
|
|
159
|
+
monkeypatch.setattr(dncli, "_package_version", lambda name, fallback="": "")
|
|
160
|
+
assert dncli.install_method() == "script"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_upgrade_argv_needs_the_installer(env, monkeypatch):
|
|
164
|
+
env("uv-receipt.toml")
|
|
165
|
+
monkeypatch.setattr(dncli.shutil, "which", lambda name: f"/bin/{name}")
|
|
166
|
+
assert dncli.upgrade_argv("uv-tool") == [
|
|
167
|
+
"/bin/uv",
|
|
168
|
+
"tool",
|
|
169
|
+
"upgrade",
|
|
170
|
+
"devnomads-cli",
|
|
171
|
+
]
|
|
172
|
+
assert dncli.upgrade_command() == "dncli upgrade"
|
|
173
|
+
monkeypatch.setattr(dncli.shutil, "which", lambda name: None)
|
|
174
|
+
assert dncli.upgrade_argv("uv-tool") is None
|
|
175
|
+
assert dncli.upgrade_command() == "uv tool upgrade devnomads-cli"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_upgrade_is_left_to_the_user_on_windows_and_system_pip(env, monkeypatch):
|
|
179
|
+
env("uv-receipt.toml")
|
|
180
|
+
monkeypatch.setattr(dncli.shutil, "which", lambda name: f"/bin/{name}")
|
|
181
|
+
monkeypatch.setattr(dncli.sys, "platform", "win32")
|
|
182
|
+
assert dncli.upgrade_argv("uv-tool") is None
|
|
183
|
+
monkeypatch.setattr(dncli.sys, "platform", "linux")
|
|
184
|
+
assert dncli.upgrade_argv("system") is None
|
|
185
|
+
assert dncli.upgrade_argv("uvx") is None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.fixture
|
|
189
|
+
def upgrade(monkeypatch):
|
|
190
|
+
"""Stub PyPI, the installer and subprocess for `dncli upgrade`."""
|
|
191
|
+
|
|
192
|
+
calls = []
|
|
193
|
+
|
|
194
|
+
class Done:
|
|
195
|
+
def __init__(self, returncode=0, stdout=""):
|
|
196
|
+
self.returncode, self.stdout = returncode, stdout
|
|
197
|
+
|
|
198
|
+
def run(argv, **kwargs):
|
|
199
|
+
calls.append(argv)
|
|
200
|
+
if argv[0] == dncli.sys.executable: # the version probe
|
|
201
|
+
return Done(stdout="99.0.0\n")
|
|
202
|
+
return Done()
|
|
203
|
+
|
|
204
|
+
monkeypatch.setattr(dncli, "latest_version", lambda: "99.0.0")
|
|
205
|
+
monkeypatch.setattr(dncli, "install_method", lambda: "uv-tool")
|
|
206
|
+
monkeypatch.setattr(
|
|
207
|
+
dncli, "upgrade_argv", lambda method: ["uv", "tool", "upgrade", "devnomads-cli"]
|
|
208
|
+
)
|
|
209
|
+
monkeypatch.setattr(dncli.subprocess, "run", run)
|
|
210
|
+
return calls
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_upgrade_runs_the_installer(upgrade):
|
|
214
|
+
from typer.testing import CliRunner
|
|
215
|
+
|
|
216
|
+
result = CliRunner().invoke(dncli.app, ["upgrade"])
|
|
217
|
+
assert result.exit_code == 0, result.output
|
|
218
|
+
assert upgrade[0] == ["uv", "tool", "upgrade", "devnomads-cli"]
|
|
219
|
+
assert f"upgraded from {dncli.__version__} to 99.0.0" in result.stderr
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_upgrade_dry_run_only_reports(upgrade):
|
|
223
|
+
from typer.testing import CliRunner
|
|
224
|
+
|
|
225
|
+
result = CliRunner().invoke(dncli.app, ["upgrade", "--dry-run"])
|
|
226
|
+
assert result.exit_code == 0
|
|
227
|
+
assert "installed with: uv-tool" in result.stderr
|
|
228
|
+
assert "would run: uv tool upgrade devnomads-cli" in result.stderr
|
|
229
|
+
assert upgrade == []
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_upgrade_when_already_current(upgrade, monkeypatch):
|
|
233
|
+
from typer.testing import CliRunner
|
|
234
|
+
|
|
235
|
+
monkeypatch.setattr(dncli, "latest_version", lambda: dncli.__version__)
|
|
236
|
+
result = CliRunner().invoke(dncli.app, ["upgrade"])
|
|
237
|
+
assert result.exit_code == 0
|
|
238
|
+
assert "already up to date" in result.stderr
|
|
239
|
+
assert upgrade == []
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_upgrade_explains_what_it_cannot_do(upgrade, monkeypatch):
|
|
243
|
+
from typer.testing import CliRunner
|
|
244
|
+
|
|
245
|
+
monkeypatch.setattr(dncli, "install_method", lambda: "system")
|
|
246
|
+
monkeypatch.setattr(dncli, "upgrade_argv", lambda method: None)
|
|
247
|
+
result = CliRunner().invoke(dncli.app, ["upgrade"])
|
|
248
|
+
assert result.exit_code == 1
|
|
249
|
+
assert "pip install -U devnomads-cli" in result.stderr
|
|
250
|
+
assert upgrade == []
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_user_agent_names_dncli_library_and_python():
|
|
254
|
+
agent = dncli.user_agent()
|
|
255
|
+
assert agent.startswith(f"dncli/{dncli.__version__} devnomads/")
|
|
256
|
+
assert " python/3." in agent
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@respx.mock
|
|
260
|
+
def test_api_version_header_is_checked_on_every_call(
|
|
261
|
+
write_profile, api_warning, monkeypatch
|
|
262
|
+
):
|
|
263
|
+
from typer.testing import CliRunner
|
|
264
|
+
|
|
265
|
+
write_profile(api_key="testkey-1234567890", api_url="https://api.test")
|
|
266
|
+
respx.get("https://api.test/services/servers").mock(
|
|
267
|
+
return_value=Response(200, json=[], headers={"X-Api-Version": "2.0.0"})
|
|
268
|
+
)
|
|
269
|
+
result = CliRunner().invoke(dncli.app, ["--out", "json", "servers", "list"])
|
|
270
|
+
assert result.exit_code == 0
|
|
271
|
+
assert "the API reports 2.0.0" in result.stderr
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"""Update notice, API version check and the user agent."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
import respx
|
|
7
|
-
from httpx import Response
|
|
8
|
-
|
|
9
|
-
import dncli
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def interactive(monkeypatch):
|
|
14
|
-
monkeypatch.delenv(dncli.ENV_NO_UPDATE_CHECK, raising=False)
|
|
15
|
-
monkeypatch.delenv("CI", raising=False)
|
|
16
|
-
monkeypatch.setattr(dncli.err_console, "_force_terminal", True)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def write_state(**state):
|
|
20
|
-
path = dncli._update_state_path()
|
|
21
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
-
path.write_text(json.dumps(state))
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_version_tuple():
|
|
26
|
-
assert dncli._version_tuple("0.10.0") == (0, 10, 0)
|
|
27
|
-
assert dncli._version_tuple("2.1") == (2, 1, 0)
|
|
28
|
-
assert dncli._version_tuple("1.0.0rc1") == (1, 0, 0)
|
|
29
|
-
assert dncli._version_tuple("") is None
|
|
30
|
-
assert dncli._version_tuple(None) is None
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_update_check_is_off_outside_a_terminal(monkeypatch):
|
|
34
|
-
monkeypatch.setattr(dncli.err_console, "_force_terminal", False)
|
|
35
|
-
assert not dncli._update_check_enabled()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_update_check_can_be_disabled(interactive, monkeypatch):
|
|
39
|
-
assert dncli._update_check_enabled()
|
|
40
|
-
monkeypatch.setenv(dncli.ENV_NO_UPDATE_CHECK, "1")
|
|
41
|
-
assert not dncli._update_check_enabled()
|
|
42
|
-
monkeypatch.delenv(dncli.ENV_NO_UPDATE_CHECK)
|
|
43
|
-
monkeypatch.setenv("CI", "true")
|
|
44
|
-
assert not dncli._update_check_enabled()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_notice_when_a_newer_version_is_out(interactive, capsys):
|
|
48
|
-
write_state(latest="99.0.0", checked_at=0)
|
|
49
|
-
dncli.notify_update()
|
|
50
|
-
assert "dncli 99.0.0 is available" in capsys.readouterr().err
|
|
51
|
-
# shown at most once a day
|
|
52
|
-
dncli.notify_update()
|
|
53
|
-
assert capsys.readouterr().err == ""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_no_notice_when_up_to_date(interactive, capsys):
|
|
57
|
-
write_state(latest=dncli.__version__, checked_at=0)
|
|
58
|
-
dncli.notify_update()
|
|
59
|
-
assert capsys.readouterr().err == ""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@respx.mock
|
|
63
|
-
def test_fetch_stores_the_latest_version():
|
|
64
|
-
respx.get(dncli.PYPI_URL).mock(
|
|
65
|
-
return_value=Response(200, json={"info": {"version": "1.2.3"}})
|
|
66
|
-
)
|
|
67
|
-
dncli._fetch_latest_version()
|
|
68
|
-
assert dncli._read_update_state()["latest"] == "1.2.3"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@respx.mock
|
|
72
|
-
def test_fetch_failures_are_ignored():
|
|
73
|
-
respx.get(dncli.PYPI_URL).mock(return_value=Response(500, text="down"))
|
|
74
|
-
dncli._fetch_latest_version()
|
|
75
|
-
assert dncli._read_update_state() == {}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_fresh_state_skips_the_background_check(interactive, monkeypatch):
|
|
79
|
-
started = []
|
|
80
|
-
monkeypatch.setattr(
|
|
81
|
-
dncli.threading, "Thread", lambda **kw: started.append(kw) or _Dummy()
|
|
82
|
-
)
|
|
83
|
-
write_state(latest="0.0.1", checked_at=dncli.time.time())
|
|
84
|
-
dncli.start_update_check()
|
|
85
|
-
assert started == []
|
|
86
|
-
write_state(latest="0.0.1", checked_at=0)
|
|
87
|
-
dncli.start_update_check()
|
|
88
|
-
assert len(started) == 1
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class _Dummy:
|
|
92
|
-
def start(self):
|
|
93
|
-
pass
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@pytest.fixture
|
|
97
|
-
def api_warning(monkeypatch):
|
|
98
|
-
monkeypatch.setattr(dncli, "_api_version_warned", False)
|
|
99
|
-
monkeypatch.setattr(dncli, "API_VERSION", "1.4.0")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def test_same_api_major_is_silent(api_warning, capsys):
|
|
103
|
-
dncli.check_api_version("1.9.2")
|
|
104
|
-
dncli.check_api_version(None)
|
|
105
|
-
assert capsys.readouterr().err == ""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def test_newer_api_major_warns_once(api_warning, capsys):
|
|
109
|
-
dncli.check_api_version("2.0.0")
|
|
110
|
-
dncli.check_api_version("2.0.0")
|
|
111
|
-
err = capsys.readouterr().err
|
|
112
|
-
assert err.count("warning:") == 1
|
|
113
|
-
assert "built for API 1.4.0" in err and "update dncli" in err
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_older_api_major_warns(api_warning, capsys):
|
|
117
|
-
dncli.check_api_version("0.9.0")
|
|
118
|
-
assert "newer than the API" in capsys.readouterr().err
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def test_upgrade_required_message():
|
|
122
|
-
message = dncli._error_message(426, "Upgrade Required")
|
|
123
|
-
assert "no longer supported" in message
|
|
124
|
-
assert dncli.upgrade_command() in message
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@pytest.mark.parametrize(
|
|
128
|
-
"prefix, expected",
|
|
129
|
-
[
|
|
130
|
-
("/home/u/.local/share/uv/tools/devnomads-cli", "uv tool upgrade"),
|
|
131
|
-
("/home/u/.local/pipx/venvs/devnomads-cli", "pipx upgrade"),
|
|
132
|
-
],
|
|
133
|
-
)
|
|
134
|
-
def test_upgrade_command_follows_the_install_method(monkeypatch, prefix, expected):
|
|
135
|
-
monkeypatch.setattr(dncli.sys, "prefix", prefix)
|
|
136
|
-
assert dncli.upgrade_command().startswith(expected)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def test_user_agent_names_dncli_library_and_python():
|
|
140
|
-
agent = dncli.user_agent()
|
|
141
|
-
assert agent.startswith(f"dncli/{dncli.__version__} devnomads/")
|
|
142
|
-
assert " python/3." in agent
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@respx.mock
|
|
146
|
-
def test_api_version_header_is_checked_on_every_call(
|
|
147
|
-
write_profile, api_warning, monkeypatch
|
|
148
|
-
):
|
|
149
|
-
from typer.testing import CliRunner
|
|
150
|
-
|
|
151
|
-
write_profile(api_key="testkey-1234567890", api_url="https://api.test")
|
|
152
|
-
respx.get("https://api.test/services/servers").mock(
|
|
153
|
-
return_value=Response(200, json=[], headers={"X-Api-Version": "2.0.0"})
|
|
154
|
-
)
|
|
155
|
-
result = CliRunner().invoke(dncli.app, ["--out", "json", "servers", "list"])
|
|
156
|
-
assert result.exit_code == 0
|
|
157
|
-
assert "the API reports 2.0.0" in result.stderr
|
|
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
|