Agentic schematic validation: datasheet extraction via Claude Console Skills, netlist/BOM design graph, per-IC direct datasheet review with page citations, capacitor derating, Next.js report UI. Extracted from the Pinscope cloud codebase. Auth and billing live in the private gateway repo behind stable seams (billing_hook.py, adapter files listed in CLAUDE.md).
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
"""Onboarding survey — appends responses to a Google Sheet and tracks completion."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from backend.config import settings
|
|
from backend.services.storage import StorageBackend
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_SURVEY_PREFIX = "admin/survey/"
|
|
|
|
|
|
def _status_key(user_id: str) -> str:
|
|
return f"{_SURVEY_PREFIX}{user_id}.json"
|
|
|
|
|
|
def is_completed(storage: StorageBackend, user_id: str) -> bool:
|
|
return storage.exists(_status_key(user_id))
|
|
|
|
|
|
def _mark_completed(storage: StorageBackend, user_id: str) -> None:
|
|
payload = {"completed": True, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
storage.write_json(_status_key(user_id), payload)
|
|
|
|
|
|
def _build_sheets_service():
|
|
"""Build an authenticated Google Sheets API service.
|
|
|
|
On Cloud Run, google.auth.default() returns Compute Engine credentials
|
|
which are auto-scoped. We just need the Sheets API enabled in the GCP
|
|
project and the service account shared on the sheet.
|
|
"""
|
|
try:
|
|
import google.auth
|
|
from googleapiclient.discovery import build
|
|
except ImportError:
|
|
logger.warning("google-api-python-client not installed; survey sheet disabled")
|
|
return None
|
|
|
|
try:
|
|
credentials, project = google.auth.default()
|
|
logger.debug("Sheets: credentials type=%s project=%s", type(credentials).__name__, project)
|
|
# Compute Engine credentials don't need explicit scopes — they use
|
|
# the access scopes set on the instance (which default to cloud-platform).
|
|
# For user/SA key credentials, we need to scope them.
|
|
if hasattr(credentials, "with_scopes"):
|
|
credentials = credentials.with_scopes(
|
|
["https://www.googleapis.com/auth/spreadsheets"]
|
|
)
|
|
return build("sheets", "v4", credentials=credentials, cache_discovery=False)
|
|
except Exception:
|
|
logger.exception("Could not build Sheets service")
|
|
return None
|
|
|
|
|
|
async def append_to_sheet(
|
|
user_id: str,
|
|
email: str,
|
|
name: str,
|
|
referral_source: str,
|
|
user_profile: str,
|
|
) -> bool:
|
|
"""Append a survey row to the configured Google Sheet. Returns True on success."""
|
|
sheet_id = settings.survey_sheet_id
|
|
if not sheet_id:
|
|
logger.warning("SURVEY_SHEET_ID not set; skipping sheet append for user %s", user_id)
|
|
return False
|
|
|
|
service = _build_sheets_service()
|
|
if not service:
|
|
return False
|
|
|
|
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
row = [timestamp, user_id, email, name, referral_source, user_profile]
|
|
|
|
try:
|
|
await asyncio.to_thread(
|
|
service.spreadsheets()
|
|
.values()
|
|
.append(
|
|
spreadsheetId=sheet_id,
|
|
range="Sheet1!A:F",
|
|
valueInputOption="RAW",
|
|
insertDataOption="INSERT_ROWS",
|
|
body={"values": [row]},
|
|
)
|
|
.execute
|
|
)
|
|
logger.info("Survey response appended for user %s", user_id)
|
|
return True
|
|
except Exception:
|
|
logger.exception("Failed to append survey response to Google Sheet for user %s", user_id)
|
|
return False
|