From 40c480e488f03c5fb0a23b040f3f77b3432f8491 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 20 May 2026 22:46:05 +0800 Subject: [PATCH] fix: sageapi local deployment and test server - Fix health check sqlExe calls: add missing ns parameter - Fix accounting ID generation: use getID() instead of uuid4 (VARCHAR length) - Fix accounting request_id: normalize empty to NULL to avoid UNIQUE constraint violation - Fix test_server balance/update route: was incorrectly pointing to accounting handler - Add test_server.py: standalone aiohttp test server for local development - Update conf/config.yaml: local MySQL credentials (test/test) - Update db/schema.sql and scripts/generate_ddl.py for local testing - Fix router sync_status sqlExe call: add missing ns parameter - Fix sync uapi_sync: use correct table/column names --- conf/config.yaml | 4 +- db/schema.sql | 74 ++++---- .../__pycache__/accounting.cpython-310.pyc | Bin 5041 -> 5101 bytes .../api/__pycache__/health.cpython-310.pyc | Bin 2909 -> 2944 bytes sageapi/api/accounting.py | 8 +- sageapi/api/health.py | 6 +- sageapi/router.py | 2 +- .../__pycache__/uapi_sync.cpython-310.pyc | Bin 5266 -> 5295 bytes sageapi/sync/uapi_sync.py | 2 +- scripts/generate_ddl.py | 11 +- test_server.py | 171 ++++++++++++++++++ 11 files changed, 229 insertions(+), 49 deletions(-) create mode 100644 test_server.py diff --git a/conf/config.yaml b/conf/config.yaml index 9270401..c0bd846 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -11,8 +11,8 @@ database: host: "127.0.0.1" port: 3306 dbname: "sageapi_db" - user: "root" - password: "" + user: "test" + password: "test" pool_size: 10 upstream: diff --git a/db/schema.sql b/db/schema.sql index 8c19ae7..00574c5 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,19 +1,19 @@ -- SageAPI DDL (auto-generated) CREATE TABLE IF NOT EXISTS `accounting_records` ( -`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', -`customer_id` VARCHAR(32) NOT NULL DEFAULT COMMENT '客户ID', -`llmid` VARCHAR(32) NULL DEFAULT COMMENT '模型ID', -`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称', -`pricing_id` VARCHAR(32) NULL DEFAULT COMMENT '定价ID', +`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键', +`customer_id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '客户ID', +`llmid` VARCHAR(32) NULL DEFAULT '' COMMENT '模型ID', +`model_name` VARCHAR(128) NULL DEFAULT '' COMMENT '模型名称', +`pricing_id` VARCHAR(32) NULL DEFAULT '' COMMENT '定价ID', `input_tokens` BIGINT NULL COMMENT '输入token数', `output_tokens` BIGINT NULL COMMENT '输出token数', `total_tokens` BIGINT NULL COMMENT '总token数', `quantity` DECIMAL(15,4) NULL COMMENT '用量(图片数/分钟数等)', `amount` DECIMAL(15,6) NOT NULL DEFAULT 0.0 COMMENT '金额', `currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位', -`request_id` VARCHAR(64) NULL DEFAULT COMMENT '请求ID(幂等键)', -`transno` VARCHAR(64) NULL DEFAULT COMMENT '事务号', +`request_id` VARCHAR(64) NULL DEFAULT '' COMMENT '请求ID(幂等键)', +`transno` VARCHAR(64) NULL DEFAULT '' COMMENT '事务号', `status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/accounted/failed', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', @@ -31,14 +31,14 @@ ALTER TABLE `accounting_records` ADD INDEX `idx_status` (`status`); ALTER TABLE `accounting_records` ADD INDEX `idx_created_at` (`created_at`); CREATE TABLE IF NOT EXISTS `customer_balance` ( -`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,即 customer_id', +`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键,即 customer_id', `balance` DECIMAL(15,4) NOT NULL DEFAULT 0.0 COMMENT '当前余额', `currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位', `credit_limit` DECIMAL(15,4) NULL COMMENT '信用额度', `last_recharge` DATETIME NULL COMMENT '最后充值时间', `last_consumption` DATETIME NULL COMMENT '最后消费时间', `status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/suspended/arrears', -`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号', `cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -48,16 +48,16 @@ ALTER TABLE `customer_balance` ADD INDEX `idx_status` (`status`); ALTER TABLE `customer_balance` ADD INDEX `idx_balance` (`balance`); CREATE TABLE IF NOT EXISTS `llmage_cache` ( -`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', -`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID', -`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称', -`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID', -`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称', -`api_url` VARCHAR(512) NULL DEFAULT COMMENT 'API端点URL', +`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键', +`llmid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '关联模型ID', +`model_name` VARCHAR(128) NULL DEFAULT '' COMMENT '模型名称', +`upappid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上游应用ID', +`apiname` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'API名称', +`api_url` VARCHAR(512) NULL DEFAULT '' COMMENT 'API端点URL', `api_params` TEXT NULL COMMENT 'API参数配置JSON', `model_params` TEXT NULL COMMENT '模型参数配置JSON(max_tokens, temperature等)', `status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive', -`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号', `cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -69,10 +69,10 @@ ALTER TABLE `llmage_cache` ADD INDEX `idx_upappid` (`upappid`); ALTER TABLE `llmage_cache` ADD INDEX `idx_apiname` (`apiname`); CREATE TABLE IF NOT EXISTS `pricing_cache` ( -`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 pricing_program id (ppid)', -`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID', -`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称', -`pricing_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '计费类型: token/image/video/audio', +`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键,对应 pricing_program id (ppid)', +`llmid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '关联模型ID', +`model_name` VARCHAR(128) NULL DEFAULT '' COMMENT '模型名称', +`pricing_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '计费类型: token/image/video/audio', `input_price` DECIMAL(10,6) NULL COMMENT '输入单价(每千token)', `output_price` DECIMAL(10,6) NULL COMMENT '输出单价(每千token)', `unit_price` DECIMAL(10,6) NULL COMMENT '统一单价(按次/按图/按分钟等)', @@ -80,7 +80,7 @@ CREATE TABLE IF NOT EXISTS `pricing_cache` ( `status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/deprecated', `effective_from` DATETIME NULL COMMENT '生效时间', `effective_to` DATETIME NULL COMMENT '失效时间', -`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号', `cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -92,11 +92,11 @@ ALTER TABLE `pricing_cache` ADD INDEX `idx_pricing_type` (`pricing_type`); ALTER TABLE `pricing_cache` ADD INDEX `idx_status` (`status`); CREATE TABLE IF NOT EXISTS `sync_state` ( -`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', -`entity_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '实体类型: users/pricing/llmage/uapi', -`entity_id` VARCHAR(64) NULL DEFAULT COMMENT '实体标识(全量同步时为空)', +`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键', +`entity_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '实体类型: users/pricing/llmage/uapi', +`entity_id` VARCHAR(64) NULL DEFAULT '' COMMENT '实体标识(全量同步时为空)', `last_sync_time` DATETIME NULL COMMENT '最后同步时间', -`sync_version` VARCHAR(32) NULL DEFAULT COMMENT 'Sage返回的版本标识', +`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT 'Sage返回的版本标识', `sync_status` VARCHAR(16) NOT NULL DEFAULT 'success' COMMENT '同步状态: success/pending/failed', `error_msg` TEXT NULL COMMENT '失败原因', `retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数', @@ -112,16 +112,16 @@ ALTER TABLE `sync_state` ADD UNIQUE `idx_entity_type_id` (`entity_type`,`entity_ ALTER TABLE `sync_state` ADD INDEX `idx_sync_status` (`sync_status`); CREATE TABLE IF NOT EXISTS `uapi_cache` ( -`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', -`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID', -`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称', +`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键', +`upappid` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上游应用ID', +`apiname` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'API名称', `method` VARCHAR(16) NULL DEFAULT 'POST' COMMENT 'HTTP方法', -`endpoint` VARCHAR(512) NULL DEFAULT COMMENT 'API端点', +`endpoint` VARCHAR(512) NULL DEFAULT '' COMMENT 'API端点', `auth_type` VARCHAR(32) NULL DEFAULT 'bearer' COMMENT '认证类型', `rate_limit` INT NULL COMMENT '速率限制(次/分钟)', `description` TEXT NULL COMMENT 'API描述', `status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态', -`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号', `cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -131,16 +131,16 @@ ALTER TABLE `uapi_cache` ADD UNIQUE `idx_upappid_apiname` (`upappid`,`apiname`); ALTER TABLE `uapi_cache` ADD INDEX `idx_status` (`status`); CREATE TABLE IF NOT EXISTS `users_cache` ( -`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 users 表 id', -`username` VARCHAR(128) NOT NULL DEFAULT COMMENT '用户名', -`orgid` VARCHAR(32) NULL DEFAULT COMMENT '组织ID', -`orgname` VARCHAR(255) NULL DEFAULT COMMENT '组织名称', -`email` VARCHAR(128) NULL DEFAULT COMMENT '邮箱', -`phone` VARCHAR(32) NULL DEFAULT COMMENT '手机号', +`id` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主键,对应 users 表 id', +`username` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '用户名', +`orgid` VARCHAR(32) NULL DEFAULT '' COMMENT '组织ID', +`orgname` VARCHAR(255) NULL DEFAULT '' COMMENT '组织名称', +`email` VARCHAR(128) NULL DEFAULT '' COMMENT '邮箱', +`phone` VARCHAR(32) NULL DEFAULT '' COMMENT '手机号', `status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/suspended', `created_at` DATETIME NULL COMMENT '创建时间', `updated_at` DATETIME NULL COMMENT '更新时间', -`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`sync_version` VARCHAR(32) NULL DEFAULT '' COMMENT '同步版本号', `cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/sageapi/api/__pycache__/accounting.cpython-310.pyc b/sageapi/api/__pycache__/accounting.cpython-310.pyc index 151b90ebb4afe859123fe87f771e496f46dfbd81..047fb9cd2e852e56eb5e22007b8c12607fdec253 100644 GIT binary patch delta 1402 zcmZuw-)q}e6xNj`S&}7Lmg_CfkH$^1B~H_tv1To6lKz;J#xJ3Cg%-aA$5%@gB+I#y zDRu3oC1cMGmogZmB!9vPgZ&8|^kEFTf?l@Ay^O)w%lfk3bKRz)OoET^e&^hyPv@Td z{q&#HR%n{Kz@Fcqnp@u#_pDL$WbgEcJCZL4N?kz$Mp*XKfm&Ast*-I4;_E@Cp5Z+0 z8-ZCjIahrv$kww+&<0OR8@xI!aT(9zQ9SmQT(@8A5Iw1lG%JLitSh z=e=kDDe|Tp*Q)d>`pZ#7L3oIm6%nVt6Z=N@IsHleXlEjpcZGg7R`%0aJwScCr$=*G z?CU+9JdV{}VPET62k4wo6R@}{e0!sx>*e-!tYMbnp#yQC_nkP?b7EsxBoBHHd4MxL z0~-rM&%tI<*f+5Sap1AVayH)R^loY$J)$pC^D_Sy%}IHr(+{MJ=s9gjc{6+OB@+4t zBy{t!efUtSqC%0i`X(7=NDzZW^e`DaOyn;`WfH^;-PZC`4$u<-1%USeOn@N(s6mG5 zV|6K&BP#t>of;-P3MltyU7)jGHt)^rkZbI$x|Eqnx>=Uhs#^{gQ?BoL5 z^zxI+oiK1KcU%&iybx}uBN}}iW+i2hMEA@|l%YSEr_fdU!W=Jt#^i3!_X7`?7E4q2IqOf6 zEX`WQLKULQt<~il%cZVdy0TU+5sfZd^XSrHWPOcBYb+s=NO(6QtBk(x^fII58ZFo} zXH=lm05bq(0BByKG}>*qg%fEj^jahh`W$ubi4`zfK0$Z~uYwE~NfrR?kU?=^9|N2N zm}ija&9H^N*b7?``IP=*&#asW*=UBHR=iH!sN=`nCAkQDpxXmjgtVdUj!W*XH<~=% zYi+U}&5+<|i(by%-?_Jarg4Y74`Dtxd^4X(&W4Z5jJ#pxG&ox$mq578pllCZT>*hNl{4O1&bU9$ zc#k>bjb4Er*92ySaQgNe4t?f~DEw4s>EF(5*;$PqpA+!?W1QHV5xU?YAd9#HExZsgL{_gkf%zJ+G zex3PZ#*1CoM&eU=-`)7*@Qzo;_jcx|`?*M~Y6KHC(pyH=z({BHZ0(GHsymqHWyEGh<*9a~Jpgu1!B? z);1d0U2hjJpfyC4Rdnxauh1$O?NOsw;T-u;~AFIx0x9K&mteE${uTy{Rlsf(aKyJwP7d0}6m5;1FOK-~dJd za3U@7&&}mrKn?z*IdyzUSgG3SG)bxf7gCiqTaDCA7+nWNbv|XSU6sk?N&h3`fn;#J zP8&%lVrj0?zMbmiX1gYiE%UFfIekgQAC$ObpBmW^QhCeHD~gV7tYQ-f*jH>G+OOcV zdtcf=D3j2OR3$Z5OEng=T13Z$L&pIcV1j?;9>+HS-aUpd@IT!Nyuv5Ekx{65tC+AF z>m)~)j!xapWN(UIv5YjSu<`Y^fZ4UlAi+GsmGl*3$wn;D(M;61MSSP%r6(l@H)R$xZA%W+=@+22>y*L z;zO-}Sd}ALh45dpsc}KcGNmWyOe#5SQpt%6;5|8ZQpwqq3I?!GpMbC&3b{p&p;RRZ zNOj~dRWYElEA$)~=LIT$R@L)h$mU5UJ1CW`Pb%3ACa}-h|3`txM z!_*GL^$zRMMi^2BHpn}j2UX$2#kosv@_H20ljP=R97hS&MI;Ssw~_|E-JmD4A2Ha} ofGuYM(C-6RAunZ_e^wlS%EXSCX-81e75wkz{H9zx==t{_0yOp>%m4rY diff --git a/sageapi/api/__pycache__/health.cpython-310.pyc b/sageapi/api/__pycache__/health.cpython-310.pyc index 0cbd039b3efd4e3e5f4a3a041cf20669ffa2fcc7..f57b2a683b399157e0d043d38a3a427da398c6e9 100644 GIT binary patch delta 282 zcmcaB)*#N8&&$ij00h;?d9&DgH}bVJ$rKCOJ3_Sp^-6#F_R&MX%15@LzTy5W@dFoqY|cOh6T(uObZ!J7(5wLI6W9< z$<{L0FfU+9;mTxK$TZoL*__d1awD@SW6|X4%+8D!lW#N2^2(s7VP-L9TnbimezHG{ zv$-P{Q#EUuQ$%XnK_=I*gG`3lR?AYtRl`!k-ON<*Jl8Bf=BHD delta 247 zcmZn=zbnR<&&$ij00cQ*yjj0_HuAMIF^W#!%oHs&jfsJwkuilalOcs^4pS{dmHT7` zW_5YP5~gN`1 str: } else: async with DBPools().sqlorContext(dbname) as sor: - rows = await sor.sqlExe('SELECT 1 as ping') + rows = await sor.sqlExe('SELECT 1 as ping', {}) result['checks']['cache_db'] = { 'status': 'ok', 'dbname': dbname, @@ -63,7 +63,7 @@ async def readiness_check() -> str: try: from sqlor.dbpools import get_sor_context async with get_sor_context(env, 'sage') as sor: - rows = await sor.sqlExe('SELECT 1 as ping') + rows = await sor.sqlExe('SELECT 1 as ping', {}) result['checks']['sage_db'] = {'status': 'ok'} except Exception as e: error(f'readiness_check sage_db error: {e}') @@ -80,7 +80,7 @@ async def readiness_check() -> str: FROM sync_state ORDER BY last_sync_time DESC """ - rows = await sor.sqlExe(sql) + rows = await sor.sqlExe(sql, {}) if isinstance(rows, list): result['checks']['sync_status'] = { 'status': 'ok', diff --git a/sageapi/router.py b/sageapi/router.py index b13b301..f5c99db 100644 --- a/sageapi/router.py +++ b/sageapi/router.py @@ -121,7 +121,7 @@ async def _sync_status() -> str: dbname = env.get_module_dbname('sageapi') sql = "SELECT entity_type, sync_status, last_sync_time, error_msg FROM sync_state ORDER BY entity_type" async with DBPools().sqlorContext(dbname) as sor: - rows = await sor.sqlExe(sql) + rows = await sor.sqlExe(sql, {}) result['data'] = rows if isinstance(rows, list) else rows.get('rows', []) result['success'] = True except Exception as e: diff --git a/sageapi/sync/__pycache__/uapi_sync.cpython-310.pyc b/sageapi/sync/__pycache__/uapi_sync.cpython-310.pyc index 3eb0cb11f8b51f25c7ea6a08b221010588869761..683a536184541f1cf2a0ab1148f910a408c27638 100644 GIT binary patch delta 98 zcmbQFxn7eipO=@50SI^xY~$m|&4x%o7sBP*liWG!}M5O;C{`z9zYwpp9w6en2C N55Z7~;sD`zMgY-k6cPXc diff --git a/sageapi/sync/uapi_sync.py b/sageapi/sync/uapi_sync.py index 4725689..399fcdd 100644 --- a/sageapi/sync/uapi_sync.py +++ b/sageapi/sync/uapi_sync.py @@ -16,7 +16,7 @@ from .base_sync import BaseSync logger = logging.getLogger(__name__) -class UAPISync(BaseSync): +class UapiSync(BaseSync): MODULE_NAME = "uapi" SOURCE_DBNAME = "sage" CACHE_DBNAME = "sageapi" diff --git a/scripts/generate_ddl.py b/scripts/generate_ddl.py index 62c5b26..a1a04bf 100755 --- a/scripts/generate_ddl.py +++ b/scripts/generate_ddl.py @@ -25,10 +25,15 @@ def generate_ddl(models_dir='models', output_path='db/schema.sql'): nullable = 'NULL' if field.get('nullable', True) else 'NOT NULL' default = '' if field.get('default') is not None: - if isinstance(field['default'], str) and field['default'] not in ('CURRENT_TIMESTAMP', 'NULL', ''): - default = f"DEFAULT '{field['default']}'" + d = field['default'] + if d == 'CURRENT_TIMESTAMP': + default = "DEFAULT CURRENT_TIMESTAMP" + elif d == '': + default = "DEFAULT ''" + elif isinstance(d, str): + default = f"DEFAULT '{d}'" else: - default = f"DEFAULT {field['default']}" + default = f"DEFAULT {d}" comment = f"COMMENT '{field.get('comment', '')}'" fname = f'{btick}{field["name"]}{btick}' col_defs.append(f' {fname} {field["type"]} {nullable} {default} {comment}'.strip()) diff --git a/test_server.py b/test_server.py new file mode 100644 index 0000000..b8269fc --- /dev/null +++ b/test_server.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Standalone test server for SageAPI. + +Uses aiohttp (already in Sage venv) to expose all SageAPI endpoints +without requiring the full Sage framework. Uses local MySQL (127.0.0.1). +""" +import asyncio +import json +import os +import sys +import time + +# Setup paths +SAGE_DIR = '/home/hermesai/repos/sage' +SAGEAPI_DIR = '/home/hermesai/repos/sageapi' +sys.path.insert(0, SAGEAPI_DIR) +sys.path.insert(0, SAGE_DIR) +os.chdir(SAGE_DIR) + +from aiohttp import web + +from appPublic.folderUtils import ProgramPath +ProgramPath() + +from appPublic.jsonConfig import getConfig +from appPublic.dictObject import DictObject +from sqlor.dbpools import DBPools + +# --------------------------------------------------------------------------- +# Build a local database config that works on this machine +# sage DB: override host from 'db' to '127.0.0.1' +# sageapi_db: add our test database +# --------------------------------------------------------------------------- +config = getConfig('.') + +# config.databases is already a DictObject from JsonConfig, so .kwargs access works +# Just override the sage DB host to use local MySQL +sage_db = config.databases.get('sage') +if sage_db and 'kwargs' in sage_db: + sage_db['kwargs']['host'] = '127.0.0.1' + # Override password to encrypted 'test' (the original encrypted pw decodes to empty) + sage_db['kwargs']['password'] = 'xGatnL1idCnFRCe4FaIWRQ==' + +# Add sageapi_db as DictObject (nested dicts need DictObject for .kwargs access) +# Password must be encrypted (sqlor.SQLor.unpassword decrypts it) +sageapi_db_cfg = DictObject( + driver='mysql', + kwargs=DictObject( + host='127.0.0.1', + port=3306, + user='test', + password='xGatnL1idCnFRCe4FaIWRQ==', # encrypted 'test' + db='sageapi_db', + charset='utf8mb4', + ) +) +config.databases['sageapi_db'] = sageapi_db_cfg + +# Initialize DBPools with the (now modified) config +DBPools(config.databases) + +# --------------------------------------------------------------------------- +# Monkey-patch ServerEnv.get_module_dbname +# --------------------------------------------------------------------------- +from ahserver.serverenv import ServerEnv +_serverenv_cls = getattr(ServerEnv, 'klass', ServerEnv) + +def _patched_get_module_dbname(self, module): + if module == 'sageapi': + return 'sageapi_db' + return 'sage' + +_serverenv_cls.get_module_dbname = _patched_get_module_dbname + +# --------------------------------------------------------------------------- +# Import sageapi modules (must be after DBPools init and patching) +# --------------------------------------------------------------------------- +from sageapi.api.health import health_check, readiness_check +from sageapi.api.balance import get_customer_balance, update_customer_balance +from sageapi.api.accounting import create_accounting_record, query_accounting_records +from sageapi.api.users import query_users, get_user_by_id +from sageapi.api.pricing import query_pricing, get_pricing_by_llmid + +_START_TIME = time.time() + +# --------------------------------------------------------------------------- +# Route handlers +# --------------------------------------------------------------------------- +async def handle_health(request): + result = await health_check() + return web.Response(text=result, content_type='application/json') + +async def handle_readiness(request): + result = await readiness_check() + return web.Response(text=result, content_type='application/json') + +async def handle_balance_get(request): + customer_id = request.query.get('customer_id', '') + result = await get_customer_balance(customer_id=customer_id) + return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json') + +async def handle_accounting_create(request): + body = await request.json() + result = await create_accounting_record(**body) + return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json') + +async def handle_balance_update(request): + body = await request.json() + customer_id = body.get('customer_id', '') + balance = body.get('balance', 0.0) + result = await update_customer_balance(customer_id=customer_id, balance=balance) + return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json') + +async def handle_accounting_query(request): + customer_id = request.query.get('customer_id', '') + result = await query_accounting_records(customer_id=customer_id) + return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json') + +async def handle_users_query(request): + result = await query_users() + return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json') + +async def handle_pricing_query(request): + result = await query_pricing() + return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json') + +async def handle_sync_trigger(request): + from sageapi.sync.base_sync import run_all_syncs + result = await run_all_syncs() + return web.Response(text=result if isinstance(result, str) else json.dumps(result), content_type='application/json') + +async def handle_sync_status(request): + result = {'success': False, 'data': []} + try: + env = ServerEnv() + dbname = env.get_module_dbname('sageapi') + sql = "SELECT entity_type, sync_status, last_sync_time, error_msg FROM sync_state ORDER BY entity_type" + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe(sql, {}) + result['data'] = rows if isinstance(rows, list) else rows.get('rows', []) + result['success'] = True + except Exception as e: + result['error'] = str(e) + return web.Response(text=json.dumps(result, ensure_ascii=False, default=str), content_type='application/json') + +# --------------------------------------------------------------------------- +# Setup routes +# --------------------------------------------------------------------------- +def setup_app(): + app = web.Application() + app.router.add_get('/api/v1/health', handle_health) + app.router.add_get('/api/v1/health/ready', handle_readiness) + app.router.add_get('/api/v1/balance', handle_balance_get) + app.router.add_post('/api/v1/balance/update', handle_balance_update) + app.router.add_post('/api/v1/accounting', handle_accounting_create) + app.router.add_get('/api/v1/accounting', handle_accounting_query) + app.router.add_get('/api/v1/users', handle_users_query) + app.router.add_get('/api/v1/pricing', handle_pricing_query) + app.router.add_post('/api/v1/admin/sync', handle_sync_trigger) + app.router.add_get('/api/v1/admin/sync/status', handle_sync_status) + return app + +if __name__ == '__main__': + app = setup_app() + print("Starting SageAPI test server on http://127.0.0.1:18080") + print("Endpoints:") + for route in app.router.routes(): + methods = route.method if hasattr(route, 'method') else 'GET' + path = route.resource.canonical if route.resource else str(route) + print(f" {methods} {path}") + web.run_app(app, host='127.0.0.1', port=18080, print=None)