quranic-universal-audio / scripts /devenv /bootstrap_dev_env.py
hetchyy's picture
deploy: inspector prod @ c16da46
50ff870 verified
Raw
History Blame Contribute Delete
9.52 kB
#!/usr/bin/env python3
"""Tier-1/2 onboarding: provision a personal bucket + Space under *your* HF
account, so you get full backend/DB/bucket behaviour isolated from everyone
else.
Why isolation is mandatory: the Inspector's SQLite DB syncs full-file to its
bucket, so two people sharing one bucket clobber each other's state. Every
contributor gets their own bucket.
What this does (all under your own namespace, using your own token):
1. Create a private bucket ``<you>/quranic-inspector-<name>``.
2. Seed it from the public fixtures dataset (Hub copy; small files round-trip
through the local cache, large Xet files copy server-side).
3. Create a private docker Space ``<you>/quranic-inspector-<name>``.
4. Attach the bucket as a Space volume at ``/data/inspector-bucket`` (where the
image expects it) — so there's no manual "Attach bucket" click in the UI.
5. Set the Space's secrets/variables:
- ``HF_TOKEN`` (secret) — lets the Space read/write your bucket
- ``INSPECTOR_SESSION_SECRET`` (secret, auto-generated) — signs cookies
- ``INSPECTOR_BUCKET_REPO`` (variable) — points the Space at your bucket
OAuth client credentials are injected automatically by HF because the Space
README sets ``hf_oauth: true`` — you never register an OAuth app.
6. (optional, --deploy) push the current code to the Space.
End to end, the only thing you do by hand is create an HF token — no clicking
in the HF UI to clone/duplicate Spaces or buckets, attach storage, or enter
secrets.
Usage::
python scripts/devenv/bootstrap_dev_env.py myname # provision
python scripts/devenv/bootstrap_dev_env.py myname --deploy # + push code
python scripts/devenv/bootstrap_dev_env.py myname --dry-run # show plan only
python scripts/devenv/bootstrap_dev_env.py myname --teardown # delete both
Auth: reads ``HF_TOKEN`` (or ``INSPECTOR_HF_TOKEN``) from env or repo-root
``.env``. Tip: use a token scoped to your own repos.
"""
from __future__ import annotations
import argparse
import os
import secrets
import sys
from pathlib import Path
# Windows consoles default to cp1252, which can't encode characters like the
# arrow used in help text — force UTF-8 so output never crashes cross-platform.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except Exception:
pass
# Seeding the bucket (copy_files) routes non-Xet files through the local HF
# cache, which uses symlinks. On Windows without Developer Mode/admin that
# raises WinError 1314 ("required privilege is not held"). Disabling symlinks
# makes the cache copy real files — required for the one-command bootstrap to
# work cross-platform.
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS", "1")
_REPO_ROOT = Path(__file__).resolve().parents[2]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from qua_shared._env import load_repo_env # noqa: E402
FIXTURES_DATASET = os.environ.get(
"INSPECTOR_FIXTURES_DATASET", "hetchyy/quranic-inspector-fixtures"
)
def _resolve_token() -> str:
load_repo_env()
token = os.environ.get("INSPECTOR_HF_TOKEN") or os.environ.get("HF_TOKEN")
if not token:
raise SystemExit(
"HF_TOKEN / INSPECTOR_HF_TOKEN missing. Add it to .env at the repo "
"root or export it. Create one at https://huggingface.co/settings/tokens"
)
return token
def _whoami(api) -> str:
info = api.whoami()
name = info.get("name") if isinstance(info, dict) else None
if not name:
raise SystemExit("Could not resolve your HF username from the token.")
return name
def _provision(api, *, user: str, name: str, deploy: bool, public_bucket: bool) -> None:
from huggingface_hub import Volume, copy_files, create_bucket
bucket_id = f"{user}/quranic-inspector-{name}"
space_id = f"{user}/quranic-inspector-{name}"
print(f"==> [1/6] Creating bucket {bucket_id} (private={not public_bucket})")
create_bucket(f"quranic-inspector-{name}", private=not public_bucket, exist_ok=True)
print(f"==> [2/6] Seeding bucket from {FIXTURES_DATASET}")
copy_files(
f"hf://datasets/{FIXTURES_DATASET}/",
f"hf://buckets/{bucket_id}/",
)
print(f"==> [3/6] Creating Space {space_id} (private)")
api.create_repo(
repo_id=space_id,
repo_type="space",
space_sdk="docker",
private=True,
exist_ok=True,
)
# The Docker image expects the bucket NFS-mounted at /data/inspector-bucket
# (INSPECTOR_BUCKET_MOUNT). Attach it programmatically so there's no manual
# "Attach bucket" step in the Space UI.
print("==> [4/6] Attaching bucket volume at /data/inspector-bucket")
api.set_space_volumes(
space_id,
volumes=[Volume(type="bucket", source=bucket_id, mount_path="/data/inspector-bucket")],
)
print("==> [5/6] Setting Space secrets + variables")
session_secret = secrets.token_hex(32)
token = api.token
api.add_space_secret(
space_id, "HF_TOKEN", token, description="Bucket access for this dev Space"
)
api.add_space_secret(
space_id,
"INSPECTOR_SESSION_SECRET",
session_secret,
description="Cookie-signing key (auto-generated)",
)
api.add_space_variable(
space_id, "INSPECTOR_BUCKET_REPO", bucket_id, description="This dev's isolated bucket"
)
if deploy:
print("==> [6/6] Deploying current code to the Space")
import subprocess
deploy_script = _REPO_ROOT / "scripts" / "deploy" / "deploy_space.py"
subprocess.run([sys.executable, str(deploy_script), space_id], check=True)
else:
print("==> [6/6] Skipping deploy. Push code when ready with:")
print(f" python scripts/deploy/deploy_space.py {space_id}")
print("\n==> Done.")
print(f" Bucket : hf://buckets/{bucket_id}")
print(f" Space : https://huggingface.co/spaces/{space_id}")
print("\n For LOCAL Tier-1 dev against this bucket, set in your .env:")
print(f" INSPECTOR_BUCKET_REPO={bucket_id}")
print(" HF_TOKEN=<your token>")
def _teardown(api, *, user: str, name: str) -> None:
from huggingface_hub import delete_bucket
bucket_id = f"{user}/quranic-inspector-{name}"
space_id = f"{user}/quranic-inspector-{name}"
print(f"==> Deleting Space {space_id}")
try:
api.delete_repo(repo_id=space_id, repo_type="space", missing_ok=True)
except TypeError:
api.delete_repo(repo_id=space_id, repo_type="space")
print(f"==> Deleting bucket {bucket_id} (PERMANENT)")
delete_bucket(bucket_id, missing_ok=True)
print("==> Teardown complete.")
def _print_plan(*, user: str, name: str, deploy: bool, teardown: bool) -> None:
bid = f"{user}/quranic-inspector-{name}"
if teardown:
print("DRY RUN — would delete:")
print(f" - Space {bid}")
print(f" - bucket {bid}")
return
print("DRY RUN — would create under your account:")
print(f" - bucket {bid} (private)")
print(f" seeded from hf://datasets/{FIXTURES_DATASET}/")
print(f" - Space {bid} (private, docker, hf_oauth)")
print(f" volume: hf://buckets/{bid} -> /data/inspector-bucket")
print(" secrets: HF_TOKEN, INSPECTOR_SESSION_SECRET")
print(f" variable: INSPECTOR_BUCKET_REPO={bid}")
print(f" - deploy code: {'yes' if deploy else 'no (run deploy_space.py later)'}")
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
p.add_argument(
"name", help="Short suffix for your env, e.g. 'alice' -> quranic-inspector-alice"
)
p.add_argument("--deploy", action="store_true", help="Also push code to the Space now.")
p.add_argument(
"--public-bucket", action="store_true", help="Make the bucket public (default: private)."
)
p.add_argument("--teardown", action="store_true", help="Delete the bucket + Space.")
p.add_argument("--dry-run", action="store_true", help="Print the plan; change nothing.")
args = p.parse_args(argv)
if not args.name.replace("-", "").replace("_", "").isalnum():
raise SystemExit(f"name must be alphanumeric (got {args.name!r})")
token = _resolve_token()
from huggingface_hub import HfApi
api = HfApi(token=token)
user = _whoami(api)
if args.dry_run:
_print_plan(user=user, name=args.name, deploy=args.deploy, teardown=args.teardown)
return 0
if args.teardown:
_teardown(api, user=user, name=args.name)
else:
try:
_provision(
api, user=user, name=args.name, deploy=args.deploy, public_bucket=args.public_bucket
)
except Exception:
# Provisioning is multi-step; a mid-flight failure can leave a
# partially-created bucket/Space. Don't auto-delete (the bucket may
# hold data), but point at the idempotent re-run and the teardown.
print(
f"\n!! Provisioning failed partway. Re-run "
f"`bootstrap_dev_env.py {args.name}` to resume (idempotent), or "
f"`bootstrap_dev_env.py {args.name} --teardown` to remove what "
f"was created.",
file=sys.stderr,
)
raise
return 0
if __name__ == "__main__":
sys.exit(main())