Spaces:
Sleeping
Sleeping
Download database/supabase_client.py from yusef75/smart-agent-store: direct link, hf CLI and curl.
- Browser
- Download file 17.7 kB
-
https://huggingface.co/spaces/yusef75/smart-agent-store/resolve/main/database/supabase_client.py
- Command line
-
hf download hf://spaces/yusef75/smart-agent-store/database/supabase_client.py
-
curl -L -o supabase_client.py https://huggingface.co/spaces/yusef75/smart-agent-store/resolve/main/database/supabase_client.py
17.7 kB
| """Supabase client and helper functions for Project Memo. | |
| Tables expected in Supabase (create via the SQL editor): | |
| CREATE TABLE products ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| title TEXT NOT NULL, | |
| subtitle TEXT, | |
| description TEXT, | |
| bullet_points JSONB DEFAULT '[]'::jsonb, | |
| images JSONB DEFAULT '[]'::jsonb, | |
| category TEXT, | |
| tags JSONB DEFAULT '[]'::jsonb, | |
| aliexpress_id TEXT, | |
| aliexpress_url TEXT, | |
| base_cost NUMERIC(10,2) NOT NULL DEFAULT 0, | |
| shipping_cost NUMERIC(10,2) NOT NULL DEFAULT 0, | |
| current_price NUMERIC(10,2) NOT NULL DEFAULT 0, | |
| markup_multiplier NUMERIC(4,2) NOT NULL DEFAULT 2.5, | |
| demand_multiplier NUMERIC(4,2) NOT NULL DEFAULT 1.0, | |
| ai_multiplier NUMERIC(4,2) NOT NULL DEFAULT 1.0, | |
| stock_status TEXT NOT NULL DEFAULT 'in_stock', | |
| rating NUMERIC(3,2) DEFAULT 0, | |
| total_orders INTEGER DEFAULT 0, | |
| is_active BOOLEAN NOT NULL DEFAULT TRUE, | |
| variants JSONB DEFAULT '[]'::jsonb, | |
| seo_keywords JSONB DEFAULT '[]'::jsonb, | |
| score INTEGER DEFAULT 0 | |
| ); | |
| CREATE TABLE orders ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| customer_name TEXT NOT NULL, | |
| customer_email TEXT NOT NULL, | |
| shipping_address JSONB, | |
| items JSONB NOT NULL, | |
| subtotal NUMERIC(10,2) NOT NULL, | |
| shipping_total NUMERIC(10,2) NOT NULL DEFAULT 0, | |
| paypal_fee NUMERIC(10,2) NOT NULL DEFAULT 0, | |
| total NUMERIC(10,2) NOT NULL, | |
| status TEXT NOT NULL DEFAULT 'pending', | |
| paypal_order_id TEXT, | |
| tracking_number TEXT, | |
| notes TEXT | |
| ); | |
| CREATE TABLE analytics ( | |
| id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| product_id UUID REFERENCES products(id) ON DELETE SET NULL, | |
| event_type TEXT NOT NULL, | |
| source TEXT DEFAULT 'direct', | |
| metadata JSONB DEFAULT '{}'::jsonb | |
| ); | |
| CREATE TABLE agent_status ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| agent_name TEXT NOT NULL UNIQUE, | |
| status TEXT NOT NULL DEFAULT 'idle', | |
| last_run TIMESTAMPTZ, | |
| runs_today INTEGER DEFAULT 0, | |
| errors_today INTEGER DEFAULT 0, | |
| metadata JSONB DEFAULT '{}'::jsonb | |
| ); | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import re | |
| import uuid | |
| from datetime import datetime, timezone | |
| from typing import Any | |
| from config import get_settings | |
| logger = logging.getLogger("memo.db") | |
| # ββ Optional Supabase import βββββββββββββββββββββββββββββββββββββ | |
| # Gracefully handle missing supabase package (e.g. during UI-only preview) | |
| _SUPABASE_AVAILABLE = False | |
| _Client = None | |
| _create_client = None | |
| try: | |
| from supabase import create_client as _create_client, Client as _Client | |
| _SUPABASE_AVAILABLE = True | |
| except ImportError: | |
| logger.warning("supabase package not installed β database calls will return empty results") | |
| # ββ Singleton client βββββββββββββββββββββββββββββββββββββββββββββ | |
| _settings = get_settings() | |
| _client: Any = None | |
| def get_supabase() -> Any: | |
| """Return a lazily-initialised Supabase client. | |
| Raises RuntimeError if the supabase package is not installed or | |
| credentials are not configured. | |
| """ | |
| global _client | |
| if not _SUPABASE_AVAILABLE or _create_client is None: | |
| raise RuntimeError("Supabase package not installed β run: pip install supabase") | |
| if _client is None: | |
| _client = _create_client(_settings.supabase_url, _settings.supabase_key) | |
| return _client | |
| def _is_db_available() -> bool: | |
| """Check if Supabase is configured and reachable.""" | |
| if not _SUPABASE_AVAILABLE: | |
| return False | |
| if not _settings.supabase_url or not _settings.supabase_key: | |
| return False | |
| if "mock" in _settings.supabase_url.lower(): | |
| return False | |
| return True | |
| def _now_iso() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| # ββ Input Validation Guards ββββββββββββββββββββββββββββββββββββββ | |
| _UUID_RE = re.compile( | |
| r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", | |
| re.IGNORECASE, | |
| ) | |
| def _validate_uuid(value: str, field_name: str = "id") -> str: | |
| """Validate that a string is a well-formed UUID. | |
| Raises ValueError if not a valid UUID, preventing injection | |
| through ID fields passed to Supabase query filters. | |
| """ | |
| if not value or not _UUID_RE.match(value): | |
| raise ValueError(f"Invalid {field_name}: expected UUID format") | |
| return value | |
| def _validate_limit(limit: int, max_val: int = 500) -> int: | |
| """Clamp a limit parameter to a safe range.""" | |
| return max(1, min(int(limit), max_val)) | |
| def _sanitize_str(value: str, max_length: int = 1000) -> str: | |
| """Truncate and strip a string field to prevent oversized payloads.""" | |
| return value.strip()[:max_length] if value else value | |
| # ββ Products βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ββ Allowed sort columns (prevent injection via order_by) βββββββ | |
| _ALLOWED_ORDER_COLUMNS = {"created_at", "updated_at", "current_price", "rating", "total_orders", "score", "title"} | |
| def fetch_products( | |
| *, | |
| active_only: bool = True, | |
| category: str | None = None, | |
| limit: int = 50, | |
| order_by: str = "created_at", | |
| ascending: bool = False, | |
| ) -> list[dict[str, Any]]: | |
| """Return a list of products, optionally filtered.""" | |
| if not _is_db_available(): | |
| return [] | |
| # Validate order_by against allowlist to prevent column injection | |
| if order_by not in _ALLOWED_ORDER_COLUMNS: | |
| order_by = "created_at" | |
| limit = _validate_limit(limit) | |
| db = get_supabase() | |
| query = db.table("products").select("*") | |
| if active_only: | |
| query = query.eq("is_active", True) | |
| if category: | |
| query = query.eq("category", _sanitize_str(category, 100)) | |
| query = query.order(order_by, desc=not ascending).limit(limit) | |
| resp = query.execute() | |
| return resp.data or [] | |
| def fetch_product(product_id: str) -> dict[str, Any] | None: | |
| """Return a single product by its UUID.""" | |
| if not _is_db_available(): | |
| return None | |
| _validate_uuid(product_id, "product_id") | |
| db = get_supabase() | |
| resp = db.table("products").select("*").eq("id", product_id).single().execute() | |
| return resp.data | |
| def insert_product(data: dict[str, Any]) -> dict[str, Any]: | |
| """Insert a new product row.""" | |
| if not _is_db_available(): | |
| return data | |
| db = get_supabase() | |
| resp = db.table("products").insert(data).execute() | |
| return resp.data[0] if resp.data else {} | |
| def update_product(product_id: str, data: dict[str, Any]) -> dict[str, Any] | None: | |
| """Update fields on an existing product.""" | |
| if not _is_db_available(): | |
| return data | |
| _validate_uuid(product_id, "product_id") | |
| db = get_supabase() | |
| data["updated_at"] = _now_iso() | |
| resp = db.table("products").update(data).eq("id", product_id).execute() | |
| return resp.data[0] if resp.data else None | |
| def upsert_product(data: dict[str, Any]) -> dict[str, Any]: | |
| """Insert or update a product (matched by aliexpress_id).""" | |
| if not _is_db_available(): | |
| return data | |
| existing = None | |
| if data.get("aliexpress_id"): | |
| db = get_supabase() | |
| resp = ( | |
| db.table("products") | |
| .select("id") | |
| .eq("aliexpress_id", data["aliexpress_id"]) | |
| .limit(1) | |
| .execute() | |
| ) | |
| if resp.data: | |
| existing = resp.data[0] | |
| if existing: | |
| return update_product(existing["id"], data) # type: ignore[arg-type] | |
| return insert_product(data) | |
| def count_products() -> int: | |
| if not _is_db_available(): | |
| return 0 | |
| db = get_supabase() | |
| resp = db.table("products").select("id", count="exact").execute() | |
| return resp.count or 0 # type: ignore[return-value] | |
| # ββ Orders βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fetch_orders(*, limit: int = 100, status: str | None = None) -> list[dict[str, Any]]: | |
| if not _is_db_available(): | |
| return [] | |
| limit = _validate_limit(limit) | |
| db = get_supabase() | |
| query = db.table("orders").select("*") | |
| if status: | |
| query = query.eq("status", _sanitize_str(status, 50)) | |
| query = query.order("created_at", desc=True).limit(limit) | |
| resp = query.execute() | |
| return resp.data or [] | |
| def fetch_order(order_id: str) -> dict[str, Any] | None: | |
| if not _is_db_available(): | |
| return None | |
| _validate_uuid(order_id, "order_id") | |
| db = get_supabase() | |
| resp = db.table("orders").select("*").eq("id", order_id).single().execute() | |
| return resp.data | |
| def insert_order(data: dict[str, Any]) -> dict[str, Any]: | |
| if not _is_db_available(): | |
| return data | |
| db = get_supabase() | |
| resp = db.table("orders").insert(data).execute() | |
| return resp.data[0] if resp.data else {} | |
| def update_order(order_id: str, data: dict[str, Any]) -> dict[str, Any] | None: | |
| if not _is_db_available(): | |
| return data | |
| _validate_uuid(order_id, "order_id") | |
| db = get_supabase() | |
| data["updated_at"] = _now_iso() | |
| resp = db.table("orders").update(data).eq("id", order_id).execute() | |
| return resp.data[0] if resp.data else None | |
| def update_order_by_paypal_id(paypal_id: str, data: dict[str, Any]) -> dict[str, Any] | None: | |
| """Update an order matched by its PayPal order ID.""" | |
| if not _is_db_available(): | |
| return data | |
| db = get_supabase() | |
| data["updated_at"] = _now_iso() | |
| resp = db.table("orders").update(data).eq("paypal_order_id", paypal_id).execute() | |
| return resp.data[0] if resp.data else None | |
| def fetch_order_by_paypal_id(paypal_id: str) -> dict[str, Any] | None: | |
| """Return a single order matched by its PayPal order ID.""" | |
| if not _is_db_available(): | |
| return None | |
| db = get_supabase() | |
| resp = db.table("orders").select("*").eq("paypal_order_id", paypal_id).limit(1).execute() | |
| return resp.data[0] if resp.data else None | |
| def count_orders() -> int: | |
| if not _is_db_available(): | |
| return 0 | |
| db = get_supabase() | |
| resp = db.table("orders").select("id", count="exact").execute() | |
| return resp.count or 0 # type: ignore[return-value] | |
| def total_revenue() -> float: | |
| if not _is_db_available(): | |
| return 0.0 | |
| db = get_supabase() | |
| resp = db.table("orders").select("total").in_("status", ["paid", "shipped", "delivered"]).execute() | |
| return sum(float(o["total"]) for o in (resp.data or [])) | |
| # ββ Analytics ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def track_event( | |
| product_id: str | None, | |
| event_type: str, | |
| source: str = "direct", | |
| metadata: dict | None = None, | |
| ) -> None: | |
| """Insert a lightweight analytics event.""" | |
| if not _is_db_available(): | |
| return | |
| db = get_supabase() | |
| db.table("analytics").insert( | |
| { | |
| "product_id": product_id, | |
| "event_type": event_type, | |
| "source": source, | |
| "metadata": metadata or {}, | |
| } | |
| ).execute() | |
| def get_product_events(product_id: str, event_type: str, hours: int = 6) -> int: | |
| """Count events for a product within a time window.""" | |
| if not _is_db_available(): | |
| return 0 | |
| _validate_uuid(product_id, "product_id") | |
| db = get_supabase() | |
| resp = ( | |
| db.table("analytics") | |
| .select("id", count="exact") | |
| .eq("product_id", product_id) | |
| .eq("event_type", _sanitize_str(event_type, 50)) | |
| .execute() | |
| ) | |
| return resp.count or 0 # type: ignore[return-value] | |
| def analytics_summary(hours: int = 24) -> dict[str, Any]: | |
| """Aggregate analytics for the admin dashboard.""" | |
| if not _is_db_available(): | |
| return { | |
| "total_events": 0, "views": 0, "carts": 0, "purchases": 0, | |
| "cart_rate": 0, "conversion_rate": 0, "sources": {}, | |
| } | |
| db = get_supabase() | |
| resp = db.table("analytics").select("*").execute() | |
| events = resp.data or [] | |
| views = sum(1 for e in events if e["event_type"] == "view") | |
| carts = sum(1 for e in events if e["event_type"] == "add_to_cart") | |
| purchases = sum(1 for e in events if e["event_type"] == "purchase") | |
| sources: dict[str, int] = {} | |
| for e in events: | |
| src = e.get("source", "direct") | |
| sources[src] = sources.get(src, 0) + 1 | |
| return { | |
| "total_events": len(events), | |
| "views": views, | |
| "carts": carts, | |
| "purchases": purchases, | |
| "cart_rate": round(carts / views * 100, 2) if views else 0, | |
| "conversion_rate": round(purchases / views * 100, 2) if views else 0, | |
| "sources": sources, | |
| } | |
| # ββ Agent Status βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fetch_agent_statuses() -> list[dict[str, Any]]: | |
| if not _is_db_available(): | |
| return [] | |
| db = get_supabase() | |
| resp = db.table("agent_status").select("*").order("agent_name").execute() | |
| return resp.data or [] | |
| def update_agent_status(agent_name: str, status: str, metadata: dict | None = None) -> None: | |
| if not _is_db_available(): | |
| return | |
| db = get_supabase() | |
| db.table("agent_status").upsert( | |
| { | |
| "agent_name": agent_name, | |
| "status": status, | |
| "updated_at": _now_iso(), | |
| "last_run": _now_iso(), | |
| "metadata": metadata or {}, | |
| }, | |
| on_conflict="agent_name", | |
| ).execute() | |
| # ββ Recent Purchases (Social Proof) ββββββββββββββββββββββββββββββ | |
| _SIMULATED_CITIES = [ | |
| "New York", "London", "Paris", "Berlin", "Tokyo", "Sydney", "Toronto", | |
| "Amsterdam", "Stockholm", "Dubai", "Singapore", "Los Angeles", "Madrid", | |
| "Rome", "Vienna", "Copenhagen", "Munich", "Barcelona", "Melbourne", "Zurich", | |
| ] | |
| _SIMULATED_FIRST_NAMES = [ | |
| "Sarah", "James", "Emma", "Michael", "Olivia", "William", "Ava", "Oliver", | |
| "Sophia", "Elijah", "Isabella", "Lucas", "Mia", "Mason", "Charlotte", "Ethan", | |
| "Amelia", "Logan", "Harper", "Benjamin", "Evelyn", "Alexander", "Abigail", "Daniel", | |
| ] | |
| def fetch_recent_purchases(*, limit: int = 10, fallback_to_simulated: bool = True) -> list[dict[str, Any]]: | |
| """Return recent successful orders formatted for social proof notifications. | |
| If no real paid orders exist and fallback_to_simulated is True, | |
| generates realistic simulated purchases from active products. | |
| """ | |
| if not _is_db_available(): | |
| return [] | |
| db = get_supabase() | |
| resp = ( | |
| db.table("orders") | |
| .select("*") | |
| .in_("status", ["paid", "shipped", "delivered"]) | |
| .order("created_at", desc=True) | |
| .limit(_validate_limit(limit, max_val=50)) | |
| .execute() | |
| ) | |
| orders = resp.data or [] | |
| purchases: list[dict[str, Any]] = [] | |
| for order in orders: | |
| items = order.get("items", []) | |
| if not items: | |
| continue | |
| # Use first item for the notification | |
| item = items[0] | |
| # Anonymize name | |
| raw_name = order.get("customer_name", "Someone") | |
| first_name = raw_name.split()[0] if raw_name and raw_name != "Guest" else "Someone" | |
| if first_name in {"Guest", "guest", ""}: | |
| first_name = "Someone" | |
| purchases.append({ | |
| "name": first_name, | |
| "product_title": item.get("title", "a product"), | |
| "location": "", | |
| "time_ago": "recently", | |
| "image": "", | |
| "is_simulated": False, | |
| }) | |
| if purchases or not fallback_to_simulated: | |
| return purchases[:limit] | |
| # ββ Fallback: generate simulated purchases from active products β | |
| try: | |
| product_resp = db.table("products").select("title, images, current_price").eq("is_active", True).limit(limit * 2).execute() | |
| products = product_resp.data or [] | |
| except Exception: | |
| products = [] | |
| import random | |
| rng = random.Random(42) # deterministic seed for stability | |
| rng.shuffle(products) | |
| for i, product in enumerate(products[:limit]): | |
| images = product.get("images", []) if isinstance(product.get("images"), list) else [] | |
| purchases.append({ | |
| "name": rng.choice(_SIMULATED_FIRST_NAMES), | |
| "product_title": product.get("title", "a product"), | |
| "location": rng.choice(_SIMULATED_CITIES), | |
| "time_ago": f"{rng.randint(1, 45)} minutes ago", | |
| "image": images[0] if images else "", | |
| "is_simulated": True, | |
| }) | |
| return purchases | |