""" Google Cloud Manager - Service Account Access ============================================== Manages Google Cloud service account credentials and provides access to Vertex AI, Cloud Storage, BigQuery, and other services. All burning only free credits / free tier. Service account: cryptorugmunch@appspot.gserviceaccount.com Project: cryptorugmunch Key vault: /root/.secrets/google-sa-cryptorugmunch.json Available services: - Vertex AI embedding (768d) - separate quota from AI Studio - Cloud Storage (5GB free) - RAG backups, model storage - BigQuery (1TB/mo free) - wallet analytics - Cloud Run (2M req/mo free) - optional hosting """ import json import logging import os logger = logging.getLogger("gcloud") SA_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "cryptorugmunch-sa.json") VAULT_PATH = "/root/.secrets/google-sa-cryptorugmunch.json" class GoogleCloudManager: """Manages Google Cloud credentials and service access.""" def __init__(self): self._sa: dict | None = None self._token: str | None = None self._token_expiry: float = 0 def _load_sa(self) -> dict: """Load service account from file (tries vault first, then data dir).""" for path in [VAULT_PATH, SA_PATH]: if os.path.exists(path): with open(path) as f: return json.load(f) raise FileNotFoundError("No Google service account key found") def _get_token(self) -> str: """Get fresh OAuth access token from service account.""" import time from google.auth.transport.requests import Request from google.oauth2 import service_account if self._token and time.time() < self._token_expiry: return self._token if not self._sa: self._sa = self._load_sa() creds = service_account.Credentials.from_service_account_info( self._sa, scopes=["https://www.googleapis.com/auth/cloud-platform"] ) creds.refresh(Request()) self._token = creds.token self._token_expiry = time.time() + 3000 # ~50 min return self._token def get_headers(self) -> dict[str, str]: return { "Authorization": f"Bearer {self._get_token()}", "Content-Type": "application/json", } async def embed_vertex(self, texts: list) -> list | None: """Embed using Vertex AI (768d, Cloud credits).""" import httpx try: async with httpx.AsyncClient(timeout=15) as c: vectors = [] for text in texts: r = await c.post( "https://us-central1-aiplatform.googleapis.com/v1/projects/cryptorugmunch/locations/us-central1/publishers/google/models/text-embedding-004:predict", headers=self.get_headers(), json={"instances": [{"content": text}]}, ) if r.status_code == 200: data = r.json() emb = data.get("predictions", [{}])[0].get("embeddings", {}) vec = emb.get("values", emb.get("statistics", {}).get("values", [])) if vec: vectors.append(vec) elif r.status_code == 429: logger.warning("Vertex AI rate limited") return None else: logger.warning(f"Vertex AI HTTP {r.status_code}") return None return vectors if vectors else None except Exception as e: logger.warning(f"Vertex AI: {e}") return None async def list_buckets(self) -> list: """List Cloud Storage buckets.""" import httpx try: async with httpx.AsyncClient(timeout=10) as c: r = await c.get( "https://storage.googleapis.com/storage/v1/b?project=cryptorugmunch", headers=self.get_headers(), ) if r.status_code == 200: return [b.get("name", "") for b in r.json().get("items", [])] except Exception: pass return [] async def upload_to_storage( self, bucket: str, path: str, data: bytes, content_type: str = "application/octet-stream" ) -> bool: """Upload file to Cloud Storage.""" import httpx try: async with httpx.AsyncClient(timeout=30) as c: r = await c.post( f"https://storage.googleapis.com/upload/storage/v1/b/{bucket}/o?uploadType=media&name={path}", headers={**self.get_headers(), "Content-Type": content_type}, content=data, ) return r.status_code in (200, 201) except Exception: return False # ── BigQuery ──────────────────────────────────────────── async def bigquery_insert(self, dataset: str, table: str, rows: list) -> bool: """Streaming insert into BigQuery. Uses free tier (1TB queries/mo).""" import httpx try: async with httpx.AsyncClient(timeout=15) as c: r = await c.post( f"https://bigquery.googleapis.com/bigquery/v2/projects/cryptorugmunch/datasets/{dataset}/tables/{table}/insertAll", headers=self.get_headers(), json={"rows": [{"json": row} for row in rows]}, ) if r.status_code == 200: errors = r.json().get("insertErrors", []) if errors: logger.warning(f"BigQuery insert errors: {len(errors)}") return len(errors) == 0 elif r.status_code == 404: logger.info(f"BigQuery table {dataset}.{table} not found - needs creation") else: logger.warning(f"BigQuery insert HTTP {r.status_code}") return False except Exception as e: logger.warning(f"BigQuery insert: {e}") return False async def bigquery_query(self, sql: str) -> list: """Run a BigQuery query. Free tier: 1TB/month.""" import httpx try: async with httpx.AsyncClient(timeout=30) as c: r = await c.post( "https://bigquery.googleapis.com/bigquery/v2/projects/cryptorugmunch/queries", headers=self.get_headers(), json={"query": sql, "useLegacySql": False}, ) if r.status_code == 200: data = r.json() rows = [] schema = [f.get("name") for f in data.get("schema", {}).get("fields", [])] for row in data.get("rows", []): vals = [cell.get("v", "") for cell in row.get("f", [])] rows.append(dict(zip(schema, vals, strict=False))) return rows else: logger.warning(f"BigQuery query HTTP {r.status_code}") return [] except Exception as e: logger.warning(f"BigQuery query: {e}") return [] # ── Cloud Vision ─────────────────────────────────────── async def vision_ocr(self, image_url: str | None = None, image_bytes: bytes | None = None) -> str: """OCR text from image. Free tier: 1,000 units/month.""" import httpx try: body = {"requests": [{"features": [{"type": "TEXT_DETECTION"}]}]} if image_url: body["requests"][0]["image"] = {"source": {"imageUri": image_url}} elif image_bytes: import base64 body["requests"][0]["image"] = {"content": base64.b64encode(image_bytes).decode()} else: return "" async with httpx.AsyncClient(timeout=15) as c: r = await c.post( "https://vision.googleapis.com/v1/images:annotate", headers=self.get_headers(), json=body, ) if r.status_code == 200: resp = r.json().get("responses", [{}])[0] return resp.get("fullTextAnnotation", {}).get("text", "") elif r.status_code == 403: logger.info("Cloud Vision API not enabled") else: logger.warning(f"Vision HTTP {r.status_code}") return "" except Exception as e: logger.warning(f"Vision: {e}") return "" async def vision_labels(self, image_url: str | None = None, image_bytes: bytes | None = None) -> list: """Detect labels in image. Free tier: 1,000 units/month.""" import httpx try: body = {"requests": [{"features": [{"type": "LABEL_DETECTION", "maxResults": 10}]}]} if image_url: body["requests"][0]["image"] = {"source": {"imageUri": image_url}} elif image_bytes: import base64 body["requests"][0]["image"] = {"content": base64.b64encode(image_bytes).decode()} else: return [] async with httpx.AsyncClient(timeout=15) as c: r = await c.post( "https://vision.googleapis.com/v1/images:annotate", headers=self.get_headers(), json=body, ) if r.status_code == 200: annotations = r.json().get("responses", [{}])[0].get("labelAnnotations", []) return [a.get("description", "") for a in annotations] return [] except Exception as e: logger.warning(f"Vision labels: {e}") return [] # ── Natural Language ─────────────────────────────────── async def nl_extract_entities(self, text: str) -> list: """Extract named entities from text. Free tier: 5,000 units/month.""" import httpx try: async with httpx.AsyncClient(timeout=15) as c: r = await c.post( "https://language.googleapis.com/v1/documents:analyzeEntities", headers=self.get_headers(), json={"document": {"type": "PLAIN_TEXT", "content": text[:100000]}}, ) if r.status_code == 200: entities = r.json().get("entities", []) return [ { "name": e.get("name", ""), "type": e.get("type", ""), "salience": e.get("salience", 0), "metadata": e.get("metadata", {}), } for e in entities ] elif r.status_code == 403: logger.info("Natural Language API not enabled") return [] except Exception as e: logger.warning(f"NL: {e}") return [] async def nl_sentiment(self, text: str) -> dict: """Analyze sentiment. Free tier: 5,000 units/month.""" import httpx try: async with httpx.AsyncClient(timeout=15) as c: r = await c.post( "https://language.googleapis.com/v1/documents:analyzeSentiment", headers=self.get_headers(), json={"document": {"type": "PLAIN_TEXT", "content": text[:100000]}}, ) if r.status_code == 200: sentiment = r.json().get("documentSentiment", {}) return { "score": sentiment.get("score", 0), "magnitude": sentiment.get("magnitude", 0), } return {"score": 0, "magnitude": 0} except Exception as e: logger.warning(f"NL sentiment: {e}") return {"score": 0, "magnitude": 0} # ── Cloud Storage lifecycle ──────────────────────────── async def set_lifecycle(self, bucket: str, rules: list) -> bool: """Set lifecycle rules on bucket to stay under 5GB free tier.""" import httpx try: async with httpx.AsyncClient(timeout=15) as c: r = await c.patch( f"https://storage.googleapis.com/storage/v1/b/{bucket}?fields=lifecycle", headers=self.get_headers(), json={"lifecycle": {"rule": rules}}, ) return r.status_code == 200 except Exception: return False def health(self) -> dict: """Quick health check.""" try: self._sa = self._load_sa() return { "authenticated": True, "project": self._sa.get("project_id", "?"), "email": self._sa.get("client_email", "?"), } except Exception as e: return {"authenticated": False, "error": str(e)} _gcloud: GoogleCloudManager | None = None def get_gcloud() -> GoogleCloudManager: global _gcloud if _gcloud is None: _gcloud = GoogleCloudManager() return _gcloud