obsideo-cli 0.2.7__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.7 → obsideo_cli-0.2.9}/PKG-INFO +1 -1
  2. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo/cli.py +66 -17
  3. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/PKG-INFO +1 -1
  4. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_core/storage.py +25 -0
  5. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/pyproject.toml +1 -1
  6. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/tests/test_cli.py +48 -13
  7. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/README.md +0 -0
  8. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo/__init__.py +0 -0
  9. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo/__main__.py +0 -0
  10. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo/manifest.py +0 -0
  11. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo/sync.py +0 -0
  12. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/SOURCES.txt +0 -0
  13. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/dependency_links.txt +0 -0
  14. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/entry_points.txt +0 -0
  15. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/requires.txt +0 -0
  16. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_cli.egg-info/top_level.txt +0 -0
  17. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_core/__init__.py +0 -0
  18. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_core/config.py +0 -0
  19. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_core/crypto.py +0 -0
  20. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_core/identity.py +0 -0
  21. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_core/login.py +0 -0
  22. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/obsideo_core/names.py +0 -0
  23. {obsideo_cli-0.2.7 → obsideo_cli-0.2.9}/setup.cfg +0 -0
  24. {obsideo_cli-0.2.7 → 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.7
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
@@ -339,6 +341,14 @@ class ObsideoShell(cmd.Cmd):
339
341
  self._cwd = "" # S3 key prefix; "" = root
340
342
  self._refresh_prompt()
341
343
 
344
+ def precmd(self, line: str) -> str:
345
+ # Tolerate a leading "obsideo " typed out of habit inside the shell
346
+ # (e.g. "obsideo ls" -> "ls"), so it doesn't error with Unknown syntax.
347
+ stripped = line.lstrip()
348
+ if stripped.lower().startswith("obsideo "):
349
+ return stripped[len("obsideo "):]
350
+ return line
351
+
342
352
  # ── path helpers ────────────────────────────────────────────────────────
343
353
  def _refresh_prompt(self):
344
354
  self.prompt = f"obsideo:/{self._cwd} "
@@ -560,28 +570,41 @@ class ObsideoShell(cmd.Cmd):
560
570
  if not self._require_login():
561
571
  return
562
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)
563
577
  print()
564
578
  print(" -- Obsideo account --------------------------")
565
- print(" Plan: Free")
566
- if not config.account_token():
567
- # No shim token (e.g. creds set via env / pre-login account): we can't
568
- # query usage. Don't claim the service is down — tell them how to link it.
569
- print(" Usage: sign in with `obsideo login` to see usage details")
570
- else:
571
- usage = _fetch_usage()
572
- if usage:
573
- used, quota = usage["used_bytes"], usage["quota_bytes"]
574
- pct = usage.get("percent_used", (used / quota if quota else 0))
575
- print(f" Used: {_human(used)} / {_human(quota)} ({pct*100:.1f}%)")
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
576
586
  bar_len = 30
577
587
  filled = int(bar_len * min(pct, 1.0))
578
- print(f" [{'#'*filled}{'-'*(bar_len-filled)}]")
588
+ print(f" Used: {_human(used)} / {_human(quota)} ({pct*100:.1f}%)")
589
+ print(f" [{'#'*filled}{'-'*(bar_len-filled)}]")
579
590
  if pct >= 0.8:
580
- print(" You're near your limit - reply to any Obsideo email to upgrade.")
591
+ print(" Near your limit - reply to any Obsideo email to upgrade.")
581
592
  else:
582
- print(" Usage: couldn't reach the account service - try again shortly")
583
- print(f" Files: bucket '{storage.bucket()}' · sync folder {sync_mod.ensure_sync_dir()}")
584
- print(f" Keys: {config.CONFIG_DIR} (back up data.key)")
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)")
598
+ else:
599
+ print(" Plan: Free")
600
+ try:
601
+ used, n = storage.total_usage()
602
+ print(f" Used: {_human(used)} across {n} file(s)")
603
+ except Exception:
604
+ print(" Used: (couldn't read storage just now)")
605
+ print(f" Bucket: {storage.bucket()}")
606
+ print(f" Sync folder: {sync_mod.ensure_sync_dir()}")
607
+ print(f" Keys: {config.CONFIG_DIR} (back up data.key)")
585
608
  print(" ---------------------------------------------")
586
609
  print()
587
610
 
@@ -732,6 +755,32 @@ def _fetch_usage() -> dict | None:
732
755
  return None
733
756
 
734
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
+
735
784
  def main():
736
785
  argv = sys.argv[1:]
737
786
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: obsideo-cli
3
- Version: 0.2.7
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
@@ -189,6 +189,31 @@ def exists(key: str) -> bool:
189
189
  return head(key) is not None
190
190
 
191
191
 
192
+ def total_usage() -> tuple[int, int]:
193
+ """Total stored bytes + object count across the account's bucket (flat list).
194
+ Lets `account` show real usage without the signup-service token — it just reads
195
+ the storage the account can already see. Names stay opaque; only sizes summed."""
196
+ s3 = _s3()
197
+ total = 0
198
+ count = 0
199
+ token = None
200
+ while True:
201
+ kwargs = dict(Bucket=bucket())
202
+ if token:
203
+ kwargs["ContinuationToken"] = token
204
+ resp = s3.list_objects_v2(**kwargs)
205
+ for obj in resp.get("Contents", []):
206
+ if obj["Key"].endswith("/"):
207
+ continue # folder marker, not a real object
208
+ total += obj.get("Size", 0)
209
+ count += 1
210
+ if resp.get("IsTruncated"):
211
+ token = resp.get("NextContinuationToken")
212
+ else:
213
+ break
214
+ return total, count
215
+
216
+
192
217
  def list_prefix(prefix: str = "", delimiter: str = "/") -> dict:
193
218
  """List one VFS level. Returns {'folders': [name...], 'files': [{name,key,size}]}.
194
219
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "obsideo-cli"
7
- version = "0.2.7"
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,16 +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()
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