#!/usr/bin/env python3 """ ПУБЛИКАЦИЯ ИСХОДНИКОВ — канал доставки кода на арену. ЗАЧЕМ отдельный проект, а не зеркало grid-mirror: деплой Vercel заменяет НАБОР ФАЙЛОВ целиком. Пушер зеркала (push_to_vercel.py) гоняется по таймеру каждые 15 минут и снёс бы всё, чего нет в его списке. Отдельный проект grid-src живёт своей жизнью и обновляется только руками. ЗАЧЕМ вообще: SSH на арену закрыт, файлы туда попадают через `gcloud compute ssh --tunnel-through-iap` + curl. Печатать 10 КБ кода в терминал через браузер нельзя — символы теряются. python3 publish_src.py # опубликовать все *.py python3 publish_src.py price_sources.py # только указанные На арене: curl -fsSL https://grid-src.vercel.app/price_sources.py -o /tmp/x.py wc -c /tmp/x.py # ← ОБЯЗАТЕЛЬНО: 404-страница тоже скачается «успешно» """ import base64, hashlib, json, os, sys, time, urllib.request HERE = os.path.dirname(os.path.abspath(__file__)) TOKEN = os.environ.get("VERCEL_TOKEN", "vcp_8bLq4YMdEIJXCSUdfmjdmJ4DubQ0izZTHsTkShJqVTuQQPqGll1KbTq6") TEAM = "team_oVflHvk6poDyQRHW88LCTcg8" PROJECT = "grid-src" ALIAS = "grid-src.vercel.app" def api(url, body=None, method="GET"): req = urllib.request.Request( url, data=json.dumps(body).encode() if body else None, method=method, headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=90) as r: return json.load(r) def main(): want = sys.argv[1:] names = want or sorted(f for f in os.listdir(HERE) if f.endswith(".py") or f == "symbols.json") files, manifest = {}, [] for n in names: p = os.path.join(HERE, n) if not os.path.isfile(p): print(f" ⚠️ нет файла {n}"); continue raw = open(p, "rb").read() files[n] = base64.b64encode(raw).decode() manifest.append({"file": n, "bytes": len(raw), "sha256": hashlib.sha256(raw).hexdigest()[:16]}) print(f" + {n:24} {len(raw):>7} байт sha {manifest[-1]['sha256']}") if not files: sys.exit("нечего публиковать") # манифест нужен, чтобы на арене можно было СВЕРИТЬ скачанное, а не верить files["manifest.json"] = base64.b64encode( json.dumps({"published_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "files": manifest}, indent=1).encode()).decode() index = ("
сверка: manifest.json
") files["index.html"] = base64.b64encode(index.encode()).decode() # text/plain, иначе браузер и curl получают разное files["vercel.json"] = base64.b64encode(json.dumps({ "cleanUrls": False, "headers": [{"source": "/(.*).(py|json)", "headers": [ {"key": "Content-Type", "value": "text/plain; charset=utf-8"}, {"key": "Access-Control-Allow-Origin", "value": "*"}, {"key": "Cache-Control", "value": "no-store"}]}]}).encode()).decode() fl = [{"file": k, "data": v, "encoding": "base64"} for k, v in files.items()] d = api(f"https://api.vercel.com/v13/deployments?teamId={TEAM}&forceNew=1", {"name": PROJECT, "files": fl, "target": "production", "projectSettings": {"framework": None, "buildCommand": "", "outputDirectory": ".", "installCommand": ""}}, "POST") did = d["id"] for _ in range(60): st = api(f"https://api.vercel.com/v13/deployments/{did}?teamId={TEAM}")["readyState"] if st == "READY": break if st in ("ERROR", "CANCELED"): sys.exit(f"деплой упал: {st}") time.sleep(3) api(f"https://api.vercel.com/v2/deployments/{did}/aliases?teamId={TEAM}", {"alias": ALIAS}, "POST") print(f"\n✅ https://{ALIAS} ({len(manifest)} файлов, {did[:18]}…)") if __name__ == "__main__": main()