Download main.py from techindro/papaya: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/techindro/papaya/resolve/main/main.py
- Command line
-
hf download hf://spaces/techindro/papaya/main.py
-
curl -L -o main.py https://huggingface.co/spaces/techindro/papaya/resolve/main/main.py
14.6 kB
| """Papaya backend (FastAPI). | |
| Flow: telemetry -> validation -> pricing -> OAuth token -> PayPal Sandbox | |
| order -> capture -> log. | |
| Run from the project root: | |
| uvicorn backend.main:app --reload | |
| """ | |
| from __future__ import annotations | |
| import hmac | |
| import threading | |
| import time | |
| import uuid | |
| from collections import deque | |
| from datetime import datetime, timezone | |
| from decimal import ROUND_HALF_UP, Decimal | |
| from typing import Any | |
| import numpy as np | |
| import psutil | |
| import requests | |
| from fastapi import Depends, FastAPI, Header, HTTPException, Query | |
| from pydantic import BaseModel, Field | |
| from backend import config | |
| app = FastAPI( | |
| title="Papaya", | |
| description="Automated payment layer for servers and AI nodes (PayPal Sandbox).", | |
| version="1.0.0", | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Data models (Pydantic rejects broken or unexpected input) | |
| # --------------------------------------------------------------------------- | |
| class TelemetryPayload(BaseModel): | |
| """Workload reported by one node for one billing window.""" | |
| model_config = {"extra": "forbid"} | |
| node_id: str = Field(..., pattern=r"^[A-Za-z0-9_.-]{1,64}$") | |
| cpu_percent: float = Field(..., ge=0, le=100) | |
| tokens: int = Field(0, ge=0, le=10_000_000) | |
| duration_seconds: float = Field(..., gt=0, le=3600) | |
| memory_percent: float | None = Field(None, ge=0, le=100) | |
| class PriceBreakdown(BaseModel): | |
| cpu_cost: str | |
| token_cost: str | |
| total: str | |
| currency: str | |
| # --------------------------------------------------------------------------- | |
| # In-memory history + text logs ("Sovereign Node Logs") | |
| # --------------------------------------------------------------------------- | |
| _lock = threading.Lock() | |
| _logs: deque[str] = deque(maxlen=config.MAX_LOG_LINES) | |
| _transactions: deque[dict[str, Any]] = deque(maxlen=config.MAX_TRANSACTIONS) | |
| def _now() -> str: | |
| return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") | |
| def log(level: str, message: str) -> None: | |
| """Append a line to the in-memory log and (best effort) to a text file. | |
| Never pass secrets or tokens into this function. | |
| """ | |
| line = f"[{_now()}] {level.upper():<7} {message}" | |
| with _lock: | |
| _logs.append(line) | |
| try: | |
| config.LOG_DIR.mkdir(parents=True, exist_ok=True) | |
| with config.LOG_FILE.open("a", encoding="utf-8") as fh: | |
| fh.write(line + "\n") | |
| except OSError: | |
| pass # logging must never break the request | |
| # --------------------------------------------------------------------------- | |
| # Analytical Modeler (pricing) | |
| # --------------------------------------------------------------------------- | |
| def _money(value: Decimal) -> Decimal: | |
| return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) | |
| def price_workload(p: TelemetryPayload) -> tuple[Decimal, PriceBreakdown]: | |
| """Turn CPU load, time and data tokens into a price.""" | |
| cpu_cost = ( | |
| Decimal(str(p.cpu_percent)) / Decimal(100) | |
| * Decimal(str(p.duration_seconds)) | |
| * config.CPU_RATE_PER_SECOND | |
| ) | |
| token_cost = Decimal(p.tokens) / Decimal(1000) * config.TOKEN_RATE_PER_1K | |
| total = max(_money(cpu_cost + token_cost), config.MIN_CHARGE) | |
| breakdown = PriceBreakdown( | |
| cpu_cost=f"{cpu_cost:.6f}", | |
| token_cost=f"{token_cost:.6f}", | |
| total=f"{total:.2f}", | |
| currency=config.CURRENCY, | |
| ) | |
| return total, breakdown | |
| # --------------------------------------------------------------------------- | |
| # PayPal Sandbox client (OAuth 2.0 + Orders v2) | |
| # --------------------------------------------------------------------------- | |
| class PayPalUnavailable(Exception): | |
| """Network problem: timeout, DNS failure, connection refused...""" | |
| class PayPalError(Exception): | |
| """PayPal answered with an error status.""" | |
| def __init__(self, status_code: int, message: str) -> None: | |
| super().__init__(message) | |
| self.status_code = status_code | |
| self.message = message | |
| def _error_message(resp: requests.Response) -> str: | |
| try: | |
| data = resp.json() | |
| return str(data.get("message") or data.get("error_description") or data)[:300] | |
| except ValueError: | |
| return resp.text[:300] | |
| class _TokenCache: | |
| """Caches the OAuth token and refreshes it shortly before it expires.""" | |
| def __init__(self) -> None: | |
| self._lock = threading.Lock() | |
| self._token: str | None = None | |
| self._expires_at = 0.0 | |
| def get(self, force: bool = False) -> str: | |
| with self._lock: | |
| stale = time.time() >= self._expires_at - config.TOKEN_REFRESH_MARGIN_SECONDS | |
| if force or not self._token or stale: | |
| self._fetch() | |
| assert self._token is not None | |
| return self._token | |
| def _fetch(self) -> None: | |
| try: | |
| resp = requests.post( | |
| f"{config.PAYPAL_BASE_URL}/v1/oauth2/token", | |
| auth=(config.PAYPAL_CLIENT_ID, config.PAYPAL_CLIENT_SECRET), | |
| data={"grant_type": "client_credentials"}, | |
| headers={"Accept": "application/json"}, | |
| timeout=config.REQUEST_TIMEOUT_SECONDS, | |
| ) | |
| except requests.RequestException as exc: | |
| raise PayPalUnavailable(f"token request failed: {exc}") from exc | |
| if resp.status_code >= 400: | |
| raise PayPalError(resp.status_code, f"token request rejected: {_error_message(resp)}") | |
| data = resp.json() | |
| self._token = data["access_token"] | |
| self._expires_at = time.time() + int(data.get("expires_in", 300)) | |
| log("info", "PayPal access token refreshed") | |
| def clear(self) -> None: | |
| with self._lock: | |
| self._token = None | |
| self._expires_at = 0.0 | |
| _tokens = _TokenCache() | |
| def _paypal_call( | |
| method: str, path: str, payload: dict | None = None, request_id: str | None = None | |
| ) -> dict: | |
| """Call PayPal. Retries once with a fresh token if the old one was rejected.""" | |
| resp: requests.Response | None = None | |
| for attempt in (0, 1): | |
| token = _tokens.get(force=attempt == 1) | |
| headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} | |
| if request_id: | |
| headers["PayPal-Request-Id"] = request_id # idempotency key | |
| try: | |
| resp = requests.request( | |
| method, | |
| f"{config.PAYPAL_BASE_URL}{path}", | |
| json=payload, | |
| headers=headers, | |
| timeout=config.REQUEST_TIMEOUT_SECONDS, | |
| ) | |
| except requests.RequestException as exc: | |
| raise PayPalUnavailable(f"{method} {path} failed: {exc}") from exc | |
| if resp.status_code == 401 and attempt == 0: | |
| continue | |
| break | |
| assert resp is not None | |
| if resp.status_code >= 400: | |
| raise PayPalError(resp.status_code, _error_message(resp)) | |
| return resp.json() if resp.content else {} | |
| def _create_order(node_id: str, amount: Decimal, request_id: str) -> dict: | |
| body: dict[str, Any] = { | |
| "intent": "CAPTURE", | |
| "purchase_units": [ | |
| { | |
| "reference_id": node_id, | |
| "description": f"Papaya compute usage for node {node_id}", | |
| "amount": {"currency_code": config.CURRENCY, "value": f"{amount:.2f}"}, | |
| } | |
| ], | |
| } | |
| if config.PAYPAL_VAULT_ID: | |
| # Stored payment source: lets the merchant charge without a buyer page. | |
| body["payment_source"] = { | |
| "paypal": { | |
| "vault_id": config.PAYPAL_VAULT_ID, | |
| "stored_credential": { | |
| "payment_initiator": "MERCHANT", | |
| "payment_type": "UNSCHEDULED", | |
| "usage": "SUBSEQUENT", | |
| }, | |
| } | |
| } | |
| return _paypal_call("POST", "/v2/checkout/orders", body, request_id) | |
| def _capture_order(order_id: str, request_id: str) -> dict: | |
| return _paypal_call("POST", f"/v2/checkout/orders/{order_id}/capture", {}, request_id) | |
| def _capture_id(order: dict) -> str | None: | |
| try: | |
| return order["purchase_units"][0]["payments"]["captures"][0]["id"] | |
| except (KeyError, IndexError, TypeError): | |
| return None | |
| def _approval_url(order: dict) -> str | None: | |
| for link in order.get("links", []): | |
| if link.get("rel") in ("payer-action", "approve"): | |
| return link.get("href") | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Payment orchestration | |
| # --------------------------------------------------------------------------- | |
| def process_payment(payload: TelemetryPayload) -> dict[str, Any]: | |
| """Price the workload, then try to pay it. Never raises on network errors.""" | |
| amount, breakdown = price_workload(payload) | |
| record: dict[str, Any] = { | |
| "id": uuid.uuid4().hex[:12], | |
| "timestamp": _now(), | |
| "node_id": payload.node_id, | |
| "cpu_percent": payload.cpu_percent, | |
| "tokens": payload.tokens, | |
| "duration_seconds": payload.duration_seconds, | |
| "amount": f"{amount:.2f}", | |
| "currency": config.CURRENCY, | |
| "breakdown": breakdown.model_dump(), | |
| "mode": "paypal_sandbox", | |
| "status": "PENDING", | |
| "order_id": None, | |
| "capture_id": None, | |
| "approval_url": None, | |
| "detail": "", | |
| } | |
| if amount > config.MAX_CHARGE: | |
| record.update( | |
| mode="none", | |
| status="REJECTED", | |
| detail=f"Charge {amount:.2f} exceeds the per-call cap of {config.MAX_CHARGE:.2f}", | |
| ) | |
| log("warning", f"node={payload.node_id} rejected: {record['detail']}") | |
| return _store(record) | |
| if not config.paypal_configured(): | |
| record.update( | |
| mode="local_fallback", | |
| status="LOCAL_ONLY", | |
| detail="PayPal credentials not set; price calculated locally, no payment made", | |
| ) | |
| log("warning", f"node={payload.node_id} local fallback amount={record['amount']}") | |
| return _store(record) | |
| request_id = uuid.uuid4().hex | |
| try: | |
| order = _create_order(payload.node_id, amount, request_id) | |
| record["order_id"] = order.get("id") | |
| status = order.get("status", "UNKNOWN") | |
| log("info", f"node={payload.node_id} order {record['order_id']} status={status}") | |
| if status == "APPROVED": | |
| order = _capture_order(order["id"], request_id + "-cap") | |
| status = order.get("status", "UNKNOWN") | |
| record["status"] = status | |
| record["capture_id"] = _capture_id(order) | |
| if status == "COMPLETED": | |
| record["detail"] = "Payment captured" | |
| log("info", f"node={payload.node_id} captured {record['amount']} {config.CURRENCY}") | |
| else: | |
| record["approval_url"] = _approval_url(order) | |
| record["detail"] = ( | |
| "Order created but the payment source has not approved it yet. " | |
| "Set PAYPAL_VAULT_ID to charge a stored payment source without a buyer page." | |
| ) | |
| log("warning", f"node={payload.node_id} order not completed (status={status})") | |
| except PayPalUnavailable as exc: | |
| record.update( | |
| mode="local_fallback", | |
| status="LOCAL_ONLY", | |
| detail=f"PayPal unreachable; price calculated locally ({exc})", | |
| ) | |
| log("error", f"node={payload.node_id} PayPal unreachable, using local fallback") | |
| except PayPalError as exc: | |
| record.update(status="FAILED", detail=f"PayPal error {exc.status_code}: {exc.message}") | |
| log("error", f"node={payload.node_id} PayPal error {exc.status_code}") | |
| return _store(record) | |
| def _store(record: dict[str, Any]) -> dict[str, Any]: | |
| with _lock: | |
| _transactions.append(record) | |
| return record | |
| # --------------------------------------------------------------------------- | |
| # Telemetry Sensor | |
| # --------------------------------------------------------------------------- | |
| def sample_local_telemetry(node_id: str, window_seconds: float) -> dict[str, Any]: | |
| """Read the real CPU/memory of this machine. | |
| Data tokens are *simulated* (Poisson noise scaled by CPU load) so the demo | |
| works on any laptop. Real nodes should send their own token counts. | |
| """ | |
| cpu = float(psutil.cpu_percent(interval=0.5)) | |
| mem = float(psutil.virtual_memory().percent) | |
| rng = np.random.default_rng() | |
| tokens = int(rng.poisson(lam=200 + cpu * 20)) | |
| return { | |
| "node_id": node_id, | |
| "cpu_percent": round(cpu, 1), | |
| "memory_percent": round(mem, 1), | |
| "tokens": tokens, | |
| "duration_seconds": window_seconds, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # API routes | |
| # --------------------------------------------------------------------------- | |
| def require_api_key(x_api_key: str | None = Header(default=None)) -> None: | |
| """Protects spending endpoints when PAPAYA_API_KEY is set.""" | |
| if config.PAPAYA_API_KEY and not hmac.compare_digest( | |
| x_api_key or "", config.PAPAYA_API_KEY | |
| ): | |
| raise HTTPException(status_code=401, detail="Invalid or missing X-API-Key") | |
| def root() -> dict[str, str]: | |
| return {"name": "Papaya", "docs": "/docs", "health": "/health"} | |
| def health() -> dict[str, Any]: | |
| return { | |
| "status": "ok", | |
| "paypal_configured": config.paypal_configured(), | |
| "vault_configured": bool(config.PAYPAL_VAULT_ID), | |
| "api_key_required": bool(config.PAPAYA_API_KEY), | |
| "currency": config.CURRENCY, | |
| } | |
| def telemetry_sample( | |
| node_id: str = Query("local-node", pattern=r"^[A-Za-z0-9_.-]{1,64}$"), | |
| window_seconds: float = Query(5.0, gt=0, le=3600), | |
| ) -> dict[str, Any]: | |
| return sample_local_telemetry(node_id, window_seconds) | |
| def price(payload: TelemetryPayload) -> PriceBreakdown: | |
| """Price a workload without paying for it.""" | |
| return price_workload(payload)[1] | |
| def pay(payload: TelemetryPayload) -> dict[str, Any]: | |
| """Price a workload and pay it through the PayPal Sandbox.""" | |
| return process_payment(payload) | |
| def transactions(limit: int = Query(50, ge=1, le=500)) -> list[dict[str, Any]]: | |
| with _lock: | |
| return list(_transactions)[-limit:][::-1] | |
| def logs(limit: int = Query(100, ge=1, le=500)) -> dict[str, list[str]]: | |
| with _lock: | |
| return {"lines": list(_logs)[-limit:]} | |