mixlift
0.3.0Marketing Mix Modeling MCP server — CSV in, budget recommendations out.
License Sources
| Source | License | Class |
|---|---|---|
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."
)