Spaces:
Running on Zero
Running on Zero
Claude Opus 5 (1M context)
refactor!: hosted-model-only core, Drive report delivery, shared harnesses
79cf149 Download scripts/gdrive_auth.py from StavanShah01/grownxt-server: direct link, hf CLI and curl.
- Browser
- Download file 9.64 kB
-
https://huggingface.co/spaces/StavanShah01/grownxt-server/resolve/main/scripts/gdrive_auth.py
- Command line
-
hf download hf://spaces/StavanShah01/grownxt-server/scripts/gdrive_auth.py
-
curl -L -o gdrive_auth.py https://huggingface.co/spaces/StavanShah01/grownxt-server/resolve/main/scripts/gdrive_auth.py
9.64 kB
| """One-time Google Drive authorisation, and a health check for the stored grant. | |
| Run once to mint a refresh token: | |
| venv/Scripts/python.exe -m scripts.gdrive_auth | |
| The browser opens Google's consent screen, the code comes back to a loopback | |
| server on this machine, and the refresh token is written to ``.gdrive_token.json`` | |
| (git-ignored). The server picks it up from there on its next request -- nothing | |
| has to be pasted into an environment variable. | |
| Check an existing grant without touching the consent screen: | |
| venv/Scripts/python.exe -m scripts.gdrive_auth --check | |
| Why this exists at all: access tokens are refreshed silently by | |
| ``storage/gdrive.py``, but a *refresh* token can only be minted with a human at | |
| the consent screen. Google issues refresh tokens with a 7-day life while the | |
| OAuth app is in Testing status, so publish the consent screen to Production and | |
| this command becomes a once-ever step rather than a weekly chore. | |
| """ | |
| import argparse | |
| import http.server | |
| import logging | |
| import os | |
| from pathlib import Path | |
| import socket | |
| import sys | |
| import threading | |
| import time | |
| import urllib.parse | |
| import urllib.request | |
| import webbrowser | |
| from dotenv import load_dotenv | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| from scripts import cli # noqa: E402 | |
| from storage.gdrive import ( # noqa: E402 | |
| SCOPE, | |
| DriveAuthError, | |
| DriveError, | |
| DriveStore, | |
| _stored_token, | |
| _token_file, | |
| save_refresh_token, | |
| ) | |
| AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" | |
| TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" | |
| logger = logging.getLogger("gdrive_auth") | |
| SETUP_STEPS = """Set GDRIVE_CLIENT_ID and GDRIVE_CLIENT_SECRET first. | |
| 1. https://console.cloud.google.com/ -> new project | |
| 2. APIs & Services -> Library -> enable 'Google Drive API' | |
| 3. OAuth consent screen -> External -> add yourself as a user, then | |
| PUBLISH it (an app left in Testing expires refresh tokens after 7 days) | |
| 4. Credentials -> Create OAuth client ID -> Desktop app | |
| 5. Put the id and secret in .env as GDRIVE_CLIENT_ID / | |
| GDRIVE_CLIENT_SECRET, then run this again | |
| """ | |
| class _CallbackHandler(http.server.BaseHTTPRequestHandler): | |
| """Captures the single redirect Google makes back to this machine.""" | |
| code = None | |
| error = None | |
| def do_GET(self) -> None: # noqa: N802 - name fixed by BaseHTTPRequestHandler | |
| query = urllib.parse.urlparse(self.path).query | |
| params = urllib.parse.parse_qs(query) | |
| _CallbackHandler.code = (params.get("code") or [None])[0] | |
| _CallbackHandler.error = (params.get("error") or [None])[0] | |
| body = ( | |
| b"<html><body style='font-family:sans-serif;padding:2rem'>" | |
| b"<h2>GrowNXT — Google Drive authorised</h2>" | |
| b"<p>You can close this tab and return to the terminal.</p>" | |
| b"</body></html>" | |
| if _CallbackHandler.code | |
| else b"<html><body><h2>Authorisation failed</h2></body></html>" | |
| ) | |
| self.send_response(200) | |
| self.send_header("Content-Type", "text/html; charset=utf-8") | |
| self.send_header("Content-Length", str(len(body))) | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def log_message(self, *args) -> None: | |
| """Silences the default stderr access log.""" | |
| def _free_port() -> int: | |
| """Returns a port the loopback callback server can bind.""" | |
| with socket.socket() as probe: | |
| probe.bind(("127.0.0.1", 0)) | |
| return probe.getsockname()[1] | |
| def _post_form(url: str, fields: dict) -> dict: | |
| """POSTs a form and returns the parsed JSON body.""" | |
| import json | |
| data = urllib.parse.urlencode(fields).encode("utf-8") | |
| request = urllib.request.Request(url, data=data, method="POST") | |
| request.add_header("Content-Type", "application/x-www-form-urlencoded") | |
| try: | |
| with urllib.request.urlopen(request, timeout=60) as response: | |
| return json.loads(response.read().decode("utf-8")) | |
| except urllib.error.HTTPError as exc: | |
| detail = exc.read().decode("utf-8", "replace") | |
| raise SystemExit("Token exchange failed (HTTP %s): %s" % (exc.code, detail)) | |
| def authorise(client_id: str, client_secret: str) -> str: | |
| """Runs the consent flow and returns a refresh token. | |
| Args: | |
| client_id: OAuth client id of a Desktop-type client. | |
| client_secret: Matching client secret. | |
| Returns: | |
| str: The refresh token. | |
| """ | |
| port = _free_port() | |
| redirect_uri = "http://127.0.0.1:%d/" % port | |
| params = { | |
| "client_id": client_id, | |
| "redirect_uri": redirect_uri, | |
| "response_type": "code", | |
| "scope": SCOPE, | |
| # Without these two Google returns no refresh token on a repeat consent. | |
| "access_type": "offline", | |
| "prompt": "consent", | |
| } | |
| auth_url = "%s?%s" % (AUTH_ENDPOINT, urllib.parse.urlencode(params)) | |
| server = http.server.HTTPServer(("127.0.0.1", port), _CallbackHandler) | |
| server.timeout = 5 | |
| def serve_until_answered(deadline: float) -> None: | |
| """Serves requests until the redirect arrives, or time runs out. | |
| Handling a single request is not enough: a browser may prefetch | |
| `/favicon.ico` against the same port, which would consume the one | |
| request and leave the authorisation code unread. | |
| """ | |
| while time.time() < deadline: | |
| if _CallbackHandler.code or _CallbackHandler.error: | |
| return | |
| server.handle_request() | |
| _CallbackHandler.code = None | |
| _CallbackHandler.error = None | |
| deadline = time.time() + 300 | |
| thread = threading.Thread(target=serve_until_answered, args=(deadline,), daemon=True) | |
| thread.start() | |
| print("Opening Google's consent screen. If nothing opens, visit:\n\n%s\n" % auth_url) | |
| try: | |
| webbrowser.open(auth_url) | |
| except Exception: # noqa: BLE001 - a headless box just uses the printed URL | |
| pass | |
| thread.join(timeout=305) | |
| server.server_close() | |
| if _CallbackHandler.error: | |
| raise SystemExit("Google returned an error: %s" % _CallbackHandler.error) | |
| if not _CallbackHandler.code: | |
| raise SystemExit("No authorisation code received within 5 minutes.") | |
| payload = _post_form(TOKEN_ENDPOINT, { | |
| "code": _CallbackHandler.code, | |
| "client_id": client_id, | |
| "client_secret": client_secret, | |
| "redirect_uri": redirect_uri, | |
| "grant_type": "authorization_code", | |
| }) | |
| refresh_token = payload.get("refresh_token") | |
| if not refresh_token: | |
| raise SystemExit( | |
| "Google returned no refresh token. Revoke this app's access at " | |
| "https://myaccount.google.com/permissions and run this again." | |
| ) | |
| return refresh_token | |
| def _report_missing() -> None: | |
| """Names which credential is absent, without printing any of their values.""" | |
| have_id = bool(os.getenv("GDRIVE_CLIENT_ID", "").strip()) | |
| have_secret = bool(os.getenv("GDRIVE_CLIENT_SECRET", "").strip()) | |
| stored = _stored_token() | |
| have_token = bool(stored or os.getenv("GDRIVE_REFRESH_TOKEN", "").strip()) | |
| for label, present in ( | |
| ("GDRIVE_CLIENT_ID", have_id), | |
| ("GDRIVE_CLIENT_SECRET", have_secret), | |
| ("refresh token", have_token), | |
| ): | |
| print(" [%s] %s" % ("ok " if present else " ", label)) | |
| if have_id and have_secret and not have_token: | |
| print( | |
| "\nThe client credentials are in place; what is missing is the " | |
| "refresh token,\nwhich only the consent screen can mint. Run:\n\n" | |
| " venv/Scripts/python.exe -m scripts.gdrive_auth\n" | |
| ) | |
| def check() -> int: | |
| """Verifies the stored grant still mints access tokens. | |
| Returns: | |
| int: Process exit code. | |
| """ | |
| try: | |
| store = DriveStore() | |
| except DriveAuthError: | |
| print("Google Drive is not fully configured:\n") | |
| _report_missing() | |
| return 1 | |
| try: | |
| store.access_token() | |
| except DriveAuthError as exc: | |
| print("GRANT REJECTED: %s" % exc) | |
| return 1 | |
| except DriveError as exc: | |
| print("could not reach Google: %s" % exc) | |
| return 1 | |
| print("ok: the stored refresh token still works (token file: %s)" % _token_file()) | |
| if store.folder_id: | |
| print("uploads target folder %s" % store.folder_id) | |
| else: | |
| print("uploads land in the account root (set GDRIVE_FOLDER_ID to change that)") | |
| return 0 | |
| def _parser() -> argparse.ArgumentParser: | |
| """Builds the argument parser.""" | |
| p = argparse.ArgumentParser(description="Authorise GrowNXT against Google Drive.") | |
| p.add_argument("--check", action="store_true", | |
| help="Verify the stored refresh token instead of re-authorising.") | |
| p.add_argument("--client-id", help="Overrides GDRIVE_CLIENT_ID.") | |
| p.add_argument("--client-secret", help="Overrides GDRIVE_CLIENT_SECRET.") | |
| return p | |
| def main() -> int: | |
| """Runs the consent flow, or checks the grant already stored.""" | |
| args = _parser().parse_args() | |
| cli.setup(logging.INFO, cli.PLAIN) | |
| load_dotenv() | |
| if args.check: | |
| return check() | |
| client_id = args.client_id or os.getenv("GDRIVE_CLIENT_ID", "").strip() | |
| secret = args.client_secret or os.getenv("GDRIVE_CLIENT_SECRET", "").strip() | |
| if not (client_id and secret): | |
| print(SETUP_STEPS, file=sys.stderr) | |
| return 1 | |
| path = save_refresh_token(authorise(client_id, secret)) | |
| print("\nRefresh token saved to %s" % path) | |
| print("The server reads it from there; no restart needed for the next request.") | |
| return check() | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |