Upload 9 files
Browse files- beta_keys_schema.py +119 -0
- beta_keys_store.py +321 -0
- generate_beta_key.py +80 -81
- requirements.txt +2 -1
- tools/migrate_keys_to_dataset.py +56 -0
- tools/test_gui_import.py +13 -0
- tools/test_openapi.py +32 -0
- tools/test_schema.py +41 -0
- tools/test_validate.py +23 -0
beta_keys_schema.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
VTX beta key schema, migration, and validation helpers.
|
| 3 |
+
"""
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import secrets
|
| 7 |
+
import string
|
| 8 |
+
from datetime import date
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
VALID_STATUSES = frozenset({"ACTIVE", "REVOKED", "EXPIRED", "BANNED", "INVALID"})
|
| 12 |
+
KEY_CHARS = string.ascii_uppercase + string.digits
|
| 13 |
+
NEW_KEY_PREFIX = "VTX-"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def today_iso() -> str:
|
| 17 |
+
return date.today().isoformat()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _random_key_segment(length: int = 6) -> str:
|
| 21 |
+
return "".join(secrets.choice(KEY_CHARS) for _ in range(length))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def generate_unique_key(existing: dict[str, Any]) -> str:
|
| 25 |
+
for _ in range(5000):
|
| 26 |
+
candidate = f"{NEW_KEY_PREFIX}{_random_key_segment(6)}"
|
| 27 |
+
if candidate not in existing:
|
| 28 |
+
return candidate
|
| 29 |
+
raise RuntimeError("Unable to generate a unique beta key.")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def normalize_record(key: str, record: dict[str, Any] | None) -> dict[str, Any]:
|
| 33 |
+
data = dict(record or {})
|
| 34 |
+
key_text = str(data.get("key") or key or "").strip()
|
| 35 |
+
active = bool(data.get("active"))
|
| 36 |
+
status = str(data.get("status") or "").strip().upper()
|
| 37 |
+
if not status:
|
| 38 |
+
status = "ACTIVE" if active else "REVOKED"
|
| 39 |
+
if status not in VALID_STATUSES:
|
| 40 |
+
status = "ACTIVE" if active else "REVOKED"
|
| 41 |
+
if status != "ACTIVE":
|
| 42 |
+
active = False
|
| 43 |
+
created = str(data.get("created") or data.get("created_date") or "").strip()
|
| 44 |
+
last_activation = data.get("last_activation")
|
| 45 |
+
if last_activation is not None and last_activation != "":
|
| 46 |
+
last_activation = str(last_activation).strip()
|
| 47 |
+
else:
|
| 48 |
+
last_activation = None
|
| 49 |
+
return {
|
| 50 |
+
"key": key_text,
|
| 51 |
+
"name": str(data.get("name") or data.get("owner") or "").strip(),
|
| 52 |
+
"notes": str(data.get("notes") or "").strip(),
|
| 53 |
+
"created": created,
|
| 54 |
+
"last_activation": last_activation,
|
| 55 |
+
"active": active,
|
| 56 |
+
"status": status,
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def migrate_keys_payload(payload: Any) -> dict[str, dict[str, Any]]:
|
| 61 |
+
if payload is None:
|
| 62 |
+
return {}
|
| 63 |
+
if isinstance(payload, list):
|
| 64 |
+
migrated: dict[str, dict[str, Any]] = {}
|
| 65 |
+
for item in payload:
|
| 66 |
+
if not isinstance(item, dict):
|
| 67 |
+
continue
|
| 68 |
+
key = str(item.get("key") or "").strip()
|
| 69 |
+
if not key:
|
| 70 |
+
continue
|
| 71 |
+
migrated[key] = normalize_record(key, item)
|
| 72 |
+
return migrated
|
| 73 |
+
if not isinstance(payload, dict):
|
| 74 |
+
return {}
|
| 75 |
+
|
| 76 |
+
if "keys" in payload and isinstance(payload.get("keys"), list):
|
| 77 |
+
return migrate_keys_payload(payload["keys"])
|
| 78 |
+
|
| 79 |
+
migrated: dict[str, dict[str, Any]] = {}
|
| 80 |
+
for raw_key, raw_record in payload.items():
|
| 81 |
+
key = str(raw_key or "").strip()
|
| 82 |
+
if not key:
|
| 83 |
+
continue
|
| 84 |
+
if isinstance(raw_record, str):
|
| 85 |
+
migrated[key] = normalize_record(
|
| 86 |
+
key,
|
| 87 |
+
{"key": key, "active": True, "status": "ACTIVE", "notes": raw_record},
|
| 88 |
+
)
|
| 89 |
+
continue
|
| 90 |
+
if not isinstance(raw_record, dict):
|
| 91 |
+
continue
|
| 92 |
+
migrated[key] = normalize_record(key, raw_record)
|
| 93 |
+
return migrated
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def validation_result(record: dict[str, Any] | None) -> dict[str, Any]:
|
| 97 |
+
if not record:
|
| 98 |
+
return {"valid": False, "active": False, "status": "INVALID"}
|
| 99 |
+
normalized = normalize_record(str(record.get("key") or ""), record)
|
| 100 |
+
active = bool(normalized.get("active"))
|
| 101 |
+
status = str(normalized.get("status") or "").strip().upper()
|
| 102 |
+
if status == "ACTIVE" and active:
|
| 103 |
+
return {"valid": True, "active": True, "status": "ACTIVE"}
|
| 104 |
+
if status == "REVOKED" or (not active and status == "ACTIVE"):
|
| 105 |
+
return {"valid": True, "active": False, "status": "REVOKED"}
|
| 106 |
+
if status in {"EXPIRED", "BANNED"}:
|
| 107 |
+
return {"valid": True, "active": False, "status": status}
|
| 108 |
+
return {"valid": True, "active": active, "status": status}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def new_record(name: str, notes: str = "") -> dict[str, Any]:
|
| 112 |
+
return {
|
| 113 |
+
"name": str(name or "").strip(),
|
| 114 |
+
"notes": str(notes or "").strip(),
|
| 115 |
+
"created": today_iso(),
|
| 116 |
+
"last_activation": None,
|
| 117 |
+
"active": True,
|
| 118 |
+
"status": "ACTIVE",
|
| 119 |
+
}
|
beta_keys_store.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
VTX beta keys — private Hugging Face dataset storage with local cache.
|
| 3 |
+
|
| 4 |
+
Environment:
|
| 5 |
+
VTX_KEYS_SOURCE=local|dataset (default: dataset)
|
| 6 |
+
VTX_KEYS_PATH=path/to/keys.json (local mode / cache file override)
|
| 7 |
+
VTX_BETA_KEYS_DATASET=org/repo (default: ManChildTechnologies/VTX-BetaKeys)
|
| 8 |
+
VTX_BETA_KEYS_FILENAME=keys.json
|
| 9 |
+
VTX_BETA_KEYS_REVISION=main
|
| 10 |
+
VTX_KEYS_REFRESH_SECONDS=30
|
| 11 |
+
HF_TOKEN / HUGGING_FACE_HUB_TOKEN
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import io
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import threading
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
from beta_keys_schema import migrate_keys_payload, normalize_record
|
| 24 |
+
|
| 25 |
+
APP_DIR = Path(__file__).resolve().parent
|
| 26 |
+
DEFAULT_KEYS_PATH = APP_DIR / "keys.json"
|
| 27 |
+
DEFAULT_CACHE_PATH = APP_DIR / "keys_cache.json"
|
| 28 |
+
DEFAULT_DATASET_REPO = "ManChildTechnologies/VTX-BetaKeys"
|
| 29 |
+
DEFAULT_DATASET_FILENAME = "keys.json"
|
| 30 |
+
|
| 31 |
+
_store_lock = threading.RLock()
|
| 32 |
+
_keys_cache: dict[str, Any] = {
|
| 33 |
+
"source": None,
|
| 34 |
+
"signature": None,
|
| 35 |
+
"fetched_at": 0.0,
|
| 36 |
+
"data": {},
|
| 37 |
+
"error": "",
|
| 38 |
+
"using_stale_cache": False,
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def keys_source() -> str:
|
| 43 |
+
raw = str(os.environ.get("VTX_KEYS_SOURCE", "dataset") or "dataset").strip().lower()
|
| 44 |
+
return "local" if raw == "local" else "dataset"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def keys_path() -> Path:
|
| 48 |
+
custom = str(os.environ.get("VTX_KEYS_PATH", "") or "").strip()
|
| 49 |
+
if custom:
|
| 50 |
+
return Path(custom)
|
| 51 |
+
return DEFAULT_KEYS_PATH
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def cache_path() -> Path:
|
| 55 |
+
custom = str(os.environ.get("VTX_KEYS_CACHE_PATH", "") or "").strip()
|
| 56 |
+
if custom:
|
| 57 |
+
return Path(custom)
|
| 58 |
+
return DEFAULT_CACHE_PATH
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def dataset_repo() -> str:
|
| 62 |
+
return str(os.environ.get("VTX_BETA_KEYS_DATASET", DEFAULT_DATASET_REPO) or DEFAULT_DATASET_REPO).strip()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def dataset_filename() -> str:
|
| 66 |
+
return str(os.environ.get("VTX_BETA_KEYS_FILENAME", DEFAULT_DATASET_FILENAME) or DEFAULT_DATASET_FILENAME).strip()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def dataset_revision() -> str:
|
| 70 |
+
return str(os.environ.get("VTX_BETA_KEYS_REVISION", "main") or "main").strip() or "main"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def refresh_seconds() -> int:
|
| 74 |
+
try:
|
| 75 |
+
return max(5, int(os.environ.get("VTX_KEYS_REFRESH_SECONDS", "30") or 30))
|
| 76 |
+
except Exception:
|
| 77 |
+
return 30
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def hf_token() -> str:
|
| 81 |
+
for key in ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HUGGINGFACE_HUB_TOKEN"):
|
| 82 |
+
value = str(os.environ.get(key, "") or "").strip()
|
| 83 |
+
if value:
|
| 84 |
+
return value
|
| 85 |
+
return ""
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _require_hf_token() -> str:
|
| 89 |
+
token = hf_token()
|
| 90 |
+
if not token:
|
| 91 |
+
raise RuntimeError(
|
| 92 |
+
"HF_TOKEN is required for VTX beta key dataset access. "
|
| 93 |
+
"Set HF_TOKEN as an environment variable or Hugging Face Space secret."
|
| 94 |
+
)
|
| 95 |
+
return token
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _get_hf_api():
|
| 99 |
+
try:
|
| 100 |
+
from huggingface_hub import HfApi
|
| 101 |
+
except ImportError as exc:
|
| 102 |
+
raise RuntimeError("huggingface_hub is required for dataset key storage.") from exc
|
| 103 |
+
return HfApi(token=_require_hf_token())
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _serialize_keys(keys: dict[str, dict[str, Any]]) -> str:
|
| 107 |
+
ordered = {
|
| 108 |
+
key: normalize_record(key, record)
|
| 109 |
+
for key, record in sorted(keys.items(), key=lambda item: item[0])
|
| 110 |
+
}
|
| 111 |
+
return json.dumps(ordered, indent=2) + "\n"
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _read_json_file(path: Path) -> dict[str, dict[str, Any]]:
|
| 115 |
+
if not path.is_file():
|
| 116 |
+
return {}
|
| 117 |
+
try:
|
| 118 |
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 119 |
+
except Exception:
|
| 120 |
+
return {}
|
| 121 |
+
return migrate_keys_payload(payload)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _write_json_file(path: Path, keys: dict[str, dict[str, Any]]) -> None:
|
| 125 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 126 |
+
path.write_text(_serialize_keys(keys), encoding="utf-8")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _local_signature(path: Path) -> str | None:
|
| 130 |
+
try:
|
| 131 |
+
if not path.is_file():
|
| 132 |
+
return None
|
| 133 |
+
stat = path.stat()
|
| 134 |
+
return f"local:{path}:{stat.st_mtime_ns}:{stat.st_size}"
|
| 135 |
+
except OSError:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _fetch_dataset_keys() -> tuple[dict[str, dict[str, Any]], str]:
|
| 140 |
+
token = _require_hf_token()
|
| 141 |
+
try:
|
| 142 |
+
from huggingface_hub import hf_hub_download
|
| 143 |
+
except ImportError as exc:
|
| 144 |
+
raise RuntimeError("huggingface_hub is required for dataset key loading.") from exc
|
| 145 |
+
|
| 146 |
+
repo_id = dataset_repo()
|
| 147 |
+
filename = dataset_filename()
|
| 148 |
+
revision = dataset_revision()
|
| 149 |
+
cached_path = hf_hub_download(
|
| 150 |
+
repo_id=repo_id,
|
| 151 |
+
repo_type="dataset",
|
| 152 |
+
filename=filename,
|
| 153 |
+
revision=revision,
|
| 154 |
+
token=token,
|
| 155 |
+
force_download=True,
|
| 156 |
+
)
|
| 157 |
+
path = Path(cached_path)
|
| 158 |
+
data = _read_json_file(path)
|
| 159 |
+
signature = f"dataset:{repo_id}:{revision}:{filename}:{path.stat().st_mtime_ns}:{path.stat().st_size}"
|
| 160 |
+
_write_json_file(cache_path(), data)
|
| 161 |
+
return data, signature
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def push_keys_to_dataset(keys: dict[str, dict[str, Any]], commit_message: str) -> None:
|
| 165 |
+
token = _require_hf_token()
|
| 166 |
+
api = _get_hf_api()
|
| 167 |
+
payload = _serialize_keys(keys).encode("utf-8")
|
| 168 |
+
api.upload_file(
|
| 169 |
+
path_or_fileobj=io.BytesIO(payload),
|
| 170 |
+
path_in_repo=dataset_filename(),
|
| 171 |
+
repo_id=dataset_repo(),
|
| 172 |
+
repo_type="dataset",
|
| 173 |
+
revision=dataset_revision(),
|
| 174 |
+
commit_message=commit_message,
|
| 175 |
+
token=token,
|
| 176 |
+
)
|
| 177 |
+
_write_json_file(cache_path(), keys)
|
| 178 |
+
global _keys_cache
|
| 179 |
+
_keys_cache = {
|
| 180 |
+
"source": "dataset",
|
| 181 |
+
"signature": f"pushed:{time.time_ns()}",
|
| 182 |
+
"fetched_at": time.time(),
|
| 183 |
+
"data": dict(keys),
|
| 184 |
+
"error": "",
|
| 185 |
+
"using_stale_cache": False,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def load_keys(force: bool = False) -> dict[str, dict[str, Any]]:
|
| 190 |
+
global _keys_cache
|
| 191 |
+
|
| 192 |
+
with _store_lock:
|
| 193 |
+
source = keys_source()
|
| 194 |
+
now = time.time()
|
| 195 |
+
ttl = refresh_seconds()
|
| 196 |
+
|
| 197 |
+
if source == "local":
|
| 198 |
+
path = keys_path()
|
| 199 |
+
signature = _local_signature(path)
|
| 200 |
+
if (
|
| 201 |
+
not force
|
| 202 |
+
and _keys_cache.get("source") == "local"
|
| 203 |
+
and signature == _keys_cache.get("signature")
|
| 204 |
+
and (now - float(_keys_cache.get("fetched_at") or 0.0)) < ttl
|
| 205 |
+
):
|
| 206 |
+
return dict(_keys_cache.get("data") or {})
|
| 207 |
+
|
| 208 |
+
data = _read_json_file(path)
|
| 209 |
+
_keys_cache = {
|
| 210 |
+
"source": "local",
|
| 211 |
+
"signature": signature,
|
| 212 |
+
"fetched_at": now,
|
| 213 |
+
"data": data,
|
| 214 |
+
"error": "",
|
| 215 |
+
"using_stale_cache": False,
|
| 216 |
+
}
|
| 217 |
+
return dict(data)
|
| 218 |
+
|
| 219 |
+
if (
|
| 220 |
+
not force
|
| 221 |
+
and _keys_cache.get("source") == "dataset"
|
| 222 |
+
and (now - float(_keys_cache.get("fetched_at") or 0.0)) < ttl
|
| 223 |
+
and _keys_cache.get("data") is not None
|
| 224 |
+
):
|
| 225 |
+
return dict(_keys_cache.get("data") or {})
|
| 226 |
+
|
| 227 |
+
try:
|
| 228 |
+
data, signature = _fetch_dataset_keys()
|
| 229 |
+
_keys_cache = {
|
| 230 |
+
"source": "dataset",
|
| 231 |
+
"signature": signature,
|
| 232 |
+
"fetched_at": now,
|
| 233 |
+
"data": data,
|
| 234 |
+
"error": "",
|
| 235 |
+
"using_stale_cache": False,
|
| 236 |
+
}
|
| 237 |
+
return dict(data)
|
| 238 |
+
except Exception as exc:
|
| 239 |
+
cached_file = _read_json_file(cache_path())
|
| 240 |
+
if cached_file:
|
| 241 |
+
_keys_cache = {
|
| 242 |
+
"source": "dataset",
|
| 243 |
+
"signature": _keys_cache.get("signature"),
|
| 244 |
+
"fetched_at": now,
|
| 245 |
+
"data": cached_file,
|
| 246 |
+
"error": str(exc),
|
| 247 |
+
"using_stale_cache": True,
|
| 248 |
+
}
|
| 249 |
+
return dict(cached_file)
|
| 250 |
+
_keys_cache = {
|
| 251 |
+
"source": "dataset",
|
| 252 |
+
"signature": None,
|
| 253 |
+
"fetched_at": now,
|
| 254 |
+
"data": {},
|
| 255 |
+
"error": str(exc),
|
| 256 |
+
"using_stale_cache": False,
|
| 257 |
+
}
|
| 258 |
+
raise
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def save_keys(keys: dict[str, dict[str, Any]], commit_message: str = "Update VTX beta keys") -> None:
|
| 262 |
+
normalized = {
|
| 263 |
+
key: normalize_record(key, record)
|
| 264 |
+
for key, record in (keys or {}).items()
|
| 265 |
+
if str(key or "").strip()
|
| 266 |
+
}
|
| 267 |
+
with _store_lock:
|
| 268 |
+
if keys_source() == "local":
|
| 269 |
+
_write_json_file(keys_path(), normalized)
|
| 270 |
+
global _keys_cache
|
| 271 |
+
_keys_cache = {
|
| 272 |
+
"source": "local",
|
| 273 |
+
"signature": _local_signature(keys_path()),
|
| 274 |
+
"fetched_at": time.time(),
|
| 275 |
+
"data": dict(normalized),
|
| 276 |
+
"error": "",
|
| 277 |
+
"using_stale_cache": False,
|
| 278 |
+
}
|
| 279 |
+
return
|
| 280 |
+
push_keys_to_dataset(normalized, commit_message)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def update_key_record(
|
| 284 |
+
beta_key: str,
|
| 285 |
+
updater,
|
| 286 |
+
commit_message: str,
|
| 287 |
+
) -> dict[str, dict[str, Any]]:
|
| 288 |
+
key = str(beta_key or "").strip()
|
| 289 |
+
if not key:
|
| 290 |
+
raise ValueError("Beta key is required.")
|
| 291 |
+
|
| 292 |
+
with _store_lock:
|
| 293 |
+
keys = load_keys(force=True)
|
| 294 |
+
record = dict(keys.get(key) or normalize_record(key, {"key": key}))
|
| 295 |
+
record = normalize_record(key, updater(record))
|
| 296 |
+
record["key"] = key
|
| 297 |
+
keys[key] = record
|
| 298 |
+
save_keys(keys, commit_message=commit_message)
|
| 299 |
+
return record
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def keys_status() -> dict[str, Any]:
|
| 303 |
+
source = keys_source()
|
| 304 |
+
status = {
|
| 305 |
+
"source": source,
|
| 306 |
+
"keys_loaded": len(_keys_cache.get("data") or {}),
|
| 307 |
+
"last_error": str(_keys_cache.get("error") or ""),
|
| 308 |
+
"cache_age_seconds": max(0, int(time.time() - float(_keys_cache.get("fetched_at") or 0.0))),
|
| 309 |
+
"using_stale_cache": bool(_keys_cache.get("using_stale_cache")),
|
| 310 |
+
}
|
| 311 |
+
if source == "local":
|
| 312 |
+
path = keys_path()
|
| 313 |
+
status["keys_path"] = str(path)
|
| 314 |
+
status["keys_exists"] = path.is_file()
|
| 315 |
+
else:
|
| 316 |
+
status["dataset_repo"] = dataset_repo()
|
| 317 |
+
status["dataset_filename"] = dataset_filename()
|
| 318 |
+
status["dataset_revision"] = dataset_revision()
|
| 319 |
+
status["hf_token_configured"] = bool(hf_token())
|
| 320 |
+
status["cache_path"] = str(cache_path())
|
| 321 |
+
return status
|
generate_beta_key.py
CHANGED
|
@@ -1,116 +1,113 @@
|
|
| 1 |
"""
|
| 2 |
VTX Studio beta key administration utility.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
No accounts, billing, subscriptions, machine locking, or activation limits.
|
| 6 |
"""
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
import argparse
|
| 10 |
import json
|
| 11 |
-
import secrets
|
| 12 |
-
import string
|
| 13 |
from pathlib import Path
|
| 14 |
from typing import Any
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
APP_DIR = Path(__file__).resolve().parent
|
| 17 |
DEFAULT_KEYS_PATH = APP_DIR / "keys.json"
|
| 18 |
-
KEY_PREFIX = "VTX-BETA"
|
| 19 |
-
KEY_CHARS = string.ascii_uppercase + string.digits
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def keys_path(path: str | Path | None = None) -> Path:
|
| 23 |
-
if path:
|
| 24 |
-
return Path(path)
|
| 25 |
-
env = __import__("os").environ.get("VTX_KEYS_PATH", "").strip()
|
| 26 |
-
if env:
|
| 27 |
-
return Path(env)
|
| 28 |
-
return DEFAULT_KEYS_PATH
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def load_keys(path: str | Path | None = None) -> dict[str, dict[str, Any]]:
|
| 32 |
-
file_path = keys_path(path)
|
| 33 |
-
if not file_path.is_file():
|
| 34 |
-
return {}
|
| 35 |
-
data = json.loads(file_path.read_text(encoding="utf-8"))
|
| 36 |
-
if not isinstance(data, dict):
|
| 37 |
-
raise ValueError(f"keys file must be a JSON object: {file_path}")
|
| 38 |
-
return data
|
| 39 |
|
| 40 |
|
| 41 |
-
def
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
-
def
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
return candidate
|
| 57 |
-
raise RuntimeError("Unable to generate a unique beta key.")
|
| 58 |
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
""
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
return
|
| 67 |
|
| 68 |
|
| 69 |
-
def
|
| 70 |
-
"""Revoke an existing beta key."""
|
| 71 |
key = str(beta_key or "").strip()
|
| 72 |
if not key:
|
| 73 |
raise ValueError("Beta key is required.")
|
| 74 |
|
| 75 |
-
keys = load_keys(
|
| 76 |
if key not in keys:
|
| 77 |
raise KeyError(f"Beta key not found: {key}")
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
-
def
|
| 85 |
-
"""Reactivate a previously revoked beta key."""
|
| 86 |
key = str(beta_key or "").strip()
|
| 87 |
if not key:
|
| 88 |
raise ValueError("Beta key is required.")
|
| 89 |
|
| 90 |
-
keys = load_keys(
|
| 91 |
if key not in keys:
|
| 92 |
raise KeyError(f"Beta key not found: {key}")
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
keys[key] = {"active": True, "status": "ACTIVE"}
|
| 95 |
-
save_keys(keys, path)
|
| 96 |
-
return keys[key]
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
keys = load_keys(path)
|
| 102 |
-
out = Path(output) if output else keys_path(path)
|
| 103 |
out.parent.mkdir(parents=True, exist_ok=True)
|
| 104 |
-
|
|
|
|
| 105 |
return out
|
| 106 |
|
| 107 |
|
| 108 |
def main() -> int:
|
| 109 |
parser = argparse.ArgumentParser(description="VTX Studio beta key manager")
|
| 110 |
-
parser.add_argument("--keys", default=
|
| 111 |
sub = parser.add_subparsers(dest="command", required=True)
|
| 112 |
|
| 113 |
-
sub.add_parser("generate", help="Generate a new ACTIVE beta key")
|
|
|
|
|
|
|
| 114 |
|
| 115 |
revoke = sub.add_parser("revoke", help="Revoke an existing beta key")
|
| 116 |
revoke.add_argument("beta_key", help="Beta key to revoke")
|
|
@@ -118,34 +115,36 @@ def main() -> int:
|
|
| 118 |
reactivate = sub.add_parser("reactivate", help="Reactivate a revoked beta key")
|
| 119 |
reactivate.add_argument("beta_key", help="Beta key to reactivate")
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
help="Optional output path (defaults to keys file)",
|
| 127 |
-
)
|
| 128 |
|
| 129 |
args = parser.parse_args()
|
| 130 |
-
keys_file = args.keys
|
| 131 |
|
| 132 |
if args.command == "generate":
|
| 133 |
-
beta_key = generate_key(
|
| 134 |
print(beta_key)
|
| 135 |
return 0
|
| 136 |
|
| 137 |
if args.command == "revoke":
|
| 138 |
-
record = revoke_key(args.beta_key
|
| 139 |
-
print(json.dumps(
|
| 140 |
return 0
|
| 141 |
|
| 142 |
if args.command == "reactivate":
|
| 143 |
-
record = reactivate_key(args.beta_key
|
| 144 |
-
print(json.dumps(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
return 0
|
| 146 |
|
| 147 |
if args.command == "export":
|
| 148 |
-
out = export_json(
|
| 149 |
print(str(out))
|
| 150 |
return 0
|
| 151 |
|
|
|
|
| 1 |
"""
|
| 2 |
VTX Studio beta key administration utility.
|
| 3 |
|
| 4 |
+
Keys are stored in the private ManChildTechnologies/VTX-BetaKeys dataset.
|
|
|
|
| 5 |
"""
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
import argparse
|
| 9 |
import json
|
|
|
|
|
|
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
+
from beta_keys_schema import generate_unique_key, new_record, normalize_record
|
| 14 |
+
from beta_keys_store import keys_path, load_keys, save_keys
|
| 15 |
+
|
| 16 |
APP_DIR = Path(__file__).resolve().parent
|
| 17 |
DEFAULT_KEYS_PATH = APP_DIR / "keys.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
+
def generate_key(
|
| 21 |
+
*,
|
| 22 |
+
name: str,
|
| 23 |
+
notes: str = "",
|
| 24 |
+
path: str | Path | None = None,
|
| 25 |
+
push: bool = True,
|
| 26 |
+
) -> str:
|
| 27 |
+
"""Generate a new ACTIVE beta key and persist it to the dataset."""
|
| 28 |
+
if not str(name or "").strip():
|
| 29 |
+
raise ValueError("Tester name is required.")
|
| 30 |
+
|
| 31 |
+
keys = load_keys(force=True)
|
| 32 |
+
beta_key = generate_unique_key(keys)
|
| 33 |
+
record = new_record(name=name, notes=notes)
|
| 34 |
+
record["key"] = beta_key
|
| 35 |
+
keys[beta_key] = normalize_record(beta_key, record)
|
| 36 |
+
if push:
|
| 37 |
+
save_keys(keys, commit_message=f"Generate beta key for {name.strip()}")
|
| 38 |
+
elif path:
|
| 39 |
+
from beta_keys_store import _write_json_file
|
| 40 |
+
|
| 41 |
+
_write_json_file(Path(path), keys)
|
| 42 |
+
return beta_key
|
| 43 |
|
| 44 |
|
| 45 |
+
def revoke_key(beta_key: str, path: str | Path | None = None, push: bool = True) -> dict[str, Any]:
|
| 46 |
+
key = str(beta_key or "").strip()
|
| 47 |
+
if not key:
|
| 48 |
+
raise ValueError("Beta key is required.")
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
keys = load_keys(force=True)
|
| 51 |
+
if key not in keys:
|
| 52 |
+
raise KeyError(f"Beta key not found: {key}")
|
| 53 |
|
| 54 |
+
record = normalize_record(key, keys[key])
|
| 55 |
+
record["active"] = False
|
| 56 |
+
record["status"] = "REVOKED"
|
| 57 |
+
keys[key] = record
|
| 58 |
+
if push:
|
| 59 |
+
save_keys(keys, commit_message=f"Revoke beta key {key}")
|
| 60 |
+
return record
|
| 61 |
|
| 62 |
|
| 63 |
+
def reactivate_key(beta_key: str, path: str | Path | None = None, push: bool = True) -> dict[str, Any]:
|
|
|
|
| 64 |
key = str(beta_key or "").strip()
|
| 65 |
if not key:
|
| 66 |
raise ValueError("Beta key is required.")
|
| 67 |
|
| 68 |
+
keys = load_keys(force=True)
|
| 69 |
if key not in keys:
|
| 70 |
raise KeyError(f"Beta key not found: {key}")
|
| 71 |
|
| 72 |
+
record = normalize_record(key, keys[key])
|
| 73 |
+
record["active"] = True
|
| 74 |
+
record["status"] = "ACTIVE"
|
| 75 |
+
keys[key] = record
|
| 76 |
+
if push:
|
| 77 |
+
save_keys(keys, commit_message=f"Reactivate beta key {key}")
|
| 78 |
+
return record
|
| 79 |
|
| 80 |
|
| 81 |
+
def delete_key(beta_key: str, push: bool = True) -> None:
|
|
|
|
| 82 |
key = str(beta_key or "").strip()
|
| 83 |
if not key:
|
| 84 |
raise ValueError("Beta key is required.")
|
| 85 |
|
| 86 |
+
keys = load_keys(force=True)
|
| 87 |
if key not in keys:
|
| 88 |
raise KeyError(f"Beta key not found: {key}")
|
| 89 |
+
del keys[key]
|
| 90 |
+
if push:
|
| 91 |
+
save_keys(keys, commit_message=f"Delete beta key {key}")
|
| 92 |
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
def export_json(output: str | Path | None = None) -> Path:
|
| 95 |
+
keys = load_keys(force=True)
|
| 96 |
+
out = Path(output) if output else keys_path()
|
|
|
|
|
|
|
| 97 |
out.parent.mkdir(parents=True, exist_ok=True)
|
| 98 |
+
ordered = {key: normalize_record(key, record) for key, record in sorted(keys.items())}
|
| 99 |
+
out.write_text(json.dumps(ordered, indent=2) + "\n", encoding="utf-8")
|
| 100 |
return out
|
| 101 |
|
| 102 |
|
| 103 |
def main() -> int:
|
| 104 |
parser = argparse.ArgumentParser(description="VTX Studio beta key manager")
|
| 105 |
+
parser.add_argument("--keys", default="", help="Optional local export path")
|
| 106 |
sub = parser.add_subparsers(dest="command", required=True)
|
| 107 |
|
| 108 |
+
generate = sub.add_parser("generate", help="Generate a new ACTIVE beta key")
|
| 109 |
+
generate.add_argument("--name", required=True, help="Tester name")
|
| 110 |
+
generate.add_argument("--notes", default="", help="Optional notes")
|
| 111 |
|
| 112 |
revoke = sub.add_parser("revoke", help="Revoke an existing beta key")
|
| 113 |
revoke.add_argument("beta_key", help="Beta key to revoke")
|
|
|
|
| 115 |
reactivate = sub.add_parser("reactivate", help="Reactivate a revoked beta key")
|
| 116 |
reactivate.add_argument("beta_key", help="Beta key to reactivate")
|
| 117 |
|
| 118 |
+
delete = sub.add_parser("delete", help="Delete an existing beta key")
|
| 119 |
+
delete.add_argument("beta_key", help="Beta key to delete")
|
| 120 |
+
|
| 121 |
+
export = sub.add_parser("export", help="Export keys.json snapshot")
|
| 122 |
+
export.add_argument("-o", "--output", default="", help="Optional output path")
|
|
|
|
|
|
|
| 123 |
|
| 124 |
args = parser.parse_args()
|
|
|
|
| 125 |
|
| 126 |
if args.command == "generate":
|
| 127 |
+
beta_key = generate_key(name=args.name, notes=args.notes)
|
| 128 |
print(beta_key)
|
| 129 |
return 0
|
| 130 |
|
| 131 |
if args.command == "revoke":
|
| 132 |
+
record = revoke_key(args.beta_key)
|
| 133 |
+
print(json.dumps(record, indent=2))
|
| 134 |
return 0
|
| 135 |
|
| 136 |
if args.command == "reactivate":
|
| 137 |
+
record = reactivate_key(args.beta_key)
|
| 138 |
+
print(json.dumps(record, indent=2))
|
| 139 |
+
return 0
|
| 140 |
+
|
| 141 |
+
if args.command == "delete":
|
| 142 |
+
delete_key(args.beta_key)
|
| 143 |
+
print("deleted")
|
| 144 |
return 0
|
| 145 |
|
| 146 |
if args.command == "export":
|
| 147 |
+
out = export_json(args.output or None)
|
| 148 |
print(str(out))
|
| 149 |
return 0
|
| 150 |
|
requirements.txt
CHANGED
|
@@ -1,2 +1,3 @@
|
|
| 1 |
fastapi>=0.110.0
|
| 2 |
-
uvicorn>=0.27.0
|
|
|
|
|
|
| 1 |
fastapi>=0.110.0
|
| 2 |
+
uvicorn>=0.27.0
|
| 3 |
+
huggingface_hub>=0.23.0
|
tools/migrate_keys_to_dataset.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""One-time migration: local keys.json -> private VTX-BetaKeys dataset."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 9 |
+
|
| 10 |
+
from beta_keys_schema import migrate_keys_payload
|
| 11 |
+
from beta_keys_store import cache_path, dataset_repo, hf_token, push_keys_to_dataset
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main() -> int:
|
| 15 |
+
parser = argparse.ArgumentParser(description="Migrate local keys.json to the private HF dataset")
|
| 16 |
+
parser.add_argument(
|
| 17 |
+
"--input",
|
| 18 |
+
default=str(Path(__file__).resolve().parents[1] / "keys.json"),
|
| 19 |
+
help="Path to legacy keys.json",
|
| 20 |
+
)
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--dry-run",
|
| 23 |
+
action="store_true",
|
| 24 |
+
help="Print migrated key count without uploading",
|
| 25 |
+
)
|
| 26 |
+
args = parser.parse_args()
|
| 27 |
+
|
| 28 |
+
if not hf_token():
|
| 29 |
+
print("HF_TOKEN is required.", file=sys.stderr)
|
| 30 |
+
return 1
|
| 31 |
+
|
| 32 |
+
source = Path(args.input)
|
| 33 |
+
if not source.is_file():
|
| 34 |
+
print(f"Missing input file: {source}", file=sys.stderr)
|
| 35 |
+
return 1
|
| 36 |
+
|
| 37 |
+
import json
|
| 38 |
+
|
| 39 |
+
payload = json.loads(source.read_text(encoding="utf-8"))
|
| 40 |
+
migrated = migrate_keys_payload(payload)
|
| 41 |
+
print(f"Migrated {len(migrated)} keys from {source}")
|
| 42 |
+
print(f"Target dataset: {dataset_repo()}")
|
| 43 |
+
|
| 44 |
+
if args.dry_run:
|
| 45 |
+
for key, record in sorted(migrated.items()):
|
| 46 |
+
print(f" {key}: {record.get('status')} name={record.get('name')!r}")
|
| 47 |
+
return 0
|
| 48 |
+
|
| 49 |
+
push_keys_to_dataset(migrated, commit_message="Migrate legacy keys.json to private dataset")
|
| 50 |
+
print(f"Uploaded keys.json to {dataset_repo()}")
|
| 51 |
+
print(f"Local cache written to {cache_path()}")
|
| 52 |
+
return 0
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
raise SystemExit(main())
|
tools/test_gui_import.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 5 |
+
|
| 6 |
+
from beta_keys_schema import normalize_record
|
| 7 |
+
import vtx_beta_key_manager as gui
|
| 8 |
+
|
| 9 |
+
record = normalize_record("VTX-TEST01", {"active": True, "status": "ACTIVE"})
|
| 10 |
+
assert record["status"] == "ACTIVE"
|
| 11 |
+
assert gui.APP_NAME == "VTX Beta Key Manager"
|
| 12 |
+
assert gui.APP_VERSION == "1.1.0"
|
| 13 |
+
print("OK")
|
tools/test_openapi.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verify OpenAPI documents ValidateRequest for Swagger."""
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 6 |
+
|
| 7 |
+
from app import app
|
| 8 |
+
|
| 9 |
+
schema = app.openapi()
|
| 10 |
+
request_schema = (
|
| 11 |
+
schema["paths"]["/api/validate"]["post"]
|
| 12 |
+
.get("requestBody", {})
|
| 13 |
+
.get("content", {})
|
| 14 |
+
.get("application/json", {})
|
| 15 |
+
.get("schema", {})
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
ref = request_schema.get("$ref", "")
|
| 19 |
+
assert ref.endswith("/ValidateRequest"), ref
|
| 20 |
+
|
| 21 |
+
model = schema["components"]["schemas"]["ValidateRequest"]
|
| 22 |
+
props = model.get("properties", {})
|
| 23 |
+
assert "beta_key" in props, props
|
| 24 |
+
|
| 25 |
+
examples = props["beta_key"].get("examples") or []
|
| 26 |
+
if not examples:
|
| 27 |
+
example = props["beta_key"].get("example")
|
| 28 |
+
assert example == "VTX-BETA-XXXX-XXXX", props["beta_key"]
|
| 29 |
+
else:
|
| 30 |
+
assert "VTX-BETA-XXXX-XXXX" in examples, examples
|
| 31 |
+
|
| 32 |
+
print("OK")
|
tools/test_schema.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Schema migration and validation tests."""
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 6 |
+
|
| 7 |
+
from beta_keys_schema import migrate_keys_payload, normalize_record, validation_result
|
| 8 |
+
|
| 9 |
+
legacy = {
|
| 10 |
+
"VTX-BETA-7K4P-X92M": {"active": True, "status": "ACTIVE"},
|
| 11 |
+
"VTX-BETA-R8JL-Q4WT": {"active": False, "status": "REVOKED"},
|
| 12 |
+
}
|
| 13 |
+
migrated = migrate_keys_payload(legacy)
|
| 14 |
+
assert "VTX-BETA-7K4P-X92M" in migrated
|
| 15 |
+
record = migrated["VTX-BETA-7K4P-X92M"]
|
| 16 |
+
assert record["key"] == "VTX-BETA-7K4P-X92M"
|
| 17 |
+
assert record["name"] == ""
|
| 18 |
+
assert record["notes"] == ""
|
| 19 |
+
assert record["created"] == ""
|
| 20 |
+
assert record["last_activation"] is None
|
| 21 |
+
assert record["active"] is True
|
| 22 |
+
assert validation_result(record)["status"] == "ACTIVE"
|
| 23 |
+
assert validation_result(migrated["VTX-BETA-R8JL-Q4WT"])["status"] == "REVOKED"
|
| 24 |
+
assert validation_result(None)["status"] == "INVALID"
|
| 25 |
+
|
| 26 |
+
modern = normalize_record(
|
| 27 |
+
"VTX-4D8A9B",
|
| 28 |
+
{
|
| 29 |
+
"key": "VTX-4D8A9B",
|
| 30 |
+
"name": "Ramon",
|
| 31 |
+
"notes": "Beta Tester",
|
| 32 |
+
"created": "2026-06-12",
|
| 33 |
+
"last_activation": None,
|
| 34 |
+
"active": True,
|
| 35 |
+
"status": "ACTIVE",
|
| 36 |
+
},
|
| 37 |
+
)
|
| 38 |
+
assert modern["name"] == "Ramon"
|
| 39 |
+
assert modern["notes"] == "Beta Tester"
|
| 40 |
+
|
| 41 |
+
print("OK")
|
tools/test_validate.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smoke test for validate_beta_key logic (local mode, no HF required)."""
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
+
|
| 8 |
+
os.environ["VTX_KEYS_SOURCE"] = "local"
|
| 9 |
+
os.environ["VTX_KEYS_PATH"] = str(Path(__file__).resolve().parents[1] / "keys.json")
|
| 10 |
+
|
| 11 |
+
from app import validate_beta_key
|
| 12 |
+
|
| 13 |
+
active = validate_beta_key("VTX-BETA-7K4P-X92M", track_activation=False)
|
| 14 |
+
revoked = validate_beta_key("VTX-BETA-R8JL-Q4WT", track_activation=False)
|
| 15 |
+
unknown = validate_beta_key("VTX-BETA-NOT-REAL", track_activation=False)
|
| 16 |
+
empty = validate_beta_key("", track_activation=False)
|
| 17 |
+
|
| 18 |
+
assert active == {"valid": True, "active": True, "status": "ACTIVE"}, active
|
| 19 |
+
assert revoked == {"valid": True, "active": False, "status": "REVOKED"}, revoked
|
| 20 |
+
assert unknown == {"valid": False, "active": False, "status": "INVALID"}, unknown
|
| 21 |
+
assert empty == {"valid": False, "active": False, "status": "INVALID"}, empty
|
| 22 |
+
|
| 23 |
+
print("OK")
|