From a43ca627a9d999ff565fc356fb0d45cbd22c0a1d Mon Sep 17 00:00:00 2001 From: inportb Date: Sat, 29 Apr 2023 18:23:20 -0400 Subject: [PATCH] Cache API --- htdocs/vista.mjs | 12 +++++++++- htdocs/vistax.mjs | 53 ++++++++++++++++++++++++------------------- main.py | 57 ++++++++++++++++++++++++++++++++--------------- rpc.py | 5 +++++ util.py | 14 ++++++++++-- 5 files changed, 97 insertions(+), 44 deletions(-) diff --git a/htdocs/vista.mjs b/htdocs/vista.mjs index 1571aa5..dfb7ccd 100644 --- a/htdocs/vista.mjs +++ b/htdocs/vista.mjs @@ -14,6 +14,15 @@ export async function close(cid) { })).json(); } +export async function call(cid, body) { + return await (await fetch('/v1/vista/' + cid, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + })).json(); +} + +/* export async function call(cid, method, ...params) { return await (await fetch('/v1/vista/' + cid, { method: 'POST', @@ -29,6 +38,7 @@ export async function callctx(cid, context, method, ...params) { body: JSON.stringify({ method: method, params: params, context: context, id: Date.now() }) })).json(); } +*/ export async function serverinfo(cid) { return await (await fetch('/v1/vista/' + cid + '/serverinfo', { @@ -55,5 +65,5 @@ export async function authenticate(cid, avcode=null) { } export default window.vista = { - connect, close, call, callctx, serverinfo, userinfo, authenticate + connect, close, call, serverinfo, userinfo, authenticate }; diff --git a/htdocs/vistax.mjs b/htdocs/vistax.mjs index 6516950..198af90 100644 --- a/htdocs/vistax.mjs +++ b/htdocs/vistax.mjs @@ -33,7 +33,10 @@ export function unwrapped(fn) { return async function(...args) { var res = await fn(...args); if(res.error) throw new RPCError(res.error.type, ...res.error.args); - else return res.result; + if(res.ts) try { + res.result._ts = res.ts; + } catch(ex) {} + return res.result; } } @@ -289,15 +292,19 @@ export function Client(cid, secret) { this.connected.value = false; return vista.close(cid); }; - this.call = async function(method, ...params) { - var res = await vista.call(cid, method, ...params); + this.call = async function(body, ...params) { + body = (typeof body === 'string') || (body instanceof String) ? { method: body, params, id: Date.now() } : Object.assign({ id: Date.now() }, body); + if(params.length > 0) { + if(body.params) Array.prototype.push.apply(body.params, params); + else body.params = params; + } + var res = await vista.call(cid, body); if((res.error) && (res.error.type == 'ConnectionResetError')) this.close(); + res._request = body; return res; }; - this.callctx = async function(context, method, ...params) { - var res = vista.callctx(cid, context, method, ...params); - if((res.error) && (res.error.type == 'ConnectionResetError')) this.close(); - return res; + this.callctx = function(context, method, ...params) { + return this.call({ context, method, params, id: Date.now() }); }; this.heartbeat = async function(interval=null) { if(!interval) interval = 0.45*1000*(await this.XWB_GET_BROKER_INFO())[0]; @@ -332,15 +339,15 @@ export function Client(cid, secret) { if(localstate.practitioner) delete localstate.practitioner; }; - this.XWB_IM_HERE = unwrapped(logged(() => this.call('XWB_IM_HERE'), 'XWB_IM_HERE')); + this.XWB_IM_HERE = unwrapped(logged(() => this.call({ method: 'XWB_IM_HERE', ttl: 30 }), 'XWB_IM_HERE')); this.XUS_INTRO_MSG = memoized(unwrapped(logged(() => this.callctx(['XUCOMMAND'], 'XUS_INTRO_MSG'), 'XUS_INTRO_MSG'))); this.XWB_GET_BROKER_INFO = memoized(unwrapped(logged(() => this.callctx(['XUCOMMAND'], 'XWB_GET_BROKER_INFO'), 'XWB_GET_BROKER_INFO'))); this.XUS_GET_USER_INFO = memoized(unwrapped(logged(() => this.call('XUS_GET_USER_INFO'), 'XUS_GET_USER_INFO'))); - this.SDEC_RESOURCE = memoized(unwrapped(logged(() => this.callctx(['SDECRPC'], 'SDEC_RESOURCE'), 'SDEC_RESOURCE'))); - this.SDEC_CLINLET = memoized(unwrapped(logged((...args) => this.callctx(['SDECRPC'], 'SDEC_CLINLET', ...args), 'SDEC_CLINLET'))); - this.SDEC_CRSCHED = unwrapped(logged((...args) => this.callctx(['SDECRPC'], 'SDEC_CRSCHED', ...args), 'SDEC_CRSCHED')); + this.SDEC_RESOURCE = memoized(unwrapped(logged(() => this.call({ method: 'SDEC_RESOURCE', context: ['SDECRPC'], ttl: 2592000 }), 'SDEC_RESOURCE'))); + this.SDEC_CLINLET = unwrapped(logged((...args) => this.call({ method: 'SDEC_CLINLET', context: ['SDECRPC'], ttl: 30 }, ...args), 'SDEC_CLINLET')); + this.SDEC_CRSCHED = unwrapped(logged((...args) => this.call({ method: 'SDEC_CRSCHED', context: ['SDECRPC'], ttl: 30 }, ...args), 'SDEC_CRSCHED')); this.ORWPT_FULLSSN = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWPT_FULLSSN', ...args), 'ORWPT_FULLSSN')), ['dfn', 'name', 'date', 'pid'])); this.ORWPT_LAST5 = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWPT_LAST5', ...args), 'ORWPT_LAST5')), ['dfn', 'name', 'date', 'pid'])); @@ -362,15 +369,15 @@ export function Client(cid, secret) { this.ORWORR_AGET = memoized(caretseparated(sliced(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWORR_AGET', ...args), 'ORWORR_AGET')), 1), ['ifn', 'dgrp', 'time'])); this.ORWORR_GET4LST = memoized(parsed_orderinfo(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWORR_GET4LST', ...args), 'ORWORR_GET4LST')))); - this.TIU_TEMPLATE_GETROOTS = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETROOTS', ...args), 'TIU_TEMPLATE_GETROOTS')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); - this.TIU_TEMPLATE_GETPROOT = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETPROOT', ...args), 'TIU_TEMPLATE_GETPROOT')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); - this.TIU_TEMPLATE_GETBOIL = parsed_text(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETBOIL', ...args), 'TIU_TEMPLATE_GETBOIL'))); - this.TIU_TEMPLATE_GETITEMS = caretseparated(parsed_nullarray(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETITEMS', ...args), 'TIU_TEMPLATE_GETITEMS'))), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); - this.TIU_TEMPLATE_SET_ITEMS = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_SET_ITEMS', ...args), 'TIU_TEMPLATE_SET_ITEMS')); - this.TIU_TEMPLATE_CREATE_MODIFY = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_CREATE/MODIFY', ...args), 'TIU_TEMPLATE_CREATE/MODIFY')); - this.TIU_TEMPLATE_DELETE = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_DELETE', ...args), 'TIU_TEMPLATE_DELETE')); - this.TIU_TEMPLATE_LOCK = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_LOCK', ...args), 'TIU_TEMPLATE_LOCK')); - this.TIU_TEMPLATE_UNLOCK = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_UNLOCK', ...args), 'TIU_TEMPLATE_UNLOCK')); + this.TIU_TEMPLATE_GETROOTS = caretseparated(unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_GETROOTS', context: ['OR CPRS GUI CHART'], ttl: 86400 }, ...args), 'TIU_TEMPLATE_GETROOTS')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); + this.TIU_TEMPLATE_GETPROOT = caretseparated(unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_GETPROOT', context: ['OR CPRS GUI CHART'], ttl: 86400 }, ...args), 'TIU_TEMPLATE_GETPROOT')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); + this.TIU_TEMPLATE_GETBOIL = parsed_text(unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_GETBOIL', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'TIU_TEMPLATE_GETBOIL'))); + this.TIU_TEMPLATE_GETITEMS = caretseparated(parsed_nullarray(unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_GETITEMS', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'TIU_TEMPLATE_GETITEMS'))), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); + this.TIU_TEMPLATE_SET_ITEMS = unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_SET_ITEMS', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'TIU_TEMPLATE_SET_ITEMS')); + this.TIU_TEMPLATE_CREATE_MODIFY = unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_CREATE/MODIFY', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'TIU_TEMPLATE_CREATE/MODIFY')); + this.TIU_TEMPLATE_DELETE = unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_DELETE', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'TIU_TEMPLATE_DELETE')); + this.TIU_TEMPLATE_LOCK = unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_LOCK', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'TIU_TEMPLATE_LOCK')); + this.TIU_TEMPLATE_UNLOCK = unwrapped(logged((...args) => this.call({ method: 'TIU_TEMPLATE_UNLOCK', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'TIU_TEMPLATE_UNLOCK')); this.ORWCV_VST = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWCV_VST', ...args), 'ORWCV_VST')), ['apptinfo', 'datetime', 'location', 'status'])); @@ -383,11 +390,11 @@ export function Client(cid, secret) { this.ORWDX_DLGID = memoized(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_DLGID', ...args), 'ORWDX_DLGID'))); this.ORWDX_DLGDEF = memoized(mapped(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_DLGDEF', ...args), 'ORWDX_DLGDEF')), ['promptID', 'promptIEN', 'fmtSeq', 'fmtCode', 'omit', 'lead', 'trail', 'newLine', 'wrap', 'children', 'isChild']), 'promptID')); this.ORWDX_LOADRSP = memoized(mapped(parsed_orderoverrides(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_LOADRSP', ...args), 'ORWDX_LOADRSP')), ['promptID', 'promptIEN', 'fmtSeq', 'fmtCode', 'omit', 'lead', 'trail', 'newLine', 'wrap', 'children', 'isChild']), 'promptID')); - this.ORWDX_SAVE = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_SAVE', ...args), 'ORWDX_SAVE')); + this.ORWDX_SAVE = unwrapped(logged((...args) => this.call({ method: 'ORWDX_SAVE', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'ORWDX_SAVE')); this.ORWDXM_MENU = memoized(parsed_ordermenu(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM_MENU', ...args), 'ORWDXM_MENU')))); this.ORWDXM_DLGNAME = memoized(caretseparated1(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM_DLGNAME', ...args), 'ORWDXM_DLGNAME')), ['InternalName', 'DisplayName', 'BaseDialogIEN', 'BaseDialogName'])); this.ORWDXM_PROMPTS = memoized(mapped(parsed_caretseparated_detail(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM_PROMPTS', ...args), 'ORWDXM_PROMPTS')), ['id', 'req', 'hid', 'prompt', 'type', 'domain', 'default', 'idflt', 'help']), 'id')); - this.ORWDXM1_BLDQRSP = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM1_BLDQRSP', ...args), 'ORWDXM1_BLDQRSP')), ['QuickLevel', 'ResponseID', 'Dialog', 'Type', 'FormID', 'DGrpLST']); + this.ORWDXM1_BLDQRSP = caretseparated(unwrapped(logged((...args) => this.call({ method: 'ORWDXM1_BLDQRSP', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), 'ORWDXM1_BLDQRSP')), ['QuickLevel', 'ResponseID', 'Dialog', 'Type', 'FormID', 'DGrpLST']); this.ORWUL_FV4DG = memoized(caretseparated1(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWUL_FV4DG', ...args), 'ORWUL_FV4DG')), ['IEN', 'count'])); this.ORWUL_FVSUB = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWUL_FVSUB', ...args), 'ORWUL_FVSUB')), ['IEN', 'description'])); this.ORWUL_FVIDX = memoized(caretseparated1(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWUL_FVIDX', ...args), 'ORWUL_FVIDX')), ['index', 'description'])); @@ -456,7 +463,7 @@ Client.fromCookie = async function(secret, defaulthost='vista.northport.med.va.g console.log('Using saved secret and connection', secret); var cid = localstate.cid; var client = Client.fromID(cid, secret); - if((await vista.call(cid, 'XWB_IM_HERE')).result == '1') { + if((await vista.call(cid, { method: 'XWB_IM_HERE', ttl: 30, id: Date.now() })).result == '1') { var server = await client.serverinfo(); if((host[0] == server.result.host) && (host[1] == server.result.port)) { localstate.host = host.join(':'); diff --git a/main.py b/main.py index 129d895..c77256b 100644 --- a/main.py +++ b/main.py @@ -29,7 +29,7 @@ class CacheProxyRPC(util.CacheProxy): util.CacheProxy.__init__(self, obj) if volatile is None: volatile = util.Store().memo - self._cache(('__call__', 'close', 'authenticate', 'keepalive', 'XWB_CREATE_CONTEXT', 'XWB_IM_HERE', 'TIU_TEMPLATE_GETROOTS', 'TIU_TEMPLATE_GETPROOT', 'TIU_TEMPLATE_GETBOIL', 'TIU_TEMPLATE_GET_DESCRIPTION', 'TIU_TEMPLATE_GETITEMS', 'TIU_TEMPLATE_SET ITEMS', 'TIU_TEMPLATE_CREATE/MODIFY', 'TIU_TEMPLATE_DELETE', 'TIU_TEMPLATE_LOCK', 'TIU_TEMPLATE_UNLOCK', 'SDEC_CRSCHED', 'ORWDX_SAVE', 'ORWDXM1_BLDQRSP'), None) + self._cache(('__call__', 'close', 'authenticate', 'keepalive', 'XWB_CREATE_CONTEXT', 'TIU_TEMPLATE_SET ITEMS', 'TIU_TEMPLATE_CREATE/MODIFY', 'TIU_TEMPLATE_DELETE', 'TIU_TEMPLATE_LOCK', 'TIU_TEMPLATE_UNLOCK', 'ORWDX_SAVE', 'ORWDXM1_BLDQRSP'), None) self._cache(('XWB_GET_BROKER_INFO', 'XUS_INTRO_MSG'), volatile, prefix=prefix, ttl=float('inf')) self._cache(None, volatile, prefix=prefix, ttl=float('-inf')) self._cache_persistent(persistent=persistent, prefix=prefix) @@ -38,6 +38,12 @@ class CacheProxyRPC(util.CacheProxy): persistent = util.Store().memo self._cache(('SDEC_RESOURCE', 'ORWU1_NEWLOC', 'ORWLRR_ALLTESTS_ALL', 'ORWORDG_ALLTREE', 'ORWORDG_REVSTS', 'ORWDX_DGNM', 'ORWDX_ORDITM'), persistent, prefix=prefix, ttl=float('inf')) +def jsonify_result(value, id=None): + return jsonify({ 'result': value._base, 'error': None, 'id': request.json.get('id'), 'ts': value._ts } if isinstance(value, util.Cached) else { 'result': value, 'error': None, 'id': request.json.get('id') }) + +def jsonify_error(ex, id=None): + return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': id }) + def application(): app = Flask(__name__) app.json = JSONProviderX(app) @@ -58,12 +64,12 @@ def application(): while cid in clients: cid = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for i in range(64)) clients[cid] = client = CacheProxyRPC(rpc.ClientSync(host=params.get('host', 'test.northport.med.va.gov'), port=int(params.get('port', 19009)))) - return jsonify({ 'result': cid, 'error': None, 'id': request.json.get('id') }) + return jsonify_result(cid, id=request.json.get('id')) else: return jsonify({ 'result': None, 'error': { 'type': 'Unauthorized', 'args': [] }, 'id': request.json.get('id') }) except Exception as ex: logger.exception(request.url) - return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) + return jsonify_error(ex, id=request.json.get('id')) @app.post('/v1/vista//close') def cb_close(cid): @@ -71,28 +77,28 @@ def application(): client = clients[cid] res = client.close() del clients[cid] - return jsonify({ 'result': res, 'error': None, 'id': request.json.get('id') }) + return jsonify_result(res, id=request.json.get('id')) except Exception as ex: logger.exception(request.url) - return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) + return jsonify_error(ex, id=request.json.get('id')) @app.post('/v1/vista//serverinfo') def cb_serverinfo(cid): try: client = clients[cid] - return jsonify({ 'result': client._obj._server, 'error': None, 'id': request.json.get('id') }) + return jsonify_result(client._obj._server, id=request.json.get('id')) except Exception as ex: logger.exception(request.url) - return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) + return jsonify_error(ex, id=request.json.get('id')) @app.post('/v1/vista//userinfo') def cb_userinfo(cid): try: client = clients[cid] - return jsonify({ 'result': client._obj._user, 'error': None, 'id': request.json.get('id') }) + return jsonify_result(client._obj._user, id=request.json.get('id')) except Exception as ex: logger.exception(request.url) - return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) + return jsonify_error(ex, id=request.json.get('id')) @app.post('/v1/vista//authenticate') def cb_authenticate(cid): @@ -102,41 +108,56 @@ def application(): if 'avcode' in params: user = client.authenticate(params['avcode']) client._cache_persistent(persistent=util.Store(f'cache.{client._server["volume"].lower()}.{client._server["uci"].lower()}.{user[0]}.db', journal_mode='WAL').memo) - return jsonify({ 'result': user, 'error': None, 'id': request.json.get('id') }) + return jsonify_result(user, id=request.json.get('id')) else: from auth import XUIAMSSOi_MySsoTokenVBA if token := XUIAMSSOi_MySsoTokenVBA(): user = client.authenticate(token) client._cache_persistent(persistent=util.Store(f'cache.{client._server["volume"].lower()}.{client._server["uci"].lower()}.{user[0]}.db', journal_mode='WAL').memo) - return jsonify({ 'result': user, 'error': None, 'id': request.json.get('id') }) + return jsonify_result(user, id=request.json.get('id')) else: return jsonify({ 'result': None, 'error': { 'type': 'Unauthorized', 'args': [] }, 'id': request.json.get('id') }) except Exception as ex: logger.exception(request.url) - return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) + return jsonify_error(ex, id=request.json.get('id')) @app.post('/v1/vista/') def cb_call1(cid): try: client = clients[cid] data = request.json + kw = {} if 'context' in data: - return jsonify({ 'result': getattr(client, data['method'].upper())(*data.get('params', ()), context=data['context']), 'error': None, 'id': data.get('id') }) - else: - return jsonify({ 'result': getattr(client, data['method'].upper())(*data.get('params', ())), 'error': None, 'id': data.get('id') }) + kw['context'] = data['context'] + thunk = getattr(client, data['method'].upper()) + if getattr(thunk, 'cached', False): + if 'ttl' in data: + kw['_cache_ttl'] = data['ttl'] + if 'stale' in data: + kw['_cache_stale'] = data['stale'] + return jsonify_result(thunk(*data.get('params', ()), **kw), id=data.get('id')) except Exception as ex: logger.exception(request.url) - return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) + return jsonify_error(ex, id=request.json.get('id')) @app.post('/v1/vista//') def cb_call2(cid, method): try: client = clients[cid] data = request.json - return jsonify({ 'result': getattr(client, method.upper())(*data.get('params', ())), 'error': None, 'id': data.get('id') }) + kw = {} + if 'context' in data: + kw['context'] = data['context'] + thunk = getattr(client, method.upper()) + if getattr(thunk, 'cached', False): + if 'ttl' in data: + kw['_cache_ttl'] = data['ttl'] + if 'stale' in data: + kw['_cache_stale'] = data['stale'] + return jsonify_result(thunk(*data.get('params', ()), **kw), id=data.get('id')) except Exception as ex: logger.exception(request.url) - return jsonify({ 'result': None, 'error': { 'type': ex.__class__.__name__, 'args': ex.args }, 'id': request.json.get('id') }) + return jsonify_error(ex, id=request.json.get('id')) @app.get('/') def cb_static(path): diff --git a/rpc.py b/rpc.py index a947381..2ac63bb 100644 --- a/rpc.py +++ b/rpc.py @@ -5,12 +5,15 @@ import socket import threading import asyncio import warnings +import logging from collections import namedtuple from XWBHash import encrypt0 as XWBHash_encrypt from typing import Any, Union, Sequence +logger = logging.getLogger(__name__) + class RPCExc(Exception): pass class RPCExcFormat(ValueError, RPCExc): pass class RPCExcAuth(RPCExc): pass @@ -121,6 +124,7 @@ class ClientSync(object): if (res := rpc_unpack_result(next(self.recv_rpc_msg), encoding=encoding)) != '1': raise RPCExcInvalidResult('XWB CREATE CONTEXT', context[0], res) self.context = context[0] + logger.warning(f'RPC: {name} [{self.context}] {args}' if context else f'{name} {args}') send_rpc_msg(self.sock, rpc_pack(name, *args, command=command, envelope=envelope, encoding=encoding)) return rpc_unpack_result(next(self.recv_rpc_msg), encoding=encoding) def __getattr__(self, key: str, commands: set={'TCPConnect'}): @@ -196,6 +200,7 @@ class ClientAsync(object): if (res := rpc_unpack_result(await self.arecv_rpc_msg.__anext__(), encoding=encoding)) != '1': raise RPCExcInvalidResult('XWB CREATE CONTEXT', context[0], res) self.context = context[0] + logger.warning(f'RPC: {name} [{self.context}] {args}' if context else f'{name} {args}') await asend_rpc_msg(self.writer, rpc_pack(name, *args, command=command, envelope=envelope, encoding=encoding)) return rpc_unpack_result(await self.arecv_rpc_msg.__anext__(), encoding=encoding) def __getattr__(self, key: str, commands: set={'TCPConnect'}): diff --git a/util.py b/util.py index 1656318..cca1a1c 100644 --- a/util.py +++ b/util.py @@ -16,6 +16,15 @@ except ImportError: from typing import Any, Union, AsyncGenerator, Iterable, Tuple, Callable +class Cached(object): + def __init__(self, base, ts=None): + self._base = base + self._ts = ts + def __getattr__(self, key): + return getattr(self._base, key) + def __getitem__(self, key): + return getitem(self._base, key) + class Store(object): def __init__(self, database: Union[sqlite3.Connection, str]=':memory:', synchronous: bool=None, journal_mode: bool=None, default_factory: Union[Callable, None]=None): self._db = database if isinstance(database, sqlite3.Connection) else sqlite3.connect(database, check_same_thread=False) @@ -60,8 +69,8 @@ class Mapping(object): def get(self, key: Union[str, slice], ttl: float=float('inf'), now: float=0, **kw) -> Any: if isinstance(key, slice): key, ttl, now = key.start, key.stop, key.step - for row in self._store.execute(f'SELECT value FROM "{self._tbl}" WHERE key = ? AND ts > ? LIMIT 1', (key, (now or time.time()) - ttl)): - return loads(row[0]) + for row in self._store.execute(f'SELECT value, ts FROM "{self._tbl}" WHERE key = ? AND ts > ? LIMIT 1', (key, (now or time.time()) - ttl)): + return Cached(loads(row[0]), row[1]) if 'default' in kw: return kw['default'] elif self._store._default_factory is not None: @@ -148,6 +157,7 @@ class CacheProxy(object): return fetch(*args, **kw) else: return value + thunk.cached = True setattr(self, key, thunk) return thunk