commit d1fa283dc44bdb9b7078b91f014a5dc286c2cf7b Author: yumoqing Date: Wed Jul 16 14:20:27 2025 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..af0c479 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# uapi + +universe api \ No newline at end of file diff --git a/json/build.sh b/json/build.sh new file mode 100755 index 0000000..948b4ba --- /dev/null +++ b/json/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/bash + +xls2ui -m ../models -o ../wwwroot appbase *.json diff --git a/json/uapi.json b/json/uapi.json new file mode 100644 index 0000000..5ba992d --- /dev/null +++ b/json/uapi.json @@ -0,0 +1,37 @@ +{ + "tblname":"uapi", + "params":{ + "toolbar":{ + "tools":[ + { + "selected_row":true, + "name":"test", + "icon":"{{entire_url('/imgs/test.svg')}}", + "label": "api测试" + } + ] + }, + "binds":[ + { + "wid":"self", + "event":"test", + "actiontype": "urlwidget", + "target":"PopupWindow", + "options":{ + "methid":"POST", + "params":{}, + "url":"{{entire_url('/uapi/uapi_test.ui')}}" + } + } + ], + + "title":"API", + "description":"API定义", + "sortby":"name", + "browserfields":{ + "exclouded":["id"], + "alters":{} + }, + "editexclouded":["id", "usid"] + } +} diff --git a/json/uapiset.json b/json/uapiset.json new file mode 100644 index 0000000..cc404f4 --- /dev/null +++ b/json/uapiset.json @@ -0,0 +1,20 @@ +{ + "tblname":"uapiset", + "params":{ + "title":"API接口集", + "description":"支持的所有API接口", + "sortby":"name", + "browserfields":{ + "exclouded":["id"], + "alters":{} + }, + "editexclouded":["id"], + "subtables":[ + { + "field":"usid", + "subtable": "uapi", + "title": "API" + } + ] + } +} diff --git a/json/upapp.json b/json/upapp.json new file mode 100644 index 0000000..839383f --- /dev/null +++ b/json/upapp.json @@ -0,0 +1,25 @@ +{ + "tblname":"upapp", + "params":{ + "title":"上位系统", + "description":"上位系统", + "sortby":"name", + "browserfields":{ + "exclouded":["id"], + "alters":{} + }, + "editexclouded":["id"], + "subtables":[ + { + "field":"upappid", + "subtable": "upappapis", + "title": "API集" + }, + { + "field":"upappid", + "subtable": "upappkey", + "title": "APIKEY" + } + ] + } +} diff --git a/json/upapp1.json b/json/upapp1.json new file mode 100644 index 0000000..a4b9083 --- /dev/null +++ b/json/upapp1.json @@ -0,0 +1,20 @@ +{ + "tblname":"upapp", + "alias":"upapp1", + "params":{ + "title":"上位系统", + "sortby":"name", + "noedit":true, + "browserfields":{ + "exclouded":["id"], + "alters":{} + }, + "subtables":[ + { + "field":"upappid", + "subtable":"upappkey", + "title":"应用密码" + } + ] + } +} diff --git a/json/upappapis.json b/json/upappapis.json new file mode 100644 index 0000000..cab250d --- /dev/null +++ b/json/upappapis.json @@ -0,0 +1,33 @@ +{ + "tblname":"upappapis", + "params":{ + "toolbar":{ + "tools":[ + { + "name":"test", + "icon":"{{entire_url('/imgs/test.svg')}}", + "label": "测试", + "selected_row":true + } + ] + }, + "binds":[ + { + "wid":"self", + "event":"test", + "actiontype": "urlwidget", + "options":{ + "url":"{{entire_url('/uapi/apitest.dspy')}}" + } + } + ], + "title":"使用api集", + "description":"此系统使用的API集", + "sortby":"name", + "browserfields":{ + "exclouded":["id"], + "alters":{} + }, + "editexclouded":["id", "upappid"] + } +} diff --git a/json/upappkey.json b/json/upappkey.json new file mode 100644 index 0000000..c57b081 --- /dev/null +++ b/json/upappkey.json @@ -0,0 +1,14 @@ +{ + "tblname":"upappkey", + "params":{ + "title":"上位系统密码", + "logined_userorgid":"ownerid", + "description":"上位系统密码", + "confidential_fields":["apikey", "apipasswd" ], + "browserfields":{ + "exclouded":["id", "ownerid"], + "alters":{} + }, + "editexclouded":["id", "upappid", "ownerid"] + } +} diff --git a/models/uapi.xlsx b/models/uapi.xlsx new file mode 100644 index 0000000..1e775a5 Binary files /dev/null and b/models/uapi.xlsx differ diff --git a/models/uapiset.xlsx b/models/uapiset.xlsx new file mode 100644 index 0000000..cdf4939 Binary files /dev/null and b/models/uapiset.xlsx differ diff --git a/models/upapp.xlsx b/models/upapp.xlsx new file mode 100644 index 0000000..858f1c5 Binary files /dev/null and b/models/upapp.xlsx differ diff --git a/models/upappapis.xlsx b/models/upappapis.xlsx new file mode 100644 index 0000000..342193b Binary files /dev/null and b/models/upappapis.xlsx differ diff --git a/models/upappkey.xlsx b/models/upappkey.xlsx new file mode 100644 index 0000000..940ed14 Binary files /dev/null and b/models/upappkey.xlsx differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..59514a1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ee4ba4f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +aiohttp diff --git a/script/perms.json b/script/perms.json new file mode 100644 index 0000000..7059c09 --- /dev/null +++ b/script/perms.json @@ -0,0 +1,40 @@ +[ +{ + "path": "/uapi/upapp", + "perms": [ + { + "orgtype": "customer", + "roles":["operator"] + }, + { + "orgtype": "owner", + "roles":["operator"] + } + ] +}, +{ + "path": "/uapi/jsonhttpapi", + "perms": [ + { + "orgtype": "customer", + "roles":["operator"] + }, + { + "orgtype": "owner", + "roles":["operator"] + } + ] +}, +{ + "path":"/uapi/upappkey", + "perm":[ + { + "orgtype": "customer", + "roles":["operator"] + }, + { + "orgtype": "owner", + "roles":["operator"] + } + ] +} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..73d9929 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,19 @@ +# setup.cfg + +[metadata] +name=uapi +version = 0.0.1 +description = Your project description +author = "yu moqing" +author_email = "yumoqing@gmail.com" +readme = "README.md" +license = "MIT" + +[options] +packages = find: +requires_python = ">=3.8" +install_requires = + apppublic + sqlor + ahserver + diff --git a/test/.t.py.swp b/test/.t.py.swp new file mode 100644 index 0000000..7bc7f23 Binary files /dev/null and b/test/.t.py.swp differ diff --git a/test/t.py b/test/t.py new file mode 100644 index 0000000..d7a1670 --- /dev/null +++ b/test/t.py @@ -0,0 +1,32 @@ +import sys +import os +import asyncio +from appPublic.jsonConfig import getConfig +from sqlor.dbpools import DBPools +from ahserver.serverenv import ServerEnv +from uapi.appapi import UAPI + +def get_module_dbname(mn): + return 'sage' + +async def main(): + workdir = os.getcwd() + config = getConfig(workdir, {'workdir':workdir}) + DBPools(config.databases) + env = ServerEnv() + env.get_module_dbname = get_module_dbname + uapi = UAPI() + params = { + 'baseurl':'https://qianfan.baidubce.com', + 'model':'deepseek-v3', + 'prompt':'北京今天天气如何,适合跑步吗?', + } + upapiid = 'R47xUJay76dCCt1sLmWvE' # 百度智能搜索 + # MRXYc49LwTjyTLuOVk89R 百度文生文 + + async for line in uapi(upapiid, '0', params=params): + print(line) + +if __name__ == '__main__': + asyncio.new_event_loop().run_until_complete(main()) + diff --git a/uapi/__init__.py b/uapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uapi/appapi.py b/uapi/appapi.py new file mode 100644 index 0000000..c8025ed --- /dev/null +++ b/uapi/appapi.py @@ -0,0 +1,238 @@ +import json + +from traceback import format_exc +from functools import partial +from sqlor.dbpools import DBPools +from appPublic.streamhttpclient import StreamHttpClient, liner +from appPublic.dictObject import DictObject +from appPublic.myTE import MyTemplateEngine +from appPublic.log import debug, exception, error +from ahserver.globalEnv import password_decode +from ahserver.serverenv import get_serverenv + +def get_dbname(): + f = get_serverenv('get_module_dbname') + dbname = f('uapi') + return dbname + +class UAPI: + def __init__(self, env={}): + self.te = MyTemplateEngine([], env=env) + self.env = env + self.auth_api = None + self.auth_ret = None + + async def rendertmpl(self, tmplstr, params={}): + if tmplstr is None: + return None + ns = self.env.copy() + ns.update(params) + te = MyTemplateEngine([], env=self.env) + return te.renders(tmplstr, ns) + + async def cvt_upappapi2uapi(self, upappapiid, callerid, params={}): + self.env.update(params) + db = DBPools() + dbname = get_dbname() + uapi = None + auth_uapi = None + async with db.sqlorContext(dbname) as sor: + upappapi = await self.get_upappapis(sor, upappapiid) + kinfo = await self.get_orgapikey(sor, upappapi.upappid, callerid) + self.env.update(kinfo) + auth_uapi = None + if kinfo.auth_apiid: + auth_upapi = await self.get_upappapis(sor, kinfo.auth_apiid) + auth_uapi = await self.get_uapi(sor, kinfo.auth_upapi.uapiid) + uapi = await self.get_uapi(sor, upappapi.uapiid) + return auth_uapi, uapi + return None, None + + async def __call__(self, upapiid, callerid, params={}): + """ + """ + auth_uapi, uapi = await self.cvt_upappapi2uapi(upapiid, + callerid, params=params) + if uapi is None: + return + if auth_uapi: + await self.do_auth(auth_uapi) + async for line in self.stream_gen(uapi): + yield line + + async def request(self, upapiid, callerid, params={}): + auth_uapi, uapi = await self.cvt_upappapi2uapi(upapiid, + callerid, params=params) + if auth_uapi: + await self.do_auth(auth_uapi) + return await self.do_call(uapi) + + async def do_auth(self, auth_uapi): + b = await self.do_call(auth_uapi) + d = json.loads(b.encode('utf-8')) + self.env.update(d) + return + + async def do_call(self, api, params={}): + url = self.env.get('baseurl') + api.path + method = api.httpmethod + header = await self.rendertmpl(api.headers) + header = json.loads(headers) + body = await self.rendertmpl(api.body) + _params = await self.rendertmpl(api.params) + if _params: + _params = json.loads(_params) + shc = StreamHttpClient() + b = await shc.request(method, url, + headers=headers, + data=body, + params=_params) + + d = json.loads(b.encode('utf-8')) + if api.response: + return await self.rendertmpl(api.response, params=d) + return d + + async def stream_gen(self, api, params={}): + url = self.env.get('baseurl') + api.path + method = api.httpmethod + headers = await self.rendertmpl(api.headers) + headers = json.loads(headers) + body = await self.rendertmpl(api.data) + if body: + bdy = json.loads(body) + bdy['stream'] = True + body = json.dumps(bdy, ensure_ascii=False) + _params = await self.rendertmpl(api.params) + if _params: + _params = json.loads(_params) + debug(f'{headers=}, {body=}. {method=}, {url=}') + shc = StreamHttpClient() + gen = shc(method, url, + headers=headers, + data=body, + params=_params) + chunk_match = api.chunk_match or '' + cmlen = len(chunk_match) + async for line in liner(gen): + line = line.decode('utf-8') + if line.startswith(chunk_match): + line = line[cmlen:] + cvt_line = await self.streamline_handle(line, + api.streamresponse) + if cvt_line is not None: + yield cvt_line + else: + debug(f'{line=} after convert is None') + else: + debug(f'{chunk_match=},{line=} not matched') + + async def streamline_handle(self, line, resptmpl): + try: + dic = json.loads(line) + if resptmpl: + jstr = await self.rendertmpl(resptmpl, params=dic) + jstr += '\n' + else: + jstr = json.dumps(dic, ensure_ascii=False) + '\n' + return jstr + except Exception as e: + exception(f'{line=}\n{e=},{format_exc()}') + return None + + async def get_upappapis(self, sor, upapiid): + recs = await sor.R('upappapis', {'id': upapiid}) + if len(recs) < 1: + e = Exception(f'{upapiid} not found in table(upappapis)') + exception(f'{e}\n{format_exc()}') + raise e + return recs[0] + + async def get_uapi(self, sor, uapiid): + recs = await sor.R('uapi', {'id': uapiid}) + if len(recs) < 1: + e = Exception(f'{uapiid} not found in table(uapi)') + exception(f'{e}\n{format_exc()}') + raise e + return recs[0] + + async def get_orgapikey(self, sor, upappid, callerid): + """ + argumemts: + upappid: upappid which will make call to + orgid: owner organization or user which as the caller of the call + return: + None: this orgid has not gotton apikey from upapp + dict if apikey and upapp infos + """ + sql = """select +a.myappid, +a.apisetid, +a.auth_apiid, +a.baseurl, +b.* +from upapp a, upappkey b +where a.id = b.upappid + and a.id = ${appid}$ + and b.ownerid = ${orgid}$""" + recs = await sor.sqlExe(sql, {'appid':upappid, 'orgid': callerid}) + if len(recs) < 1: + e = Exception(f'{appid=}, {callerid=} has not apikey') + exception(f'{e}, {format_exc()}') + raise e + r = recs[0] + return DictObject(**{ + 'apikey':password_decode(r.apikey), + 'secretkey':password_decode(r.secretkey), + 'apisetid': r.apisetid, + 'auth_apiid': r.auth_apiid, + 'myappid': r.myappid + }) + + async def get_authapi(self, sor): + sql = "select * from jsonhttpapi where name = 'auth' and appid=${appid}$" + recs = await sor.sqlExe(sql, {'appid': self.appid}) + if len(recs): + return recs[0] + return None + + async def get_apiinfo(self, orgids, apiname): + db = DBPools() + dbname = get_serverenv('get_module_dbname')('uapi') + async with db.sqlorContext(dbname) as sor: + sql = """select c.baseurl, +c.name as appname, +b.*, +a.apikey, +a.secretkey +from upappkey a, jsonhttpapi b, upapp c +where + a.upappid=b.appid + and a.upappid = c.id + and b.appid = ${appid}$ + and b.name = ${apiname}$ + and a.ownerid = ${orgid}$ +""" + for orgid in orgids: + ns = { + "orgid": orgid, + "apiname": appname, + "appid": self.appid + } + recs = await sor.sqlExe(sql, ns) + if len(recs) > 0: + rec = recs[0] + rec.apikey = password_decode(rec.apikey) + if rec.secretkey: + rec.secretkey = password_decode(rec.secretkey) + if rec.need_auth and self.auth_api is None: + self.auth_api = await self.get_authapi(sor) + if rec.need_auth and self.auth_api is None: + e = Exception(f'{rec.appname} app has not auth api but need') + exception(f'{e=}, {format_exc()}') + raise e + rec.auth_api = self.auth_api + rec.apikey_orgid = orgid + return recs[0] + return None + diff --git a/uapi/init.py b/uapi/init.py new file mode 100644 index 0000000..cb41627 --- /dev/null +++ b/uapi/init.py @@ -0,0 +1,7 @@ +from ahserver.serverenv import ServerEnv +from .appapi import UAPI + +def load_uapi(): + g = ServerEnv() + g.UAPI = UAPI +