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.
Files changed (24) hide show
  1. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/PKG-INFO +1 -1
  2. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/cli.py +53 -14
  3. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/PKG-INFO +1 -1
  4. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/pyproject.toml +1 -1
  5. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/tests/test_cli.py +48 -33
  6. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/README.md +0 -0
  7. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/__init__.py +0 -0
  8. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/__main__.py +0 -0
  9. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/manifest.py +0 -0
  10. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo/sync.py +0 -0
  11. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/SOURCES.txt +0 -0
  12. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/dependency_links.txt +0 -0
  13. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/entry_points.txt +0 -0
  14. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/requires.txt +0 -0
  15. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/top_level.txt +0 -0
  16. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/__init__.py +0 -0
  17. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/config.py +0 -0
  18. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/crypto.py +0 -0
  19. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/identity.py +0 -0
  20. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/login.py +0 -0
  21. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/names.py +0 -0
  22. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/obsideo_core/storage.py +0 -0
  23. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/setup.cfg +0 -0
  24. {obsideo_cli-0.2.8 → obsideo_cli-0.2.9}/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.8
3
+ Version: 0.2.9
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
@@ -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
- print(" Plan: Free")
574
- # Prefer the signup service (it knows your quota); otherwise compute usage
575
- # straight from your storage so this always works - never nag to "log in".
576
- usage = _fetch_usage() if config.account_token() else None
577
- if usage:
578
- used, quota = usage["used_bytes"], usage["quota_bytes"]
579
- pct = usage.get("percent_used", (used / quota if quota else 0))
580
- print(f" Used: {_human(used)} / {_human(quota)} ({pct*100:.1f}%)")
581
- bar_len = 30
582
- filled = int(bar_len * min(pct, 1.0))
583
- print(f" [{'#'*filled}{'-'*(bar_len-filled)}]")
584
- if pct >= 0.8:
585
- print(" You're near your limit - reply to any Obsideo email to upgrade.")
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: obsideo-cli
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "obsideo-cli"
7
- version = "0.2.8"
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 -> account must compute usage from storage, NOT nag to log in.
302
- from obsideo import sync
303
- monkeypatch.setattr(cli.config, "account_token", lambda: None)
304
- monkeypatch.setattr(cli.storage, "total_usage", lambda: (1_500_000, 7))
305
- monkeypatch.setattr(cli.storage, "bucket", lambda: "tb")
306
- monkeypatch.setattr(sync, "_sync_dir", lambda: tmp_path / "s")
307
- cli.ObsideoShell().do_account("")
308
- out = capsys.readouterr().out
309
- assert "across 7 file" in out
310
- assert "obsideo login" not in out and "sign in" not in out.lower()
311
-
312
-
313
- def test_precmd_strips_obsideo_prefix():
314
- sh = cli.ObsideoShell()
315
- assert sh.precmd("obsideo ls") == "ls"
316
- assert sh.precmd("obsideo login") == "login"
317
- assert sh.precmd("ls") == "ls" # unchanged when no prefix
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