obsideo-cli 0.2.0__tar.gz → 0.2.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.
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/PKG-INFO +2 -1
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo/cli.py +128 -5
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_cli.egg-info/PKG-INFO +2 -1
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_cli.egg-info/requires.txt +1 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_core/config.py +19 -4
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_core/login.py +1 -1
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/pyproject.toml +2 -1
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/tests/test_cli.py +77 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/README.md +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo/__init__.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo/__main__.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo/manifest.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo/sync.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_cli.egg-info/SOURCES.txt +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_cli.egg-info/dependency_links.txt +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_cli.egg-info/entry_points.txt +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_cli.egg-info/top_level.txt +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_core/__init__.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_core/crypto.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_core/identity.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_core/names.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/obsideo_core/storage.py +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/setup.cfg +0 -0
- {obsideo_cli-0.2.0 → obsideo_cli-0.2.2}/tests/test_core.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: obsideo-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Obsideo Cloud - encrypted storage we can't read. Save, browse, and sync whatever you want, from your terminal.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://obsideo.io
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.10
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: boto3>=1.28
|
|
10
10
|
Requires-Dist: cryptography>=41.0
|
|
11
|
+
Requires-Dist: certifi
|
|
11
12
|
|
|
12
13
|
# obsideo-cli
|
|
13
14
|
|
|
@@ -11,6 +11,7 @@ leaves, so Obsideo can't read it. An interactive shell plus one-shot commands.
|
|
|
11
11
|
import cmd
|
|
12
12
|
import os
|
|
13
13
|
import shlex
|
|
14
|
+
import subprocess
|
|
14
15
|
import sys
|
|
15
16
|
import urllib.error
|
|
16
17
|
import urllib.request
|
|
@@ -88,7 +89,7 @@ def show_notices() -> None:
|
|
|
88
89
|
f"{config.signup_url()}/v1/notices",
|
|
89
90
|
headers={"User-Agent": config.USER_AGENT},
|
|
90
91
|
)
|
|
91
|
-
with urllib.request.urlopen(req, timeout=4) as resp:
|
|
92
|
+
with urllib.request.urlopen(req, timeout=4, context=config.ssl_context()) as resp:
|
|
92
93
|
notices = json.loads(resp.read().decode()).get("notices", [])
|
|
93
94
|
except Exception:
|
|
94
95
|
return
|
|
@@ -106,6 +107,116 @@ def show_notices() -> None:
|
|
|
106
107
|
_mark_seen(shown)
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
# ── Branding banner + version self-check ──────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
_BANNER = r"""
|
|
113
|
+
/\
|
|
114
|
+
/ \ OBSIDEO DRIVE
|
|
115
|
+
\ / encrypted storage not even we can read
|
|
116
|
+
\/
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
_BANNER_SHOWN = False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _chrome_enabled() -> bool:
|
|
123
|
+
"""Banner/prompt are human chrome: only on an interactive stdout, and never
|
|
124
|
+
when OBSIDEO_NO_BANNER / NO_COLOR is set. Keeps stdout clean for agents/pipes."""
|
|
125
|
+
if os.environ.get("OBSIDEO_NO_BANNER") or os.environ.get("NO_COLOR"):
|
|
126
|
+
return False
|
|
127
|
+
return sys.stdout.isatty()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def show_banner() -> None:
|
|
131
|
+
"""Branded ASCII banner to stderr, once per process, TTY-gated."""
|
|
132
|
+
global _BANNER_SHOWN
|
|
133
|
+
if _BANNER_SHOWN or not _chrome_enabled():
|
|
134
|
+
return
|
|
135
|
+
_BANNER_SHOWN = True
|
|
136
|
+
print(f"\033[36m{_BANNER}\033[0m", file=sys.stderr)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _usage_bar(pct: float, cells: int = 10) -> str:
|
|
140
|
+
filled = min(cells, max(0, round(pct * cells)))
|
|
141
|
+
return "#" * filled + "-" * (cells - filled)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def show_status() -> None:
|
|
145
|
+
"""One-line account status (tier · usage bar · upgrade hint) to stderr, TTY-gated.
|
|
146
|
+
Makes a network call, so it's shown at session start / post-login only."""
|
|
147
|
+
if not _chrome_enabled() or not config.is_logged_in():
|
|
148
|
+
return
|
|
149
|
+
usage = _fetch_usage()
|
|
150
|
+
if not usage:
|
|
151
|
+
return
|
|
152
|
+
used, quota = usage.get("used_bytes", 0), usage.get("quota_bytes", 0)
|
|
153
|
+
pct = usage.get("percent_used")
|
|
154
|
+
if pct is None:
|
|
155
|
+
pct = (used / quota) if quota else 0.0
|
|
156
|
+
hint = " · \033[36mupgrade\033[0m for more" if pct >= 0.8 else ""
|
|
157
|
+
print(f"\033[2mFree\033[0m [{_usage_bar(pct)}] {_human(used)} of {_human(quota)}{hint}",
|
|
158
|
+
file=sys.stderr)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _parse_version(v: str) -> tuple:
|
|
162
|
+
"""Lenient dotted-version → comparable int tuple. '0.2.10' > '0.2.9'."""
|
|
163
|
+
out = []
|
|
164
|
+
for part in v.split("."):
|
|
165
|
+
digits = ""
|
|
166
|
+
for ch in part:
|
|
167
|
+
if ch.isdigit():
|
|
168
|
+
digits += ch
|
|
169
|
+
else:
|
|
170
|
+
break
|
|
171
|
+
out.append(int(digits) if digits else 0)
|
|
172
|
+
return tuple(out)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _latest_pypi_version() -> str | None:
|
|
176
|
+
try:
|
|
177
|
+
req = urllib.request.Request(
|
|
178
|
+
f"https://pypi.org/pypi/{config.PACKAGE}/json",
|
|
179
|
+
headers={"User-Agent": config.USER_AGENT},
|
|
180
|
+
)
|
|
181
|
+
with urllib.request.urlopen(req, timeout=5, context=config.ssl_context()) as resp:
|
|
182
|
+
return json.loads(resp.read().decode())["info"]["version"]
|
|
183
|
+
except Exception:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def check_for_update() -> None:
|
|
188
|
+
"""On interactive init: if PyPI has a newer obsideo-cli, offer to update now.
|
|
189
|
+
TTY-gated (never prompts agents/scripts), fail-silent (a network hiccup never
|
|
190
|
+
blocks startup). Disable with OBSIDEO_NO_UPDATE_CHECK."""
|
|
191
|
+
if not sys.stdout.isatty() or os.environ.get("OBSIDEO_NO_UPDATE_CHECK"):
|
|
192
|
+
return
|
|
193
|
+
current = config.VERSION
|
|
194
|
+
latest = _latest_pypi_version()
|
|
195
|
+
if not latest or _parse_version(latest) <= _parse_version(current):
|
|
196
|
+
return
|
|
197
|
+
print(f"\n\033[36mUpdate available: {current} -> {latest}\033[0m", file=sys.stderr)
|
|
198
|
+
try:
|
|
199
|
+
ans = input("Update now? [Y/n]: ").strip().lower()
|
|
200
|
+
except (EOFError, KeyboardInterrupt):
|
|
201
|
+
print(file=sys.stderr)
|
|
202
|
+
return
|
|
203
|
+
if ans not in ("", "y", "yes"):
|
|
204
|
+
print(f"Skipped. Update later with: pip install -U {config.PACKAGE}", file=sys.stderr)
|
|
205
|
+
return
|
|
206
|
+
print(f"Updating to {latest}...", file=sys.stderr)
|
|
207
|
+
try:
|
|
208
|
+
rc = subprocess.run(
|
|
209
|
+
[sys.executable, "-m", "pip", "install", "-U", config.PACKAGE]
|
|
210
|
+
).returncode
|
|
211
|
+
except Exception as e:
|
|
212
|
+
print(f"Update failed: {e}\nTry manually: pip install -U {config.PACKAGE}", file=sys.stderr)
|
|
213
|
+
return
|
|
214
|
+
if rc == 0:
|
|
215
|
+
print(f"\nUpdated to {latest}. Restart `obsideo` to use the new version.", file=sys.stderr)
|
|
216
|
+
sys.exit(0)
|
|
217
|
+
print(f"Update didn't complete. Try: pip install -U {config.PACKAGE}", file=sys.stderr)
|
|
218
|
+
|
|
219
|
+
|
|
109
220
|
# ── Operator tooling: broadcast a message to all users ────────────────────────
|
|
110
221
|
|
|
111
222
|
def run_admin(argv: list) -> int:
|
|
@@ -148,7 +259,7 @@ def run_admin(argv: list) -> int:
|
|
|
148
259
|
method="POST",
|
|
149
260
|
)
|
|
150
261
|
try:
|
|
151
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
262
|
+
with urllib.request.urlopen(req, timeout=15, context=config.ssl_context()) as resp:
|
|
152
263
|
out = json.loads(resp.read().decode())
|
|
153
264
|
except urllib.error.HTTPError as e:
|
|
154
265
|
detail = e.read().decode()[:200]
|
|
@@ -482,7 +593,7 @@ class ObsideoShell(cmd.Cmd):
|
|
|
482
593
|
if parts[0] == "set" and len(parts) == 3:
|
|
483
594
|
key, value = parts[1], parts[2]
|
|
484
595
|
cfg = config.load_config()
|
|
485
|
-
if key
|
|
596
|
+
if key in ("encrypt", "encrypt_names"):
|
|
486
597
|
value = value.lower() in ("true", "1", "yes", "on")
|
|
487
598
|
cfg[key] = value
|
|
488
599
|
config.save_config(cfg)
|
|
@@ -515,7 +626,7 @@ def _fetch_usage() -> dict | None:
|
|
|
515
626
|
f"{config.signup_url()}/v1/account/usage",
|
|
516
627
|
headers={"Authorization": f"Bearer {token}", "User-Agent": config.USER_AGENT},
|
|
517
628
|
)
|
|
518
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
629
|
+
with urllib.request.urlopen(req, timeout=15, context=config.ssl_context()) as resp:
|
|
519
630
|
return json.loads(resp.read().decode())
|
|
520
631
|
except Exception:
|
|
521
632
|
return None
|
|
@@ -524,6 +635,11 @@ def _fetch_usage() -> dict | None:
|
|
|
524
635
|
def main():
|
|
525
636
|
argv = sys.argv[1:]
|
|
526
637
|
|
|
638
|
+
# Branded banner on every init (stderr, TTY-gated). Skip for `admin` so
|
|
639
|
+
# operator tooling output stays clean.
|
|
640
|
+
if not (argv and argv[0] == "admin"):
|
|
641
|
+
show_banner()
|
|
642
|
+
|
|
527
643
|
# Standard --help / -h (cmd.Cmd would otherwise read "--help" as a command).
|
|
528
644
|
if argv and argv[0] in ("-h", "--help", "help"):
|
|
529
645
|
ObsideoShell().onecmd("help")
|
|
@@ -532,6 +648,8 @@ def main():
|
|
|
532
648
|
# `obsideo login` is interactive and handled specially.
|
|
533
649
|
if argv and argv[0] == "login":
|
|
534
650
|
ok = run_login()
|
|
651
|
+
if ok:
|
|
652
|
+
show_status()
|
|
535
653
|
sys.exit(0 if ok else 1)
|
|
536
654
|
|
|
537
655
|
# `obsideo admin ...` is operator tooling, not a shell command.
|
|
@@ -543,11 +661,15 @@ def main():
|
|
|
543
661
|
|
|
544
662
|
shell = ObsideoShell()
|
|
545
663
|
|
|
546
|
-
# One-shot: `obsideo ls`, `obsideo put file.txt`, etc.
|
|
664
|
+
# One-shot: `obsideo ls`, `obsideo put file.txt`, etc. No update prompt or
|
|
665
|
+
# status line here — one-shots stay fast and scriptable.
|
|
547
666
|
if argv:
|
|
548
667
|
shell.onecmd(" ".join(argv))
|
|
549
668
|
return
|
|
550
669
|
|
|
670
|
+
# Interactive session ("initialization"): offer an update if one's out.
|
|
671
|
+
check_for_update()
|
|
672
|
+
|
|
551
673
|
# First-run nudge: not logged in -> offer login.
|
|
552
674
|
if not config.is_logged_in():
|
|
553
675
|
print("Welcome to Obsideo - encrypted storage we can't read.")
|
|
@@ -558,6 +680,7 @@ def main():
|
|
|
558
680
|
print("Run 'obsideo login' when you're ready.")
|
|
559
681
|
return
|
|
560
682
|
|
|
683
|
+
show_status()
|
|
561
684
|
try:
|
|
562
685
|
shell.cmdloop()
|
|
563
686
|
except KeyboardInterrupt:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: obsideo-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Obsideo Cloud - encrypted storage we can't read. Save, browse, and sync whatever you want, from your terminal.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://obsideo.io
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.10
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: boto3>=1.28
|
|
10
10
|
Requires-Dist: cryptography>=41.0
|
|
11
|
+
Requires-Dist: certifi
|
|
11
12
|
|
|
12
13
|
# obsideo-cli
|
|
13
14
|
|
|
@@ -28,16 +28,31 @@ _DEFAULT_REGION = "us-east-1"
|
|
|
28
28
|
# Sent on every request to the signup shim. A descriptive User-Agent avoids
|
|
29
29
|
# Cloudflare's default-`Python-urllib` bot block (HTTP 403 / error 1010) that
|
|
30
30
|
# fronts signup.obsideo.io; without it, `obsideo login` and usage lookups fail.
|
|
31
|
+
PACKAGE = "obsideo-cli" # PyPI distribution name (used for version + update checks)
|
|
31
32
|
try:
|
|
32
33
|
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
33
34
|
try:
|
|
34
|
-
_VERSION = _pkg_version(
|
|
35
|
+
_VERSION = _pkg_version(PACKAGE)
|
|
35
36
|
except PackageNotFoundError:
|
|
36
|
-
_VERSION = "0.2.
|
|
37
|
+
_VERSION = "0.2.1"
|
|
37
38
|
except Exception:
|
|
38
|
-
_VERSION = "0.2.
|
|
39
|
+
_VERSION = "0.2.1"
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
VERSION = _VERSION
|
|
42
|
+
USER_AGENT = f"obsideo-cli/{_VERSION}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ssl_context():
|
|
46
|
+
"""An SSL context that verifies against certifi's CA bundle. urllib otherwise
|
|
47
|
+
trusts the OS certificate store, which is often incomplete on fresh/locked-down
|
|
48
|
+
Windows installs (CERTIFICATE_VERIFY_FAILED on a perfectly valid cert). Falls
|
|
49
|
+
back to the system default if certifi isn't importable."""
|
|
50
|
+
import ssl
|
|
51
|
+
try:
|
|
52
|
+
import certifi
|
|
53
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
54
|
+
except Exception:
|
|
55
|
+
return ssl.create_default_context()
|
|
41
56
|
|
|
42
57
|
|
|
43
58
|
def write_secret_file(path: Path, value: str) -> None:
|
|
@@ -24,7 +24,7 @@ def _post_json(url: str, payload: dict) -> dict:
|
|
|
24
24
|
method="POST",
|
|
25
25
|
)
|
|
26
26
|
try:
|
|
27
|
-
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
27
|
+
with urllib.request.urlopen(req, timeout=30, context=config.ssl_context()) as resp:
|
|
28
28
|
return json.loads(resp.read().decode())
|
|
29
29
|
except urllib.error.HTTPError as e:
|
|
30
30
|
try:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "obsideo-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Obsideo Cloud - encrypted storage we can't read. Save, browse, and sync whatever you want, from your terminal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -12,6 +12,7 @@ license = {text = "MIT"}
|
|
|
12
12
|
dependencies = [
|
|
13
13
|
"boto3>=1.28",
|
|
14
14
|
"cryptography>=41.0",
|
|
15
|
+
"certifi",
|
|
15
16
|
]
|
|
16
17
|
|
|
17
18
|
[project.urls]
|
|
@@ -151,3 +151,80 @@ def test_put_folder_into_cwd(shell, tmp_path):
|
|
|
151
151
|
sh.do_cd("backup")
|
|
152
152
|
sh.do_put(str(folder))
|
|
153
153
|
assert "backup/docs/x.txt" in fake.objs
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── Banner + update check (0.2.1) ─────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def test_banner_tty_gated_once(monkeypatch, capsys):
|
|
159
|
+
monkeypatch.setattr(cli.sys.stdout, "isatty", lambda: True, raising=False)
|
|
160
|
+
monkeypatch.delenv("OBSIDEO_NO_BANNER", raising=False)
|
|
161
|
+
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
162
|
+
cli._BANNER_SHOWN = False
|
|
163
|
+
cli.show_banner()
|
|
164
|
+
assert "OBSIDEO DRIVE" in capsys.readouterr().err
|
|
165
|
+
cli.show_banner() # once per process
|
|
166
|
+
assert capsys.readouterr().err == ""
|
|
167
|
+
# non-tty: silent
|
|
168
|
+
monkeypatch.setattr(cli.sys.stdout, "isatty", lambda: False, raising=False)
|
|
169
|
+
cli._BANNER_SHOWN = False
|
|
170
|
+
cli.show_banner()
|
|
171
|
+
assert capsys.readouterr().err == ""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_parse_version_ordering():
|
|
175
|
+
assert cli._parse_version("0.2.10") > cli._parse_version("0.2.9")
|
|
176
|
+
assert cli._parse_version("0.3.0") > cli._parse_version("0.2.99")
|
|
177
|
+
assert cli._parse_version("0.2.1") == cli._parse_version("0.2.1")
|
|
178
|
+
assert not (cli._parse_version("0.2.0") > cli._parse_version("0.2.1"))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_update_check_silent_when_not_tty(monkeypatch, capsys):
|
|
182
|
+
monkeypatch.setattr(cli.sys.stdout, "isatty", lambda: False, raising=False)
|
|
183
|
+
called = []
|
|
184
|
+
monkeypatch.setattr(cli, "_latest_pypi_version", lambda: called.append(1) or "9.9.9")
|
|
185
|
+
cli.check_for_update()
|
|
186
|
+
assert capsys.readouterr().err == ""
|
|
187
|
+
assert called == [] # short-circuits before any network call
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_update_check_noop_when_current(monkeypatch, capsys):
|
|
191
|
+
monkeypatch.setattr(cli.sys.stdout, "isatty", lambda: True, raising=False)
|
|
192
|
+
monkeypatch.delenv("OBSIDEO_NO_UPDATE_CHECK", raising=False)
|
|
193
|
+
monkeypatch.setattr(cli.config, "VERSION", "0.2.1")
|
|
194
|
+
monkeypatch.setattr(cli, "_latest_pypi_version", lambda: "0.2.1") # not newer
|
|
195
|
+
asked = []
|
|
196
|
+
monkeypatch.setattr("builtins.input", lambda *a: asked.append(1) or "y")
|
|
197
|
+
cli.check_for_update()
|
|
198
|
+
assert asked == [] # never prompts when already current
|
|
199
|
+
assert "Update available" not in capsys.readouterr().err
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_update_check_prompts_and_respects_no(monkeypatch, capsys):
|
|
203
|
+
monkeypatch.setattr(cli.sys.stdout, "isatty", lambda: True, raising=False)
|
|
204
|
+
monkeypatch.delenv("OBSIDEO_NO_UPDATE_CHECK", raising=False)
|
|
205
|
+
monkeypatch.setattr(cli.config, "VERSION", "0.2.1")
|
|
206
|
+
monkeypatch.setattr(cli, "_latest_pypi_version", lambda: "0.2.2")
|
|
207
|
+
monkeypatch.setattr("builtins.input", lambda *a: "n")
|
|
208
|
+
ran = []
|
|
209
|
+
monkeypatch.setattr(cli.subprocess, "run", lambda *a, **k: ran.append(a))
|
|
210
|
+
cli.check_for_update()
|
|
211
|
+
err = capsys.readouterr().err
|
|
212
|
+
assert "Update available: 0.2.1 -> 0.2.2" in err
|
|
213
|
+
assert ran == [] # declined -> no pip invocation
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_update_check_runs_pip_on_yes(monkeypatch, capsys):
|
|
217
|
+
monkeypatch.setattr(cli.sys.stdout, "isatty", lambda: True, raising=False)
|
|
218
|
+
monkeypatch.delenv("OBSIDEO_NO_UPDATE_CHECK", raising=False)
|
|
219
|
+
monkeypatch.setattr(cli.config, "VERSION", "0.2.1")
|
|
220
|
+
monkeypatch.setattr(cli, "_latest_pypi_version", lambda: "0.2.2")
|
|
221
|
+
monkeypatch.setattr("builtins.input", lambda *a: "y")
|
|
222
|
+
|
|
223
|
+
class _R:
|
|
224
|
+
returncode = 0
|
|
225
|
+
calls = []
|
|
226
|
+
monkeypatch.setattr(cli.subprocess, "run", lambda *a, **k: calls.append(a[0]) or _R())
|
|
227
|
+
with pytest.raises(SystemExit) as e:
|
|
228
|
+
cli.check_for_update()
|
|
229
|
+
assert e.value.code == 0
|
|
230
|
+
assert calls and calls[0][1:] == ["-m", "pip", "install", "-U", "obsideo-cli"]
|
|
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
|