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 151b90e..047fb9c 100644 Binary files a/sageapi/api/__pycache__/accounting.cpython-310.pyc and b/sageapi/api/__pycache__/accounting.cpython-310.pyc differ diff --git a/sageapi/api/__pycache__/health.cpython-310.pyc b/sageapi/api/__pycache__/health.cpython-310.pyc index 0cbd039..f57b2a6 100644 Binary files a/sageapi/api/__pycache__/health.cpython-310.pyc and b/sageapi/api/__pycache__/health.cpython-310.pyc differ diff --git a/sageapi/api/accounting.py b/sageapi/api/accounting.py index d61279d..f18a6b0 100644 --- a/sageapi/api/accounting.py +++ b/sageapi/api/accounting.py @@ -9,10 +9,10 @@ from __future__ import annotations import json import time -import uuid from typing import Any from appPublic.log import debug, error +from appPublic.uniqueID import getID from sqlor.dbpools import DBPools from ahserver.serverenv import ServerEnv @@ -41,9 +41,13 @@ async def create_accounting_record( result['error'] = 'No database configured for sageapi module' return json.dumps(result, ensure_ascii=False, default=str) - record_id = request_id or str(uuid.uuid4()) + record_id = request_id or getID() now = time.strftime('%Y-%m-%d %H:%M:%S') + # Normalize empty request_id to None to avoid UNIQUE constraint violation + if not request_id: + request_id = None + # Check idempotency if request_id: async with DBPools().sqlorContext(dbname) as sor: diff --git a/sageapi/api/health.py b/sageapi/api/health.py index dd11fdd..46d60fe 100644 --- a/sageapi/api/health.py +++ b/sageapi/api/health.py @@ -47,7 +47,7 @@ async def readiness_check() -> 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 3eb0cb1..683a536 100644 Binary files a/sageapi/sync/__pycache__/uapi_sync.cpython-310.pyc and b/sageapi/sync/__pycache__/uapi_sync.cpython-310.pyc differ 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)