generated from coulomb/repo-seed
Add credits store, metering on create/destroy, extension routing resolver, metered SaaS stub extension, burst/saas profiles, credits CLI, docs, and tests.
48 lines
1.6 KiB
Python
48 lines
1.6 KiB
Python
"""Org/workspace credits for metered sandbox consumption."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
def _default_credits_path() -> Path:
|
|
base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
return base / "sandboxer" / "credits.json"
|
|
|
|
|
|
class CreditsStore:
|
|
def __init__(self, path: Path | None = None) -> None:
|
|
self.path = path or _default_credits_path()
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
def _read(self) -> dict:
|
|
if not self.path.exists():
|
|
default = float(os.environ.get("SANDBOXER_DEFAULT_CREDITS", "10.0"))
|
|
return {"balance_usd": default, "currency": "USD"}
|
|
return json.loads(self.path.read_text())
|
|
|
|
def _write(self, data: dict) -> None:
|
|
self.path.write_text(json.dumps(data, indent=2))
|
|
|
|
def balance(self) -> float:
|
|
return float(self._read().get("balance_usd", 0.0))
|
|
|
|
def can_afford(self, amount_usd: float) -> bool:
|
|
return self.balance() >= amount_usd
|
|
|
|
def add(self, amount_usd: float) -> float:
|
|
data = self._read()
|
|
data["balance_usd"] = round(float(data.get("balance_usd", 0.0)) + amount_usd, 4)
|
|
self._write(data)
|
|
return data["balance_usd"]
|
|
|
|
def debit(self, amount_usd: float) -> float:
|
|
data = self._read()
|
|
new_balance = round(float(data.get("balance_usd", 0.0)) - amount_usd, 4)
|
|
if new_balance < 0:
|
|
raise ValueError(f"Insufficient credits: need {amount_usd:.4f} USD")
|
|
data["balance_usd"] = new_balance
|
|
self._write(data)
|
|
return new_balance |