Compare commits

...

5 Commits

Author SHA1 Message Date
d4f6985cef Fix misspelled TIU_TEMPLATE_SET_ITEMS 2023-04-29 18:28:24 -04:00
8e4d43ab6b Cache-aware schedule view 2023-04-29 18:24:19 -04:00
a43ca627a9 Cache API 2023-04-29 18:23:20 -04:00
a3413e382c Fix context memory 2023-04-29 17:51:39 -04:00
002be2be12 Fix inconsistent caching 2023-04-29 17:23:32 -04:00
6 changed files with 115 additions and 59 deletions

View File

@ -1,5 +1,8 @@
<template>
<div v-if="(age === undefined) || (age > 0)" class="alert alert-warning">⚠ update error<template v-if="age >= 90000">; last updated {{Math.round(age/60000)}} minutes ago</template></div>
<template v-if="age !== undefined">
<div v-if="age == Infinity" class="alert alert-danger"> update error</div>
<div v-else-if="age >= 90000" class="alert alert-warning">⚠ last updated <template v-if="age < 3600000">{{ts.toLocaleString()}}</template><template v-else>{{ts.toLocaleString()}}</template></div>
</template>
<table class="table" style="font-family: monospace;" v-if="appointments && appointments.length > 0">
<thead>
<tr><th style="width: 7rem;">Time</th><th>Clinic</th><th>Patient</th><th>Note</th><th style="width: 16rem;">Assignee</th></tr>
@ -71,7 +74,7 @@
return {
appointments: [],
ts: null,
age: 0
age: undefined
};
},
computed: {
@ -87,11 +90,14 @@
async update() {
try {
this.appointments = (await this.client.SDEC_CRSCHED(this.selection.join('|') + '|', strfdate_vista(this.date_begin), strfdate_vista(this.date_end) + '@2359')).sort((a, b) => (new Date(a.START_TIME)) - (new Date(b.START_TIME)));
this.ts = new Date();
this.age = 0;
var now = new Date();
this.ts = this.appointments._ts ? new Date(1000*this.appointments._ts) : now;
this.age = now - this.ts;
this.timer = window.setTimeout(this.update, Math.max(60000 - this.age, 10000));
} catch(ex) {
this.age = this.ts ? (new Date()) - this.ts : undefined;
this.age = this.ts ? (new Date()) - this.ts : Infinity;
console.warn(ex);
this.timer = window.setTimeout(this.update, 30000);
}
}
},
@ -99,14 +105,13 @@
this.$watch(
() => (this.client, this.selection, this.date_begin, this.date_end, {}),
debounce(async () => {
window.clearInterval(this.timer);
window.clearTimeout(this.timer);
this.update();
this.timer = window.setInterval(this.update, 60000);
}, 500)
);
},
unmounted() {
window.clearInterval(this.timer);
window.clearTimeout(this.timer);
}
};
</script>

View File

@ -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
};

View File

@ -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(':');

63
main.py
View File

@ -27,18 +27,22 @@ class JSONProviderX(DefaultJSONProvider):
class CacheProxyRPC(util.CacheProxy):
def __init__(self, obj, persistent=None, volatile=None, prefix=''):
util.CacheProxy.__init__(self, obj)
if persistent is None:
persistent = util.Store().memo
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', 'ORWDXM1_BLDQRSP'), None)
self._cache(('SDEC_RESOURCE', 'ORWU1_NEWLOC', 'ORWLRR_ALLTESTS_ALL', 'ORWORDG_ALLTREE', 'ORWORDG_REVSTS', 'ORWDX_DGNM', 'ORWDX_ORDITM'), persistent, prefix=prefix, ttl=float('inf'))
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)
def _cache_persistent(self, persistent=None, prefix=''):
if persistent is None:
persistent = util.Store().memo
self._cache(('SDEC_RESOURCE', 'ORWLRR_ALLTESTS_ALL'), persistent, prefix=prefix, ttl=float('inf'))
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__)
@ -60,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/<cid>/close')
def cb_close(cid):
@ -73,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/<cid>/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/<cid>/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/<cid>/authenticate')
def cb_authenticate(cid):
@ -104,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/<cid>')
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/<cid>/<method>')
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('/<path:path>')
def cb_static(path):

11
rpc.py
View File

@ -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
@ -113,14 +116,15 @@ class ClientSync(object):
self.context = 'XUS SIGNON'
if TCPConnect and (res := self.TCPConnect(self.sock.getsockname()[0], '0', socket.gethostname())) != 'accept':
raise RPCExcInvalidResult('TCPConnect', self.sock.getsockname()[0], '0', socket.gethostname(), res)
def __call__(self, name: str, *args: Any, command: bool=False, envelope: int=0, context: Union[bool, None]=None, encoding='latin-1'):
def __call__(self, name: str, *args: Any, command: bool=False, envelope: int=0, context: Union[Sequence, None]=None, encoding='latin-1'):
name = name.replace('_', ' ')
with self.lock:
if name != 'XWB CREATE CONTEXT' and context and len(context) > 0 and self.context not in context:
send_rpc_msg(self.sock, rpc_pack('XWB CREATE CONTEXT', XWBHash_encrypt(context[0]), envelope=envelope, encoding=encoding))
if (res := rpc_unpack_result(next(self.recv_rpc_msg), encoding=encoding)) != '1':
raise RPCExcInvalidResult('XWB CREATE CONTEXT', context[0], res)
self.context = context
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'}):
@ -195,7 +199,8 @@ class ClientAsync(object):
await asend_rpc_msg(self.writer, rpc_pack('XWB CREATE CONTEXT', XWBHash_encrypt(context[0]), envelope=envelope, encoding=encoding))
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
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'}):

14
util.py
View File

@ -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