mixlift

0.3.0

Marketing Mix Modeling MCP server — CSV in, budget recommendations out.

License
Unknown license
Published
March 8, 2026
2d ago
Package Registry
README badge Customize →
License Sources
SourceLicenseClass
Licensie (detected)
Pending-
PyPI (reported)
Not reported-

License detection is still in progress for this version.

Loading dependencies…
License File
"""License key validation for MixLift MCP Server.

Fail-closed design:
- On successful API validation, cache the result locally with a timestamp.
- On API failure, honor the cached tier if the cache is <24 hours old.
- If the cache is stale or absent on API failure, block paid features (free tier).
- Never silently downgrade a paid user on transient API errors.
"""

from __future__ import annotations

import json
import logging
import time
from dataclasses import dataclass
from pathlib import Path

import httpx

logger = logging.getLogger(__name__)

VALIDATE_URL = "https://api.mixlift.io/v1/license/validate"

# Tier limits
FREE_MAX_CHANNELS = 2
FREE_MAX_ROWS = 2_000
PRO_MAX_CHANNELS = 8
PRO_MAX_ROWS = 50_000

# Cache settings
CACHE_DIR = Path.home() / ".mixlift" / "cache"
CACHE_FILE = CACHE_DIR / "license_cache.json"
CACHE_TTL_SECONDS = 24 * 60 * 60  # 24 hours


@dataclass
class LicenseStatus:
    is_pro: bool
    max_channels: int
    max_rows: int
    message: str


def free_tier(message: str = "Free tier") -> LicenseStatus:
    return LicenseStatus(
        is_pro=False,
        max_channels=FREE_MAX_CHANNELS,
        max_rows=FREE_MAX_ROWS,
        message=message,
    )


def _pro_tier(message: str = "Pro license validated") -> LicenseStatus:
    return LicenseStatus(
        is_pro=True,
        max_channels=PRO_MAX_CHANNELS,
        max_rows=PRO_MAX_ROWS,
        message=message,
    )


def _load_cache(license_key: str) -> dict | None:
    """Load cached validation result for the given license key.

    Returns the cache entry dict if found and for this key, else None.
    """
    try:
        if CACHE_FILE.exists():
            data = json.loads(CACHE_FILE.read_text())
            if data.get("license_key") == license_key:
                return data
    except Exception as e:
        logger.warning("Could not load license cache: %s", e)
    return None


def _save_cache(license_key: str, tier: str, valid: bool) -> None:
    """Save a successful validation result to the local cache."""
    try:
        CACHE_DIR.mkdir(parents=True, exist_ok=True)
        data = {
            "license_key": license_key,
            "tier": tier,
            "valid": valid,
            "cached_at": time.time(),
        }
        CACHE_FILE.write_text(json.dumps(data))
    except Exception as e:
        logger.warning("Could not save license cache: %s", e)


def _is_cache_fresh(cache_entry: dict) -> bool:
    """Check if a cache entry is within the 24-hour grace period."""
    cached_at = cache_entry.get("cached_at", 0)
    return (time.time() - cached_at) < CACHE_TTL_SECONDS


async def validate_license(license_key: str | None) -> LicenseStatus:
    """Validate a license key against the remote endpoint.

    Fail-closed behavior:
    - No key provided: free tier (no API call).
    - API reachable + valid: pro tier, cache result.
    - API reachable + invalid: free tier, cache result (invalid).
    - API unreachable + fresh cache (<24h) with valid=True: honor cached tier.
    - API unreachable + stale/absent cache: free tier (fail-closed).
    """
    if not license_key:
        return free_tier("No license key provided")

    try:
        async with httpx.AsyncClient(timeout=5.0) as client:
            response = await client.post(
                VALIDATE_URL,
                json={"license_key": license_key},
            )
            response.raise_for_status()
            data = response.json()

            if data.get("valid") and data.get("tier") in ("pro", "growth"):
                _save_cache(license_key, data["tier"], valid=True)
                return _pro_tier("Pro license validated")

            # Explicitly invalid — cache that too so we don't grant access
            # from a stale positive cache after a key is revoked
            _save_cache(license_key, "free", valid=False)
            return free_tier("Invalid license key")

    except Exception as e:
        logger.warning("License API unreachable: %s", e)

        # Fail-closed: check local cache for grace period
        cache = _load_cache(license_key)
        if cache and cache.get("valid") and _is_cache_fresh(cache):
            cached_tier = cache.get("tier", "pro")
            logger.info(
                "Using cached license validation (grace period): tier=%s", cached_tier
            )
            return _pro_tier(
                f"License API unavailable — using cached validation (grace period, "
                f"tier={cached_tier})"
            )

        # No valid fresh cache — fail closed to free tier
        return free_tier(
            "License API unavailable and no valid cached validation — "
            "paid features blocked. Please check your connection."
        )
Versions
3 versions
VersionLicensePublishedStatus
0.3.0 Latest Viewing-Mar 8, 2026 Pending
0.2.0 -Mar 7, 2026 Pending
0.1.0 -Mar 4, 2026 Pending