""" Dark Obsidian Python SDK Sync (requests) + Async (httpx) client for the Dark Obsidian Public API. Requirements: pip install requests # for DarkObsidian (sync) pip install httpx # for AsyncDarkObsidian (async) Docs: https://darkobsedian.sameergul.com/docs """ from __future__ import annotations import json from typing import Any, Optional from urllib.parse import urlencode DEFAULT_BASE_URL = "https://qatxonlxvtgxvqjgfxpl.supabase.co/functions/v1/api" # ─── Error ──────────────────────────────────────────────────────────────────── class DarkObsidianError(Exception): """Raised when the API returns an error response.""" def __init__(self, code: str, message: str, status: int, docs: Optional[str] = None): super().__init__(message) self.code = code self.message = message self.status = status self.docs = docs def __repr__(self) -> str: return f"DarkObsidianError(code={self.code!r}, status={self.status}, message={self.message!r})" # ─── Sync Client ───────────────────────────────────────────────────────────── class DarkObsidian: """ Synchronous Dark Obsidian API client using requests. Usage: from dark_obsidian import DarkObsidian client = DarkObsidian(api_key="do_live_...") products = client.products.list(in_stock=True) """ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL): if not api_key: raise ValueError("api_key is required") self._api_key = api_key self._base_url = base_url.rstrip("/") self.products = ProductsResource(self) self.orders = OrdersResource(self) self.customers = CustomersResource(self) self.inventory = InventoryResource(self) self.invoices = InvoicesResource(self) self.analytics = AnalyticsResource(self) self.ai = AIResource(self) self.loyalty = LoyaltyResource(self) self.webhooks = WebhooksResource(self) def _request( self, method: str, path: str, body: Optional[dict] = None, params: Optional[dict] = None, ) -> dict: import requests # type: ignore url = f"{self._base_url}{path}" if params: clean = {k: str(v) for k, v in params.items() if v is not None} if clean: url += "?" + urlencode(clean) headers = { "Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json", } response = requests.request( method, url, headers=headers, data=json.dumps(body) if body is not None else None, timeout=30, ) data = response.json() if not response.ok or data.get("success") is False: err = data.get("error", {}) raise DarkObsidianError( code=err.get("code", "API_ERROR"), message=err.get("message", f"HTTP {response.status_code}"), status=response.status_code, docs=err.get("docs"), ) return data class _BaseResource: def __init__(self, client: DarkObsidian): self._client = client def _req(self, method: str, path: str, body=None, params=None) -> dict: return self._client._request(method, path, body=body, params=params) class ProductsResource(_BaseResource): def list( self, search: Optional[str] = None, category_id: Optional[str] = None, in_stock: Optional[bool] = None, is_active: Optional[bool] = None, page: int = 1, per_page: int = 20, ) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if search: p["search"] = search if category_id: p["category_id"] = category_id if in_stock is not None: p["in_stock"] = str(in_stock).lower() if is_active is not None: p["is_active"] = str(is_active).lower() return self._req("GET", "/products", params=p)["data"] def get(self, product_id: str) -> dict: return self._req("GET", f"/products/{product_id}")["data"] def create(self, name: str, price: float, **kwargs: Any) -> dict: return self._req("POST", "/products", body={"name": name, "price": price, **kwargs})["data"] def update(self, product_id: str, **kwargs: Any) -> dict: return self._req("PUT", f"/products/{product_id}", body=kwargs)["data"] def delete(self, product_id: str) -> dict: return self._req("DELETE", f"/products/{product_id}")["data"] class OrdersResource(_BaseResource): def list( self, customer_id: Optional[str] = None, status: Optional[str] = None, from_date: Optional[str] = None, to_date: Optional[str] = None, page: int = 1, per_page: int = 20, ) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if customer_id: p["customer_id"] = customer_id if status: p["status"] = status if from_date: p["from"] = from_date if to_date: p["to"] = to_date return self._req("GET", "/orders", params=p)["data"] def get(self, order_id: str) -> dict: return self._req("GET", f"/orders/{order_id}")["data"] def create( self, items: list[dict], customer: Optional[dict] = None, payment_method: str = "cash", send_invoice: bool = False, **kwargs: Any, ) -> dict: """ Create a complete sale. Dark Obsidian automatically: - Deducts stock from warehouse - Finds or creates the customer - Awards loyalty points (1 per currency unit) - Creates invoice (if send_invoice=True) - Updates financial reports - Fires sale.completed webhook Args: items: [{"product_id": "...", "quantity": 2, "price": 10.0}] customer: {"name": "...", "email": "...", "phone": "..."} payment_method: 'cash' | 'card' | 'bank_transfer' send_invoice: auto-generate and attach invoice """ body: dict[str, Any] = { "items": items, "payment_method": payment_method, "send_invoice": send_invoice, **kwargs, } if customer: body["customer"] = customer return self._req("POST", "/orders", body=body)["data"] class CustomersResource(_BaseResource): def list( self, search: Optional[str] = None, is_active: Optional[bool] = None, page: int = 1, per_page: int = 20, ) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if search: p["search"] = search if is_active is not None: p["is_active"] = str(is_active).lower() return self._req("GET", "/customers", params=p)["data"] def get(self, customer_id: str) -> dict: return self._req("GET", f"/customers/{customer_id}")["data"] def create(self, name: str, **kwargs: Any) -> dict: return self._req("POST", "/customers", body={"name": name, **kwargs})["data"] def update(self, customer_id: str, **kwargs: Any) -> dict: return self._req("PUT", f"/customers/{customer_id}", body=kwargs)["data"] def delete(self, customer_id: str) -> dict: return self._req("DELETE", f"/customers/{customer_id}")["data"] class InventoryResource(_BaseResource): def list(self, warehouse_id: Optional[str] = None, low_stock: bool = False, page: int = 1, per_page: int = 20) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if warehouse_id: p["warehouse_id"] = warehouse_id if low_stock: p["low_stock"] = "true" return self._req("GET", "/inventory", params=p)["data"] def adjust(self, product_id: str, quantity: float, warehouse_id: Optional[str] = None, reason: Optional[str] = None) -> dict: body: dict[str, Any] = {"product_id": product_id, "quantity": quantity} if warehouse_id: body["warehouse_id"] = warehouse_id if reason: body["reason"] = reason return self._req("POST", "/inventory/adjust", body=body)["data"] def transfer(self, product_id: str, from_warehouse_id: str, to_warehouse_id: str, quantity: float) -> dict: return self._req("POST", "/inventory/transfer", body={ "product_id": product_id, "from_warehouse_id": from_warehouse_id, "to_warehouse_id": to_warehouse_id, "quantity": quantity, })["data"] class InvoicesResource(_BaseResource): def list(self, status: Optional[str] = None, customer_id: Optional[str] = None, page: int = 1, per_page: int = 20) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if status: p["status"] = status if customer_id: p["customer_id"] = customer_id return self._req("GET", "/invoices", params=p)["data"] def get(self, invoice_id: str) -> dict: return self._req("GET", f"/invoices/{invoice_id}")["data"] def create(self, customer_id: str, items: list[dict], **kwargs: Any) -> dict: return self._req("POST", "/invoices", body={"customer_id": customer_id, "items": items, **kwargs})["data"] def update_status(self, invoice_id: str, status: str) -> dict: return self._req("PUT", f"/invoices/{invoice_id}", body={"status": status})["data"] class AnalyticsResource(_BaseResource): def dashboard(self, days: int = 30) -> dict: return self._req("GET", "/analytics/dashboard", params={"days": days})["data"] def revenue(self, days: int = 30) -> list[dict]: return self._req("GET", "/analytics/revenue", params={"days": days})["data"] def top_products(self, days: int = 30, limit: int = 10) -> list[dict]: return self._req("GET", "/analytics/top-products", params={"days": days, "limit": limit})["data"] def customers(self, days: int = 30) -> dict: return self._req("GET", "/analytics/customers", params={"days": days})["data"] class AIResource(_BaseResource): def analyze(self, question: str) -> dict: """Ask a natural language question about your business data.""" return self._req("POST", "/ai/analyze", body={"question": question})["data"] class LoyaltyResource(_BaseResource): def get_customer_points(self, customer_id: str) -> dict: return self._req("GET", f"/loyalty/customers/{customer_id}")["data"] def redeem(self, customer_id: str, points: int, sale_id: Optional[str] = None) -> dict: body: dict[str, Any] = {"customer_id": customer_id, "points": points} if sale_id: body["sale_id"] = sale_id return self._req("POST", "/loyalty/redeem", body=body)["data"] class WebhooksResource(_BaseResource): def list(self, page: int = 1, per_page: int = 20) -> list[dict]: return self._req("GET", "/webhooks", params={"page": page, "per_page": per_page})["data"] def get(self, webhook_id: str) -> dict: return self._req("GET", f"/webhooks/{webhook_id}")["data"] def create(self, url: str, events: list[str], secret: Optional[str] = None) -> dict: body: dict[str, Any] = {"url": url, "events": events} if secret: body["secret"] = secret return self._req("POST", "/webhooks", body=body)["data"] def update(self, webhook_id: str, **kwargs: Any) -> dict: return self._req("PUT", f"/webhooks/{webhook_id}", body=kwargs)["data"] def delete(self, webhook_id: str) -> dict: return self._req("DELETE", f"/webhooks/{webhook_id}")["data"] # ─── Async Client ───────────────────────────────────────────────────────────── class AsyncDarkObsidian: """ Asynchronous Dark Obsidian API client using httpx. Usage: import asyncio from dark_obsidian import AsyncDarkObsidian async def main(): client = AsyncDarkObsidian(api_key="do_live_...") products = await client.products.list() await client.close() asyncio.run(main()) """ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL): if not api_key: raise ValueError("api_key is required") self._api_key = api_key self._base_url = base_url.rstrip("/") self._client = None self.products = AsyncProductsResource(self) self.orders = AsyncOrdersResource(self) self.customers = AsyncCustomersResource(self) self.inventory = AsyncInventoryResource(self) self.invoices = AsyncInvoicesResource(self) self.analytics = AsyncAnalyticsResource(self) self.ai = AsyncAIResource(self) self.loyalty = AsyncLoyaltyResource(self) self.webhooks = AsyncWebhooksResource(self) def _get_http_client(self): if self._client is None: import httpx # type: ignore self._client = httpx.AsyncClient(timeout=30) return self._client async def close(self): if self._client: await self._client.aclose() self._client = None async def _request( self, method: str, path: str, body: Optional[dict] = None, params: Optional[dict] = None, ) -> dict: url = f"{self._base_url}{path}" if params: clean = {k: str(v) for k, v in params.items() if v is not None} if clean: url += "?" + urlencode(clean) headers = { "Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json", } http = self._get_http_client() response = await http.request( method, url, headers=headers, content=json.dumps(body).encode() if body is not None else None, ) data = response.json() if not response.is_success or data.get("success") is False: err = data.get("error", {}) raise DarkObsidianError( code=err.get("code", "API_ERROR"), message=err.get("message", f"HTTP {response.status_code}"), status=response.status_code, docs=err.get("docs"), ) return data async def __aenter__(self): return self async def __aexit__(self, *args): await self.close() class _AsyncBaseResource: def __init__(self, client: AsyncDarkObsidian): self._client = client async def _req(self, method: str, path: str, body=None, params=None) -> dict: return await self._client._request(method, path, body=body, params=params) class AsyncProductsResource(_AsyncBaseResource): async def list(self, search=None, category_id=None, in_stock=None, is_active=None, page=1, per_page=20) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if search: p["search"] = search if category_id: p["category_id"] = category_id if in_stock is not None: p["in_stock"] = str(in_stock).lower() if is_active is not None: p["is_active"] = str(is_active).lower() return (await self._req("GET", "/products", params=p))["data"] async def get(self, product_id: str) -> dict: return (await self._req("GET", f"/products/{product_id}"))["data"] async def create(self, name: str, price: float, **kwargs: Any) -> dict: return (await self._req("POST", "/products", body={"name": name, "price": price, **kwargs}))["data"] async def update(self, product_id: str, **kwargs: Any) -> dict: return (await self._req("PUT", f"/products/{product_id}", body=kwargs))["data"] async def delete(self, product_id: str) -> dict: return (await self._req("DELETE", f"/products/{product_id}"))["data"] class AsyncOrdersResource(_AsyncBaseResource): async def list(self, customer_id=None, status=None, from_date=None, to_date=None, page=1, per_page=20) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if customer_id: p["customer_id"] = customer_id if status: p["status"] = status if from_date: p["from"] = from_date if to_date: p["to"] = to_date return (await self._req("GET", "/orders", params=p))["data"] async def get(self, order_id: str) -> dict: return (await self._req("GET", f"/orders/{order_id}"))["data"] async def create(self, items: list[dict], customer=None, payment_method="cash", send_invoice=False, **kwargs) -> dict: """ Create a complete sale. Dark Obsidian automatically: - Deducts stock from warehouse - Finds or creates the customer - Awards loyalty points (1 per currency unit) - Creates invoice (if send_invoice=True) - Updates financial reports - Fires sale.completed webhook """ body: dict[str, Any] = {"items": items, "payment_method": payment_method, "send_invoice": send_invoice, **kwargs} if customer: body["customer"] = customer return (await self._req("POST", "/orders", body=body))["data"] class AsyncCustomersResource(_AsyncBaseResource): async def list(self, search=None, is_active=None, page=1, per_page=20) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if search: p["search"] = search if is_active is not None: p["is_active"] = str(is_active).lower() return (await self._req("GET", "/customers", params=p))["data"] async def get(self, customer_id: str) -> dict: return (await self._req("GET", f"/customers/{customer_id}"))["data"] async def create(self, name: str, **kwargs: Any) -> dict: return (await self._req("POST", "/customers", body={"name": name, **kwargs}))["data"] async def update(self, customer_id: str, **kwargs: Any) -> dict: return (await self._req("PUT", f"/customers/{customer_id}", body=kwargs))["data"] async def delete(self, customer_id: str) -> dict: return (await self._req("DELETE", f"/customers/{customer_id}"))["data"] class AsyncInventoryResource(_AsyncBaseResource): async def list(self, warehouse_id=None, low_stock=False, page=1, per_page=20) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if warehouse_id: p["warehouse_id"] = warehouse_id if low_stock: p["low_stock"] = "true" return (await self._req("GET", "/inventory", params=p))["data"] async def adjust(self, product_id, quantity, warehouse_id=None, reason=None) -> dict: body: dict[str, Any] = {"product_id": product_id, "quantity": quantity} if warehouse_id: body["warehouse_id"] = warehouse_id if reason: body["reason"] = reason return (await self._req("POST", "/inventory/adjust", body=body))["data"] async def transfer(self, product_id, from_warehouse_id, to_warehouse_id, quantity) -> dict: return (await self._req("POST", "/inventory/transfer", body={ "product_id": product_id, "from_warehouse_id": from_warehouse_id, "to_warehouse_id": to_warehouse_id, "quantity": quantity, }))["data"] class AsyncInvoicesResource(_AsyncBaseResource): async def list(self, status=None, customer_id=None, page=1, per_page=20) -> list[dict]: p: dict[str, Any] = {"page": page, "per_page": per_page} if status: p["status"] = status if customer_id: p["customer_id"] = customer_id return (await self._req("GET", "/invoices", params=p))["data"] async def get(self, invoice_id: str) -> dict: return (await self._req("GET", f"/invoices/{invoice_id}"))["data"] async def create(self, customer_id: str, items: list[dict], **kwargs: Any) -> dict: return (await self._req("POST", "/invoices", body={"customer_id": customer_id, "items": items, **kwargs}))["data"] async def update_status(self, invoice_id: str, status: str) -> dict: return (await self._req("PUT", f"/invoices/{invoice_id}", body={"status": status}))["data"] class AsyncAnalyticsResource(_AsyncBaseResource): async def dashboard(self, days: int = 30) -> dict: return (await self._req("GET", "/analytics/dashboard", params={"days": days}))["data"] async def revenue(self, days: int = 30) -> list[dict]: return (await self._req("GET", "/analytics/revenue", params={"days": days}))["data"] async def top_products(self, days: int = 30, limit: int = 10) -> list[dict]: return (await self._req("GET", "/analytics/top-products", params={"days": days, "limit": limit}))["data"] async def customers(self, days: int = 30) -> dict: return (await self._req("GET", "/analytics/customers", params={"days": days}))["data"] class AsyncAIResource(_AsyncBaseResource): async def analyze(self, question: str) -> dict: return (await self._req("POST", "/ai/analyze", body={"question": question}))["data"] class AsyncLoyaltyResource(_AsyncBaseResource): async def get_customer_points(self, customer_id: str) -> dict: return (await self._req("GET", f"/loyalty/customers/{customer_id}"))["data"] async def redeem(self, customer_id: str, points: int, sale_id=None) -> dict: body: dict[str, Any] = {"customer_id": customer_id, "points": points} if sale_id: body["sale_id"] = sale_id return (await self._req("POST", "/loyalty/redeem", body=body))["data"] class AsyncWebhooksResource(_AsyncBaseResource): async def list(self, page=1, per_page=20) -> list[dict]: return (await self._req("GET", "/webhooks", params={"page": page, "per_page": per_page}))["data"] async def get(self, webhook_id: str) -> dict: return (await self._req("GET", f"/webhooks/{webhook_id}"))["data"] async def create(self, url: str, events: list[str], secret=None) -> dict: body: dict[str, Any] = {"url": url, "events": events} if secret: body["secret"] = secret return (await self._req("POST", "/webhooks", body=body))["data"] async def update(self, webhook_id: str, **kwargs: Any) -> dict: return (await self._req("PUT", f"/webhooks/{webhook_id}", body=kwargs))["data"] async def delete(self, webhook_id: str) -> dict: return (await self._req("DELETE", f"/webhooks/{webhook_id}"))["data"] __all__ = [ "DarkObsidian", "AsyncDarkObsidian", "DarkObsidianError", ]