""" StemeDB HTTP client with Ed25519 signing. Mirrors the Go SDK pattern from sdk/go/steme/client.go """ from dataclasses import dataclass from typing import Optional import blake3 import requests # Support both package and script imports try: from .config import STEMEDB_URL, SOURCE_CLASS_SOCIAL, DEFAULT_LIFECYCLE from .signer import Signer except ImportError: from config import STEMEDB_URL, SOURCE_CLASS_SOCIAL, DEFAULT_LIFECYCLE from signer import Signer @dataclass class AssertionResult: """Result from creating an assertion.""" hash: str status: str @dataclass class HealthResult: """Result from health check.""" status: str version: str assertions_count: int class StemeDBClient: """ StemeDB HTTP API client with automatic signing. All assertions are automatically signed using the provided Signer. """ def __init__(self, base_url: str = STEMEDB_URL, signer: Optional[Signer] = None): """ Create a new StemeDB API client. Args: base_url: API endpoint (e.g., "http://localhost:18180") signer: Signer for automatic assertion signing """ self.base_url = base_url.rstrip("/") self.signer = signer self._session = requests.Session() self._session.headers.update( { "Content-Type": "application/json", "Accept": "application/json", } ) if signer: self._session.headers["X-Agent-Id"] = signer.public_key def health(self) -> HealthResult: """Check the API health status.""" resp = self._session.get(f"{self.base_url}/v1/health") resp.raise_for_status() data = resp.json() return HealthResult( status=data["status"], version=data["version"], assertions_count=data["assertions_count"], ) def assert_fact( self, subject: str, predicate: str, object_value: str, confidence: float, source_url: str, source_class: str = SOURCE_CLASS_SOCIAL, lifecycle: str = DEFAULT_LIFECYCLE, source_metadata: Optional[str] = None, ) -> AssertionResult: """ Create a new assertion in the knowledge graph. The assertion is automatically signed using the client's signer. Args: subject: Subject entity (e.g., "semaglutide") predicate: Predicate/relation (e.g., "side_effect") object_value: Object value (e.g., "nausea") confidence: Confidence score (0.0 to 1.0) source_url: URL of source evidence source_class: Source authority tier (default: "Anecdotal") lifecycle: Lifecycle stage (default: "Proposed") source_metadata: Optional JSON string with structured metadata Returns: AssertionResult with content-addressed hash Raises: ValueError: If signer is not configured requests.HTTPError: If the API request fails """ if not self.signer: raise ValueError("Signer required for assertions") # Compute source hash from URL (BLAKE3) source_hash = blake3.blake3(source_url.encode("utf-8")).hexdigest() # Create signature (v1 format: signs subject:predicate) signature = self.signer.create_signature(subject, predicate) # Build request payload # Note: ObjectValueDto uses adjacently tagged serde format payload = { "subject": subject, "predicate": predicate, "object": {"type": "Text", "value": object_value}, "confidence": confidence, "source_hash": source_hash, "source_class": source_class, "lifecycle": lifecycle, "signatures": [ { "agent_id": signature.agent_id, "signature": signature.signature, "timestamp": signature.timestamp, "version": signature.version, } ], } if source_metadata: payload["source_metadata"] = source_metadata resp = self._session.post(f"{self.base_url}/v1/assert", json=payload) resp.raise_for_status() data = resp.json() return AssertionResult(hash=data["hash"], status=data["status"]) def store_source(self, content: bytes, content_type: str) -> str: """ Store a source document and return its BLAKE3 hash. Args: content: Raw content bytes content_type: MIME type (e.g., "text/plain") Returns: Hex-encoded BLAKE3 hash (64 chars) """ import base64 payload = { "content": base64.b64encode(content).decode("ascii"), "content_type": content_type, } resp = self._session.post(f"{self.base_url}/v1/source", json=payload) resp.raise_for_status() return resp.json()["hash"]