sageapi/sageapi/init.py
Hermes Agent 5936a2f328 feat: implement sync engine, API handlers, DAPI auth, HTTP client
- Sync engine: BaseSync abstract class + 4 sync modules (users/pricing/uapi/llmage)
  - Checkpoint management via sync_state table
  - Batch processing with retry and exponential backoff
  - Incremental fetch from Sage DB via sqlor
  - UPSERT to local cache tables
- API handlers: balance/accounting/users/pricing/health
  - Balance: cache lookup + Sage fallback
  - Accounting: create with idempotency, query with filters/pagination
  - Users: keyword search, org filter
  - Pricing: filter by ppid/llmid/type/status
  - Health: basic + readiness checks (DB connectivity)
- DAPI auth: middleware + authenticate_request function
  - HMAC-SHA256 signature verification
  - Timestamp window validation
  - Sage downapikey table lookup
- HTTP client: SageHttpClient with aiohttp
  - Auto DAPI signature injection
  - Connection pooling, retry, timeout
- Router: 12 routes registered
- Module init: load_sageapi() wires everything to ServerEnv
2026-05-20 18:22:23 +08:00

153 lines
5.4 KiB
Python

"""SageAPI module initialization.
Registers all public functions to ServerEnv so they are accessible
from dspy scripts and other modules via the global environment.
Also sets up route registration and database event bindings.
"""
from appPublic.log import debug, info
from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------
from .middleware.dapi_auth import authenticate_request, DapiAuthMiddleware
# ---------------------------------------------------------------------------
# Sync
# ---------------------------------------------------------------------------
from .sync.base_sync import BaseSync, run_all_syncs
from .sync.user_sync import UserSync
from .sync.pricing_sync import PricingSync
from .sync.uapi_sync import UapiSync
from .sync.llmage_sync import LlmageSync
# Module-level convenience functions for sync
async def sync_users() -> str:
"""Convenience: run user sync."""
syncer = UserSync()
return await syncer.sync()
async def sync_pricing() -> str:
"""Convenience: run pricing sync."""
syncer = PricingSync()
return await syncer.sync()
async def sync_uapi() -> str:
"""Convenience: run uapi sync."""
syncer = UapiSync()
return await syncer.sync()
async def sync_llmage() -> str:
"""Convenience: run llmage sync."""
syncer = LlmageSync()
return await syncer.sync()
# ---------------------------------------------------------------------------
# Cache
# ---------------------------------------------------------------------------
from .cache.cache_manager import CacheManager
# Global cache instance (per-process)
_cache_manager = CacheManager(max_entries=10000, default_ttl=300)
# ---------------------------------------------------------------------------
# API
# ---------------------------------------------------------------------------
from .api.balance import get_customer_balance, update_customer_balance
from .api.accounting import create_accounting_record, query_accounting_records
from .api.users import query_users, get_user_by_id
from .api.pricing import query_pricing, get_pricing_by_llmid
from .api.health import health_check, readiness_check
# ---------------------------------------------------------------------------
# Router
# ---------------------------------------------------------------------------
from .router import Router, setup_routes
# ---------------------------------------------------------------------------
# Utils
# ---------------------------------------------------------------------------
from .utils.http_client import SageHttpClient
from .utils.crypto import encrypt_payload, decrypt_payload
def _bind_sageapi_events(dbpools: DBPools, dbname: str) -> None:
"""Bind database events to SageAPI cache invalidation handlers."""
bindings = [
(f'{dbname}:sync_state:c:after', _cache_manager.invalidate_sync_state),
(f'{dbname}:sync_state:u:after', _cache_manager.invalidate_sync_state),
(f'{dbname}:sync_state:d:after', _cache_manager.invalidate_sync_state),
(f'{dbname}:accounting_records:c:after', _cache_manager.invalidate_accounting),
(f'{dbname}:accounting_records:u:after', _cache_manager.invalidate_accounting),
(f'{dbname}:accounting_records:d:after', _cache_manager.invalidate_accounting),
]
for event_name, handler in bindings:
try:
dbpools.bind(event_name, handler)
debug(f'SageAPI event bound: {event_name}')
except Exception as e:
debug(f'SageAPI event bind skipped: {event_name} ({e})')
def load_sageapi() -> None:
"""Register all SageAPI functions into ServerEnv.
Called by the Sage server during module loading phase.
"""
env = ServerEnv()
# Auth
env.authenticate_request = authenticate_request
env.DapiAuthMiddleware = DapiAuthMiddleware
# Sync
env.sync_users = sync_users
env.sync_pricing = sync_pricing
env.sync_uapi = sync_uapi
env.sync_llmage = sync_llmage
env.run_all_syncs = run_all_syncs
env.BaseSync = BaseSync
env.UserSync = UserSync
env.PricingSync = PricingSync
env.UapiSync = UapiSync
env.LlmageSync = LlmageSync
# Cache
env.cache_manager = _cache_manager
# API
env.get_customer_balance = get_customer_balance
env.update_customer_balance = update_customer_balance
env.create_accounting_record = create_accounting_record
env.query_accounting_records = query_accounting_records
env.query_users = query_users
env.get_user_by_id = get_user_by_id
env.query_pricing = query_pricing
env.get_pricing_by_llmid = get_pricing_by_llmid
env.health_check = health_check
env.readiness_check = readiness_check
# Router
router = Router()
setup_routes(router)
env.sageapi_router = router
info(f'SageAPI: {len(router.get_routes())} routes registered')
# Utils
env.SageHttpClient = SageHttpClient
env.encrypt_payload = encrypt_payload
env.decrypt_payload = decrypt_payload
# Bind database events
dbpools = DBPools()
dbname = env.get_module_dbname('sageapi')
if dbname:
_bind_sageapi_events(dbpools, dbname)
info(f'SageAPI: event listeners bound for database: {dbname}')
else:
debug('SageAPI: event listeners skipped (no database configured)')
info('SageAPI module loaded successfully')