Pinscope open-source core
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).
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user