devnomads-cli 0.12.0__tar.gz → 0.13.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.
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/PKG-INFO +2 -2
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/devnomads_cli.egg-info/PKG-INFO +2 -2
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/devnomads_cli.egg-info/requires.txt +1 -1
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/dncli.py +60 -7
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/pyproject.toml +2 -2
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_helpers.py +55 -3
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/LICENSE +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/README.md +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/devnomads_cli.egg-info/SOURCES.txt +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/devnomads_cli.egg-info/dependency_links.txt +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/devnomads_cli.egg-info/entry_points.txt +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/devnomads_cli.egg-info/top_level.txt +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/setup.cfg +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_cert.py +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_cli.py +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_config.py +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_generate.py +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_generated_cli.py +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_hook.py +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_transfer.py +0 -0
- {devnomads_cli-0.12.0 → devnomads_cli-0.13.0}/tests/test_versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devnomads-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Manage your DevNomads services from the command line
|
|
5
5
|
Author-email: DevNomads <support@devnomads.nl>
|
|
6
6
|
License: MIT
|
|
@@ -11,7 +11,7 @@ Requires-Dist: typer>=0.19
|
|
|
11
11
|
Requires-Dist: httpx>=0.27
|
|
12
12
|
Requires-Dist: rich>=13
|
|
13
13
|
Requires-Dist: cryptography>=42
|
|
14
|
-
Requires-Dist: devnomads[acme]>=0.6.
|
|
14
|
+
Requires-Dist: devnomads[acme]>=0.6.2
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# dncli
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devnomads-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Manage your DevNomads services from the command line
|
|
5
5
|
Author-email: DevNomads <support@devnomads.nl>
|
|
6
6
|
License: MIT
|
|
@@ -11,7 +11,7 @@ Requires-Dist: typer>=0.19
|
|
|
11
11
|
Requires-Dist: httpx>=0.27
|
|
12
12
|
Requires-Dist: rich>=13
|
|
13
13
|
Requires-Dist: cryptography>=42
|
|
14
|
-
Requires-Dist: devnomads[acme]>=0.6.
|
|
14
|
+
Requires-Dist: devnomads[acme]>=0.6.2
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# dncli
|
|
@@ -26,6 +26,7 @@ import hmac
|
|
|
26
26
|
import io
|
|
27
27
|
import json
|
|
28
28
|
import logging
|
|
29
|
+
import math
|
|
29
30
|
import os
|
|
30
31
|
import re
|
|
31
32
|
import secrets
|
|
@@ -36,7 +37,7 @@ import sys
|
|
|
36
37
|
import threading
|
|
37
38
|
import time
|
|
38
39
|
import urllib.parse
|
|
39
|
-
from contextlib import nullcontext
|
|
40
|
+
from contextlib import contextmanager, nullcontext
|
|
40
41
|
from dataclasses import dataclass
|
|
41
42
|
from enum import Enum
|
|
42
43
|
from pathlib import Path
|
|
@@ -44,8 +45,8 @@ from typing import (
|
|
|
44
45
|
Annotated,
|
|
45
46
|
Any,
|
|
46
47
|
Callable,
|
|
47
|
-
ContextManager,
|
|
48
48
|
Iterable,
|
|
49
|
+
Iterator,
|
|
49
50
|
Literal,
|
|
50
51
|
Protocol,
|
|
51
52
|
TypeGuard,
|
|
@@ -65,7 +66,7 @@ from rich.markup import escape
|
|
|
65
66
|
from rich.table import Table
|
|
66
67
|
from typer.core import TyperGroup
|
|
67
68
|
|
|
68
|
-
__version__ = "0.
|
|
69
|
+
__version__ = "0.13.0"
|
|
69
70
|
|
|
70
71
|
# Distribution names, for reporting versions via `dncli version`.
|
|
71
72
|
CLI_PACKAGE = "devnomads-cli"
|
|
@@ -694,14 +695,66 @@ def _mask(secret: str) -> str:
|
|
|
694
695
|
return f"{secret[:4]}...{secret[-4:]}"
|
|
695
696
|
|
|
696
697
|
|
|
697
|
-
|
|
698
|
+
_active_status: Any = None # the spinner `working()` is showing, if any
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@contextmanager
|
|
702
|
+
def working(message: str) -> Iterator[Any]:
|
|
698
703
|
"""Spinner on stderr while a request is in flight. Inert when stderr
|
|
699
704
|
is not a terminal (pipelines, CI, redirects), so output stays clean.
|
|
700
705
|
Never nest two of these: rich allows one live display at a time."""
|
|
701
706
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
707
|
+
global _active_status
|
|
708
|
+
if not err_console.is_terminal:
|
|
709
|
+
yield None
|
|
710
|
+
return
|
|
711
|
+
with err_console.status(message, spinner="dots") as status:
|
|
712
|
+
_active_status = status
|
|
713
|
+
try:
|
|
714
|
+
yield status
|
|
715
|
+
finally:
|
|
716
|
+
_active_status = None
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class _RetryCountdown:
|
|
720
|
+
"""Spinner text that counts down to the library's next retry; Rich
|
|
721
|
+
re-renders it on every refresh, so the seconds tick without a thread.
|
|
722
|
+
Once the wait is over it shows what is being retried."""
|
|
723
|
+
|
|
724
|
+
def __init__(self, status: int, path: str, delay: float) -> None:
|
|
725
|
+
self.status = status
|
|
726
|
+
self.path = path
|
|
727
|
+
self.until = time.monotonic() + delay
|
|
728
|
+
|
|
729
|
+
def __rich__(self) -> str:
|
|
730
|
+
left = math.ceil(self.until - time.monotonic())
|
|
731
|
+
if left > 0:
|
|
732
|
+
return f"got {self.status} from {escape(self.path)}, retrying in {left}s"
|
|
733
|
+
return f"retrying {escape(self.path)} (got {self.status})"
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
# The library logs this right before it sleeps out a 429/5xx backoff.
|
|
737
|
+
_RETRY_LOG_MESSAGE = "got %s from %s, retrying in %.0fs"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _retry_countdown_filter(record: logging.LogRecord) -> bool:
|
|
741
|
+
"""Turn the library's one-off retry warning into a live countdown on the
|
|
742
|
+
spinner. Without a spinner (not a terminal) the warning prints as is."""
|
|
743
|
+
|
|
744
|
+
status = _active_status
|
|
745
|
+
args = record.args
|
|
746
|
+
if status is None or record.msg != _RETRY_LOG_MESSAGE:
|
|
747
|
+
return True
|
|
748
|
+
if not isinstance(args, tuple) or len(args) != 3:
|
|
749
|
+
return True
|
|
750
|
+
code, path, delay = args
|
|
751
|
+
if not isinstance(code, int) or not isinstance(delay, (int, float)):
|
|
752
|
+
return True
|
|
753
|
+
status.update(_RetryCountdown(code, str(path), delay))
|
|
754
|
+
return False
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
logging.getLogger("devnomads.api").addFilter(_retry_countdown_filter)
|
|
705
758
|
|
|
706
759
|
|
|
707
760
|
# ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "devnomads-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
description = "Manage your DevNomads services from the command line"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -11,7 +11,7 @@ dependencies = [
|
|
|
11
11
|
"httpx>=0.27",
|
|
12
12
|
"rich>=13",
|
|
13
13
|
"cryptography>=42",
|
|
14
|
-
"devnomads[acme]>=0.6.
|
|
14
|
+
"devnomads[acme]>=0.6.2",
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
[project.scripts]
|
|
@@ -334,9 +334,61 @@ def test_flatten_row_without_nesting_is_unchanged():
|
|
|
334
334
|
|
|
335
335
|
|
|
336
336
|
def test_working_is_inert_without_a_terminal():
|
|
337
|
-
from contextlib import nullcontext
|
|
338
|
-
|
|
339
337
|
from dncli import working
|
|
340
338
|
|
|
341
339
|
# test stderr is not a terminal, so no live display may be created
|
|
342
|
-
|
|
340
|
+
with working("busy") as status:
|
|
341
|
+
assert status is None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_retry_warning_becomes_a_countdown_on_the_spinner(monkeypatch):
|
|
345
|
+
import logging
|
|
346
|
+
|
|
347
|
+
import dncli
|
|
348
|
+
|
|
349
|
+
class FakeStatus:
|
|
350
|
+
renderable = None
|
|
351
|
+
|
|
352
|
+
def update(self, renderable):
|
|
353
|
+
self.renderable = renderable
|
|
354
|
+
|
|
355
|
+
status = FakeStatus()
|
|
356
|
+
monkeypatch.setattr(dncli, "_active_status", status)
|
|
357
|
+
clock = [100.0]
|
|
358
|
+
monkeypatch.setattr(dncli.time, "monotonic", lambda: clock[0])
|
|
359
|
+
record = logging.LogRecord(
|
|
360
|
+
"devnomads.api",
|
|
361
|
+
logging.WARNING,
|
|
362
|
+
__file__,
|
|
363
|
+
0,
|
|
364
|
+
dncli._RETRY_LOG_MESSAGE,
|
|
365
|
+
(429, "/services/databases", 19.0),
|
|
366
|
+
None,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# swallowed: the spinner shows it instead of a one-off line
|
|
370
|
+
assert dncli._retry_countdown_filter(record) is False
|
|
371
|
+
countdown = status.renderable
|
|
372
|
+
assert countdown.__rich__() == ("got 429 from /services/databases, retrying in 19s")
|
|
373
|
+
clock[0] += 4.5
|
|
374
|
+
assert countdown.__rich__().endswith("retrying in 15s")
|
|
375
|
+
clock[0] += 15
|
|
376
|
+
assert countdown.__rich__() == "retrying /services/databases (got 429)"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_retry_warning_prints_as_is_without_a_spinner(monkeypatch):
|
|
380
|
+
import logging
|
|
381
|
+
|
|
382
|
+
import dncli
|
|
383
|
+
|
|
384
|
+
monkeypatch.setattr(dncli, "_active_status", None)
|
|
385
|
+
record = logging.LogRecord(
|
|
386
|
+
"devnomads.api",
|
|
387
|
+
logging.WARNING,
|
|
388
|
+
__file__,
|
|
389
|
+
0,
|
|
390
|
+
dncli._RETRY_LOG_MESSAGE,
|
|
391
|
+
(429, "/services", 3.0),
|
|
392
|
+
None,
|
|
393
|
+
)
|
|
394
|
+
assert dncli._retry_countdown_filter(record) is True
|
|
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
|