File size: 6,297 Bytes
c49b21b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
"""
Push all variables from a .env file into a Hugging Face Space as secrets (or variables).
Requirements:
- huggingface_hub (Python SDK)
Install: pip install -U huggingface_hub
Usage examples:
python scripts/push_hf_secrets.py --repo your-username/your-space
python scripts/push_hf_secrets.py --repo your-username/your-space --env .env.production
python scripts/push_hf_secrets.py --repo your-username/your-space --dry-run
python scripts/push_hf_secrets.py --repo your-username/your-space --as-variables # send as public variables
Notes:
- This script is intentionally simple and cross-platform.
- It parses common .env formats (KEY=VALUE, supports quoted values and export prefix).
- It won’t print secret values; only key names are logged.
- "Secrets" are private; "Variables" are public. See: Settings → Secrets and variables
"""
from __future__ import annotations
import argparse
import os
import re
import sys
from typing import Dict, Tuple
ENV_LINE_RE = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$")
def _unquote(value: str) -> str:
"""Strip matching single or double quotes and unescape simple escapes for double quotes.
- If value is wrapped in double quotes, unescape common sequences (\\n, \\r, \\t, \\" , \\\\).
- If wrapped in single quotes, return inner content as-is (no escapes processing).
- Otherwise, return value trimmed of surrounding whitespace.
"""
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
quote = value[0]
inner = value[1:-1]
if quote == '"':
# Process simple escape sequences
inner = (
inner.replace(r"\\n", "\n")
.replace(r"\\r", "\r")
.replace(r"\\t", "\t")
.replace(r"\\\"", '"')
.replace(r"\\\\", "\\")
)
return inner
return value.strip()
def parse_env_file(path: str) -> Dict[str, str]:
"""Parse a .env-like file into a dict of {KEY: VALUE}.
Skips blank lines and comments (lines starting with #, ignoring leading whitespace).
Supports lines like:
- KEY=VALUE
- export KEY=VALUE
Values can be quoted with single or double quotes.
"""
if not os.path.isfile(path):
raise FileNotFoundError(f".env file not found: {path}")
env: Dict[str, str] = {}
with open(path, "r", encoding="utf-8-sig") as f:
for idx, raw in enumerate(f, start=1):
line = raw.rstrip("\n\r")
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
m = ENV_LINE_RE.match(line)
if not m:
# Non-fatal: skip lines that don't match KEY=VALUE
continue
key, raw_val = m.group(1), m.group(2).strip()
# If value is unquoted, do not strip inline comments aggressively to avoid breaking tokens.
value = _unquote(raw_val)
env[key] = value
return env
def get_hf_api():
"""Return an authenticated HfApi client or None with a helpful error.
Uses locally saved token if you previously ran `huggingface-cli login` or
set HF_TOKEN environment variable.
"""
try:
from huggingface_hub import HfApi
except Exception:
sys.stderr.write(
"huggingface_hub is not installed. Install with: pip install -U huggingface_hub\n"
)
return None
return HfApi()
def set_secret(api, repo: str, key: str, value: str, dry_run: bool = False) -> int:
if dry_run:
print(f"[DRY RUN] Set secret: {key} -> (hidden) on {repo}")
return 0
try:
api.add_space_secret(repo_id=repo, key=key, value=value)
print(f"Set secret: {key}")
return 0
except Exception as e:
sys.stderr.write(f"Error setting secret {key!r} for repo {repo!r}: {e}\n")
return 1
def set_variable(api, repo: str, key: str, value: str, dry_run: bool = False) -> int:
if dry_run:
print(f"[DRY RUN] Set variable: {key} -> (hidden) on {repo}")
return 0
try:
api.add_space_variable(repo_id=repo, key=key, value=value)
print(f"Set variable: {key}")
return 0
except Exception as e:
sys.stderr.write(f"Error setting variable {key!r} for repo {repo!r}: {e}\n")
return 1
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Push .env variables to a Hugging Face Space as secrets or variables.")
parser.add_argument("--repo", required=True, help="Space repo id, e.g. your-username/your-space")
parser.add_argument("--env", default=".env", help="Path to .env file (default: .env)")
parser.add_argument("--dry-run", action="store_true", help="Print what would be set without applying changes")
parser.add_argument(
"--as-variables",
action="store_true",
help="Send entries as public variables instead of private secrets",
)
parser.add_argument(
"--exclude",
action="append",
default=[],
help="Key(s) to exclude (can be repeated)",
)
args = parser.parse_args(argv)
api = get_hf_api()
if api is None:
return 127
try:
env_map = parse_env_file(args.env)
except Exception as e:
sys.stderr.write(f"Failed to read env file {args.env}: {e}\n")
return 2
if not env_map:
print("No variables found in .env; nothing to do.")
return 0
excluded = set(args.exclude or [])
total = 0
failures = 0
for key, value in env_map.items():
if key in excluded:
continue
total += 1
if args.as_variables:
rc = set_variable(api, args.repo, key, value, args.dry_run)
else:
rc = set_secret(api, args.repo, key, value, args.dry_run)
if rc != 0:
failures += 1
if failures:
sys.stderr.write(f"Completed with {failures}/{total} failures.\n")
return 1
print(f"Completed: {total} secrets {'validated' if args.dry_run else 'set'} for {args.repo}.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|