obsideo-cli 0.2.8__tar.gz → 0.2.9__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.8 → obsideo_cli-0.2.9}/PKG-INFO +1 -1
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/cli.py +53 -14
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/PKG-INFO +1 -1
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/pyproject.toml +1 -1
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/tests/test_cli.py +48 -33
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/README.md +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/__init__.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/__main__.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/manifest.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/sync.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/SOURCES.txt +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/dependency_links.txt +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/entry_points.txt +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/requires.txt +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/top_level.txt +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/__init__.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/config.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/crypto.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/identity.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/login.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/names.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/storage.py +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/setup.cfg +0 -0
- {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/tests/test_core.py +0 -0
|
@@ -146,10 +146,12 @@ def show_status() -> None:
|
|
|
146
146
|
Makes a network call, so it's shown at session start / post-login only."""
|
|
147
147
|
if not _chrome_enabled() or not config.is_logged_in():
|
|
148
148
|
return
|
|
149
|
-
usage = _fetch_usage()
|
|
149
|
+
usage = _fetch_account_info() or _fetch_usage()
|
|
150
150
|
if not usage:
|
|
151
151
|
return
|
|
152
152
|
used, quota = usage.get("used_bytes", 0), usage.get("quota_bytes", 0)
|
|
153
|
+
if not quota:
|
|
154
|
+
return # no quota to show a bar against; account command shows raw usage
|
|
153
155
|
pct = usage.get("percent_used")
|
|
154
156
|
if pct is None:
|
|
155
157
|
pct = (used / quota) if quota else 0.0
|
|
@@ -568,22 +570,33 @@ class ObsideoShell(cmd.Cmd):
|
|
|
568
570
|
if not self._require_login():
|
|
569
571
|
return
|
|
570
572
|
from obsideo import sync as sync_mod
|
|
573
|
+
# Usage + quota for any account via the gateway (works with just the S3
|
|
574
|
+
# creds); fall back to the signup-service token, then to a storage-only
|
|
575
|
+
# count - so this always shows something useful and never nags to log in.
|
|
576
|
+
info = _fetch_account_info() or (_fetch_usage() if config.account_token() else None)
|
|
571
577
|
print()
|
|
572
578
|
print(" -- Obsideo account --------------------------")
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
579
|
+
if info:
|
|
580
|
+
tier = (info.get("tier") or "free").replace("testdrive", "Free").title()
|
|
581
|
+
print(f" Plan: {tier}")
|
|
582
|
+
used = info.get("used_bytes", 0)
|
|
583
|
+
quota = info.get("quota_bytes", 0)
|
|
584
|
+
if quota:
|
|
585
|
+
pct = used / quota
|
|
586
|
+
bar_len = 30
|
|
587
|
+
filled = int(bar_len * min(pct, 1.0))
|
|
588
|
+
print(f" Used: {_human(used)} / {_human(quota)} ({pct*100:.1f}%)")
|
|
589
|
+
print(f" [{'#'*filled}{'-'*(bar_len-filled)}]")
|
|
590
|
+
if pct >= 0.8:
|
|
591
|
+
print(" Near your limit - reply to any Obsideo email to upgrade.")
|
|
592
|
+
else:
|
|
593
|
+
print(f" Used: {_human(used)}")
|
|
594
|
+
if info.get("object_count"):
|
|
595
|
+
print(f" Files: {info['object_count']} object(s)")
|
|
596
|
+
if info.get("days_remaining"):
|
|
597
|
+
print(f" Renews/expires in {info['days_remaining']} day(s)")
|
|
586
598
|
else:
|
|
599
|
+
print(" Plan: Free")
|
|
587
600
|
try:
|
|
588
601
|
used, n = storage.total_usage()
|
|
589
602
|
print(f" Used: {_human(used)} across {n} file(s)")
|
|
@@ -742,6 +755,32 @@ def _fetch_usage() -> dict | None:
|
|
|
742
755
|
return None
|
|
743
756
|
|
|
744
757
|
|
|
758
|
+
def _fetch_account_info() -> dict | None:
|
|
759
|
+
"""Usage + quota for the calling account via the gateway's SigV4-authed
|
|
760
|
+
/v1/account. Works for ANY account using only the S3 creds we already have
|
|
761
|
+
(no signup-service token needed). Returns {used_bytes, quota_bytes, tier,
|
|
762
|
+
object_count, days_remaining, ...} or None if unavailable (e.g. the gateway
|
|
763
|
+
endpoint isn't deployed yet, or no creds) — callers fall back gracefully."""
|
|
764
|
+
ak = os.environ.get("OBSIDEO_S3_ACCESS_KEY")
|
|
765
|
+
sk = os.environ.get("OBSIDEO_S3_SECRET_KEY")
|
|
766
|
+
if not (ak and sk):
|
|
767
|
+
return None
|
|
768
|
+
try:
|
|
769
|
+
from botocore.auth import SigV4Auth
|
|
770
|
+
from botocore.awsrequest import AWSRequest
|
|
771
|
+
from botocore.credentials import Credentials
|
|
772
|
+
endpoint = os.environ.get("OBSIDEO_S3_ENDPOINT", "https://s3.obsideo.io").rstrip("/")
|
|
773
|
+
region = os.environ.get("OBSIDEO_S3_REGION", "us-east-1")
|
|
774
|
+
url = f"{endpoint}/v1/account"
|
|
775
|
+
signed = AWSRequest(method="GET", url=url)
|
|
776
|
+
SigV4Auth(Credentials(ak, sk), "s3", region).add_auth(signed)
|
|
777
|
+
req = urllib.request.Request(url, headers=dict(signed.headers), method="GET")
|
|
778
|
+
with urllib.request.urlopen(req, timeout=15, context=config.ssl_context()) as resp:
|
|
779
|
+
return json.loads(resp.read().decode())
|
|
780
|
+
except Exception:
|
|
781
|
+
return None
|
|
782
|
+
|
|
783
|
+
|
|
745
784
|
def main():
|
|
746
785
|
argv = sys.argv[1:]
|
|
747
786
|
|
|
@@ -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.9"
|
|
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"
|
|
@@ -282,36 +282,51 @@ def test_messages_handles_unreachable(monkeypatch, capsys):
|
|
|
282
282
|
monkeypatch.setattr(cli.urllib.request, "urlopen", boom)
|
|
283
283
|
cli.ObsideoShell().do_messages("")
|
|
284
284
|
assert "couldn't reach" in capsys.readouterr().out.lower()
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def test_sync_readme_created_and_not_pushed(tmp_path, monkeypatch, capsys):
|
|
288
|
-
from obsideo import sync
|
|
289
|
-
sd = tmp_path / "obsideo-sync"
|
|
290
|
-
monkeypatch.setattr(sync, "_sync_dir", lambda: sd)
|
|
291
|
-
sync.ensure_sync_dir()
|
|
292
|
-
readme = sd / sync.README_NAME
|
|
293
|
-
assert readme.exists() and "sync push" in readme.read_text() # guide dropped in
|
|
294
|
-
# A folder containing ONLY the README is treated as empty (README never uploads).
|
|
295
|
-
n = sync.push(verbose=True)
|
|
296
|
-
assert n == 0
|
|
297
|
-
assert "empty" in capsys.readouterr().out.lower()
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def test_account_computes_usage_without_token(monkeypatch, tmp_path, capsys):
|
|
301
|
-
# No signup token ->
|
|
302
|
-
from obsideo import sync
|
|
303
|
-
monkeypatch.setattr(cli
|
|
304
|
-
monkeypatch.setattr(cli.
|
|
305
|
-
monkeypatch.setattr(cli.storage, "
|
|
306
|
-
monkeypatch.setattr(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
assert "
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_sync_readme_created_and_not_pushed(tmp_path, monkeypatch, capsys):
|
|
288
|
+
from obsideo import sync
|
|
289
|
+
sd = tmp_path / "obsideo-sync"
|
|
290
|
+
monkeypatch.setattr(sync, "_sync_dir", lambda: sd)
|
|
291
|
+
sync.ensure_sync_dir()
|
|
292
|
+
readme = sd / sync.README_NAME
|
|
293
|
+
assert readme.exists() and "sync push" in readme.read_text() # guide dropped in
|
|
294
|
+
# A folder containing ONLY the README is treated as empty (README never uploads).
|
|
295
|
+
n = sync.push(verbose=True)
|
|
296
|
+
assert n == 0
|
|
297
|
+
assert "empty" in capsys.readouterr().out.lower()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_account_computes_usage_without_token(monkeypatch, tmp_path, capsys):
|
|
301
|
+
# No gateway info + no signup token -> compute from storage, NOT nag to log in.
|
|
302
|
+
from obsideo import sync
|
|
303
|
+
monkeypatch.setattr(cli, "_fetch_account_info", lambda: None) # don't hit network
|
|
304
|
+
monkeypatch.setattr(cli.config, "account_token", lambda: None)
|
|
305
|
+
monkeypatch.setattr(cli.storage, "total_usage", lambda: (1_500_000, 7))
|
|
306
|
+
monkeypatch.setattr(cli.storage, "bucket", lambda: "tb")
|
|
307
|
+
monkeypatch.setattr(sync, "_sync_dir", lambda: tmp_path / "s")
|
|
308
|
+
cli.ObsideoShell().do_account("")
|
|
309
|
+
out = capsys.readouterr().out
|
|
310
|
+
assert "across 7 file" in out
|
|
311
|
+
assert "obsideo login" not in out and "sign in" not in out.lower()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_account_shows_percentage_from_gateway(monkeypatch, tmp_path, capsys):
|
|
315
|
+
# When the gateway returns quota, account shows used / quota / % + a bar.
|
|
316
|
+
from obsideo import sync
|
|
317
|
+
monkeypatch.setattr(cli, "_fetch_account_info",
|
|
318
|
+
lambda: {"tier": "testdrive", "used_bytes": 500_000_000,
|
|
319
|
+
"quota_bytes": 5_368_709_120, "object_count": 314})
|
|
320
|
+
monkeypatch.setattr(cli.storage, "bucket", lambda: "obsideo")
|
|
321
|
+
monkeypatch.setattr(sync, "_sync_dir", lambda: tmp_path / "s")
|
|
322
|
+
cli.ObsideoShell().do_account("")
|
|
323
|
+
out = capsys.readouterr().out
|
|
324
|
+
assert "/" in out and "%" in out and "[" in out # used / quota (pct) + bar
|
|
325
|
+
assert "314 object" in out and "Free" in out # testdrive shown as Free
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_precmd_strips_obsideo_prefix():
|
|
329
|
+
sh = cli.ObsideoShell()
|
|
330
|
+
assert sh.precmd("obsideo ls") == "ls"
|
|
331
|
+
assert sh.precmd("obsideo login") == "login"
|
|
332
|
+
assert sh.precmd("ls") == "ls" # unchanged when no prefix
|
|
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
|