403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.216
Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5
System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64
User : ec2-user ( 1000)
PHP Version : 8.4.21
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /tsai/repo/api/app/services/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tsai/repo/api/app/services/credits_guard.py
"""Credit enforcement for metered operations.

Gates expensive/automated work (article generation, image studio, AMA research)
on a site's remaining Scike Credits. The store owns the balance and the
recharge/cap decision; this guard converts the API's usage log into a spend
level, asks the store for a verdict, and caches it briefly to keep hot paths
cheap.

Design invariants:
  - Fail open. If the store is unreachable / unconfigured, or the site has no
    credit account, operations proceed — billing infra must never hard-block
    generation.
  - The store is the decision-maker; this module never decides recharge/cap on
    its own.
"""

import logging
import time
from datetime import datetime
from typing import Any, Dict, Optional

from . import credits_client, usage_history

# Short in-process caches. Per-worker; that's fine — the store stays the source
# of truth and re-evaluates whenever a cache entry expires.
_STATUS_TTL = 60.0
_VERDICT_TTL = 20.0
_status_cache: Dict[str, tuple] = {}
_verdict_cache: Dict[str, tuple] = {}

# Verdict returned when we can't reach the store — allow, but mark it so callers
# can tell an explicit "you're fine" from a fail-open.
_FAIL_OPEN = {'allowed': True, 'remaining': None, 'reason': 'unavailable', 'status': 'unknown'}


class CreditsExhausted(Exception):
    """Raised by ensure_allowed() when a site is out of credits and can't recharge."""

    def __init__(self, slug: str, reason: Optional[str], remaining: Optional[float] = None):
        self.slug = slug
        self.reason = reason or 'no_credits'
        self.remaining = remaining
        super().__init__(f"Site '{slug}' is out of credits (paused: {self.reason})")


async def _cached_status(slug: str) -> Optional[Dict[str, Any]]:
    hit = _status_cache.get(slug)
    if hit and (time.monotonic() - hit[0]) < _STATUS_TTL:
        return hit[1]
    status = await credits_client.status(slug)
    _status_cache[slug] = (time.monotonic(), status)
    return status


async def check_credits(slug: str, operation: str = 'operation') -> Dict[str, Any]:
    """Return the current verdict for a site (non-raising).

    Verdict shape: {allowed: bool, remaining: float|None, reason: str|None,
    status: str, recharged?: bool}.
    """
    if not slug or not credits_client.is_configured():
        return dict(_FAIL_OPEN)

    cached = _verdict_cache.get(slug)
    if cached and (time.monotonic() - cached[0]) < _VERDICT_TTL:
        return cached[1]

    status = await _cached_status(slug)
    if not status:
        # No account (legacy/manual site) or store unavailable → fail open.
        return dict(_FAIL_OPEN)

    period_start = status.get('periodStart')
    try:
        since = datetime.fromtimestamp(int(period_start)) if period_start else datetime.now()
    except (TypeError, ValueError, OSError):
        since = datetime.now()

    used = usage_history.used_credits_since(slug, since)
    verdict = await credits_client.check(slug, used)
    if verdict is None:
        return dict(_FAIL_OPEN)

    _verdict_cache[slug] = (time.monotonic(), verdict)
    # A recharge or setting change invalidates the cached status balance.
    if verdict.get('recharged'):
        _status_cache.pop(slug, None)
    return verdict


async def ensure_allowed(slug: str, operation: str = 'operation') -> None:
    """Raise CreditsExhausted if the site can't run a paid `operation`."""
    verdict = await check_credits(slug, operation)
    if not verdict.get('allowed', True):
        logging.info(
            "Credits guard denied %s for %s (reason=%s, remaining=%s)",
            operation, slug, verdict.get('reason'), verdict.get('remaining'),
        )
        raise CreditsExhausted(slug, verdict.get('reason'), verdict.get('remaining'))


def invalidate(slug: str) -> None:
    """Drop cached state for a slug (call after a settings change)."""
    _status_cache.pop(slug, None)
    _verdict_cache.pop(slug, None)

Youez - 2016 - github.com/yon3zu
LinuXploit