- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
198 lines
7.1 KiB
Python
198 lines
7.1 KiB
Python
"""
|
|
Multi-Account Google Cloud Manager
|
|
====================================
|
|
Drop service account JSON files into /root/.secrets/google/ -
|
|
the system auto-discovers them, manages tokens, and provides
|
|
unified access across all accounts. No console needed ever again.
|
|
|
|
Usage:
|
|
from app.gcloud_multi import get_multi_cloud
|
|
gcloud = get_multi_cloud()
|
|
|
|
# Auto-discovers all accounts
|
|
accounts = await gcloud.discover()
|
|
|
|
# Use best account for a task
|
|
result = await gcloud.best_embed("scam detection text")
|
|
|
|
# List all available services across accounts
|
|
status = await gcloud.all_status()
|
|
"""
|
|
|
|
import glob
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
|
|
import httpx
|
|
from google.auth.transport.requests import Request
|
|
from google.oauth2 import service_account
|
|
|
|
logger = logging.getLogger("gcloud.multi")
|
|
|
|
ACCOUNTS_DIR = "/root/.secrets/google"
|
|
os.makedirs(ACCOUNTS_DIR, exist_ok=True)
|
|
|
|
|
|
@dataclass
|
|
class GCloudAccount:
|
|
"""Single Google Cloud account managed via service account."""
|
|
|
|
name: str
|
|
project_id: str
|
|
email: str
|
|
json_path: str
|
|
_token: str | None = None
|
|
_token_expiry: float = 0
|
|
_services: dict[str, bool] = field(default_factory=dict)
|
|
|
|
def _get_token(self) -> str:
|
|
if self._token and time.time() < self._token_expiry:
|
|
return self._token
|
|
with open(self.json_path) as f:
|
|
sa = json.load(f)
|
|
creds = service_account.Credentials.from_service_account_info(
|
|
sa, scopes=["https://www.googleapis.com/auth/cloud-platform"]
|
|
)
|
|
creds.refresh(Request())
|
|
self._token = creds.token
|
|
self._token_expiry = time.time() + 3000
|
|
return self._token
|
|
|
|
def headers(self) -> dict:
|
|
return {"Authorization": f"Bearer {self._get_token()}", "Content-Type": "application/json"}
|
|
|
|
async def test_service(self, url: str) -> bool:
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.get(url, headers=self.headers())
|
|
return r.status_code in (200, 404, 403) # 403=disabled, 404=not found, 200=ok
|
|
except Exception:
|
|
return False
|
|
|
|
async def probe(self) -> dict:
|
|
"""Probe which services are enabled on this account."""
|
|
services = {
|
|
"vertex_ai": "https://us-central1-aiplatform.googleapis.com/v1/projects/{p}/locations/us-central1/endpoints",
|
|
"storage": "https://storage.googleapis.com/storage/v1/b?project={p}",
|
|
"bigquery": "https://bigquery.googleapis.com/bigquery/v2/projects/{p}/datasets",
|
|
"vision": "https://vision.googleapis.com/v1/images:annotate",
|
|
"language": "https://language.googleapis.com/v1/documents:analyzeEntities",
|
|
"run": "https://run.googleapis.com/v1/projects/{p}/locations/us-central1/services",
|
|
}
|
|
results = {}
|
|
for svc, url in services.items():
|
|
results[svc] = await self.test_service(url.replace("{p}", self.project_id))
|
|
self._services = results
|
|
return results
|
|
|
|
async def embed_vertex(self, texts: list) -> list | None:
|
|
"""Embed using Vertex AI on this account."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
vectors = []
|
|
for text in texts:
|
|
r = await c.post(
|
|
f"https://us-central1-aiplatform.googleapis.com/v1/projects/{self.project_id}/locations/us-central1/publishers/google/models/text-embedding-004:predict",
|
|
headers=self.headers(),
|
|
json={"instances": [{"content": text}]},
|
|
)
|
|
if r.status_code == 200:
|
|
d = r.json()
|
|
emb = d.get("predictions", [{}])[0].get("embeddings", {})
|
|
vec = emb.get("values", emb.get("statistics", {}).get("values", []))
|
|
if vec:
|
|
vectors.append(vec)
|
|
elif r.status_code == 429:
|
|
return None
|
|
return vectors if vectors else None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
class MultiCloudManager:
|
|
"""Manages multiple Google Cloud accounts from service account JSONs."""
|
|
|
|
def __init__(self):
|
|
self._accounts: dict[str, GCloudAccount] = {}
|
|
self._discovered = False
|
|
|
|
def discover(self) -> list[GCloudAccount]:
|
|
"""Discover all service account JSONs in the vault directory."""
|
|
self._accounts = {}
|
|
for path in glob.glob(os.path.join(ACCOUNTS_DIR, "*.json")):
|
|
try:
|
|
with open(path) as f:
|
|
sa = json.load(f)
|
|
if sa.get("type") != "service_account":
|
|
continue
|
|
name = os.path.basename(path).replace(".json", "")
|
|
acc = GCloudAccount(
|
|
name=name,
|
|
project_id=sa.get("project_id", "?"),
|
|
email=sa.get("client_email", "?"),
|
|
json_path=path,
|
|
)
|
|
self._accounts[name] = acc
|
|
logger.info(f"Discovered: {acc.project_id} ({acc.email})")
|
|
except Exception as e:
|
|
logger.warning(f"Skipping {path}: {e}")
|
|
|
|
self._discovered = True
|
|
return list(self._accounts.values())
|
|
|
|
@property
|
|
def accounts(self) -> list[GCloudAccount]:
|
|
if not self._discovered:
|
|
self.discover()
|
|
return list(self._accounts.values())
|
|
|
|
async def all_status(self) -> dict:
|
|
"""Status of all discovered accounts."""
|
|
result = {"accounts": [], "total": len(self.accounts)}
|
|
for acc in self.accounts:
|
|
services = await acc.probe()
|
|
result["accounts"].append(
|
|
{
|
|
"name": acc.name,
|
|
"project_id": acc.project_id,
|
|
"email": acc.email,
|
|
"services": {k: v for k, v in services.items() if v},
|
|
"service_count": sum(1 for v in services.values() if v),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def best_embed(self, texts: list, preferred_project: str | None = None) -> tuple[list | None, str]:
|
|
"""Try to embed using the best available account."""
|
|
ordered = self.accounts
|
|
if preferred_project:
|
|
ordered = sorted(ordered, key=lambda a: 0 if a.project_id == preferred_project else 1)
|
|
|
|
for acc in ordered:
|
|
if acc.project_id == "cryptorugmunch": # Primary - always try first
|
|
vecs = await acc.embed_vertex(texts)
|
|
if vecs:
|
|
return vecs, acc.project_id
|
|
|
|
# Fall back to other accounts
|
|
for acc in ordered:
|
|
if acc.project_id != "cryptorugmunch":
|
|
vecs = await acc.embed_vertex(texts)
|
|
if vecs:
|
|
return vecs, acc.project_id
|
|
|
|
return None, "none"
|
|
|
|
|
|
_multi: MultiCloudManager | None = None
|
|
|
|
|
|
def get_multi_cloud() -> MultiCloudManager:
|
|
global _multi
|
|
if _multi is None:
|
|
_multi = MultiCloudManager()
|
|
_multi.discover()
|
|
return _multi
|