284 lines
9.7 KiB
Python
284 lines
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""script_engine module initialization.
|
|
|
|
Registers all module functions with ServerEnv inside load_script_engine() so
|
|
that .dspy / .ui files can call them directly. DB name is resolved via
|
|
ServerEnv().get_module_dbname('script_engine') -- never hardcoded.
|
|
"""
|
|
from appPublic.uniqueID import getID
|
|
from appPublic.timeUtils import curDateString
|
|
from appPublic.log import debug, exception
|
|
from ahserver import ServerEnv
|
|
from sqlor.dbpools import DBPools
|
|
from . import engine
|
|
|
|
TABLE = 'script_engine'
|
|
|
|
|
|
def _dbname():
|
|
return ServerEnv().get_module_dbname('script_engine')
|
|
|
|
|
|
def _clean_ns(ns):
|
|
"""Normalize client params: drop NaN/null placeholders and _text suffixes."""
|
|
data = ns.copy()
|
|
for k, v in list(data.items()):
|
|
if v == 'NaN' or v == 'null':
|
|
data[k] = None
|
|
for k in list(data.keys()):
|
|
if k.endswith('_text'):
|
|
data.pop(k, None)
|
|
return data
|
|
|
|
|
|
def _err(code, message, field=''):
|
|
return {'code': code, 'message': message, 'field': field, 'detail': ''}
|
|
|
|
|
|
def _ok(data=None):
|
|
return {'code': 0, 'message': 'ok', 'data': data or {}}
|
|
|
|
|
|
async def create_script(request, ns):
|
|
data = _clean_ns(ns)
|
|
name = (data.get('script_name') or '').strip()
|
|
if not name:
|
|
return _err(1, 'script_name is required', 'script_name')
|
|
if len(name) > 100:
|
|
return _err(1, 'script_name too long (max 100)', 'script_name')
|
|
stype = data.get('script_type') or '0'
|
|
if stype not in ('0', '1'):
|
|
return _err(1, 'invalid script_type, only 0=Python or 1=SQL allowed', 'script_type')
|
|
content = data.get('content') or ''
|
|
if not content or not content.strip():
|
|
return _err(1, 'content is required', 'content')
|
|
v = await validate_script_api(request, {'script_type': stype, 'content': content})
|
|
if v.get('code') != 0:
|
|
return v
|
|
status = data.get('status') or '1'
|
|
if status not in ('0', '1'):
|
|
status = '1'
|
|
now = curDateString()
|
|
row = {
|
|
'id': data.get('id') or getID(),
|
|
'script_name': name,
|
|
'script_type': stype,
|
|
'content': content,
|
|
'description': (data.get('description') or '')[:255],
|
|
'status': status,
|
|
'created_at': now,
|
|
'updated_at': now,
|
|
}
|
|
try:
|
|
async with DBPools().sqlorContext(_dbname()) as sor:
|
|
await sor.C(TABLE, row)
|
|
except Exception as e:
|
|
exception('script_engine.create_script error: %s' % e)
|
|
return _err(1, 'create failed: %s' % e)
|
|
return _ok({'id': row['id']})
|
|
|
|
|
|
async def update_script(request, ns):
|
|
data = _clean_ns(ns)
|
|
sid = data.get('id') or ''
|
|
if not sid:
|
|
return _err(1, 'id is required', 'id')
|
|
name = (data.get('script_name') or '').strip()
|
|
if not name:
|
|
return _err(1, 'script_name is required', 'script_name')
|
|
if len(name) > 100:
|
|
return _err(1, 'script_name too long (max 100)', 'script_name')
|
|
stype = data.get('script_type') or '0'
|
|
if stype not in ('0', '1'):
|
|
return _err(1, 'invalid script_type, only 0=Python or 1=SQL allowed', 'script_type')
|
|
content = data.get('content') or ''
|
|
if not content or not content.strip():
|
|
return _err(1, 'content is required', 'content')
|
|
v = await validate_script_api(request, {'script_type': stype, 'content': content})
|
|
if v.get('code') != 0:
|
|
return v
|
|
status = data.get('status') or '1'
|
|
if status not in ('0', '1'):
|
|
status = '1'
|
|
row = {
|
|
'script_name': name,
|
|
'script_type': stype,
|
|
'content': content,
|
|
'description': (data.get('description') or '')[:255],
|
|
'status': status,
|
|
'updated_at': curDateString(),
|
|
}
|
|
try:
|
|
async with DBPools().sqlorContext(_dbname()) as sor:
|
|
await sor.U(TABLE, {'id': sid, **row})
|
|
except Exception as e:
|
|
exception('script_engine.update_script error: %s' % e)
|
|
return _err(1, 'update failed: %s' % e)
|
|
return _ok({'id': sid})
|
|
|
|
|
|
async def delete_script(request, ns):
|
|
data = _clean_ns(ns)
|
|
sid = data.get('id') or ''
|
|
if not sid:
|
|
return _err(1, 'id is required', 'id')
|
|
try:
|
|
async with DBPools().sqlorContext(_dbname()) as sor:
|
|
await sor.D(TABLE, {'id': sid})
|
|
except Exception as e:
|
|
exception('script_engine.delete_script error: %s' % e)
|
|
return _err(1, 'delete failed: %s' % e)
|
|
return _ok({'id': sid})
|
|
|
|
|
|
async def get_script(request, ns):
|
|
data = _clean_ns(ns)
|
|
sid = data.get('id') or ''
|
|
if not sid:
|
|
return _err(1, 'id is required', 'id')
|
|
try:
|
|
async with DBPools().sqlorContext(_dbname()) as sor:
|
|
recs = await sor.R(TABLE, {'id': sid})
|
|
except Exception as e:
|
|
exception('script_engine.get_script error: %s' % e)
|
|
return _err(1, 'get failed: %s' % e)
|
|
if not recs:
|
|
return _err(1, 'script not found', 'id')
|
|
r = recs[0]
|
|
return _ok({
|
|
'id': r.id,
|
|
'script_name': r.script_name,
|
|
'script_type': r.script_type,
|
|
'content': r.content,
|
|
'description': r.description,
|
|
'status': r.status,
|
|
'created_at': r.created_at,
|
|
'updated_at': r.updated_at,
|
|
})
|
|
|
|
|
|
async def list_scripts(request, ns):
|
|
data = _clean_ns(ns)
|
|
dbname = _dbname()
|
|
try:
|
|
page = int(data.get('page') or 1)
|
|
rows = int(data.get('rows') or 20)
|
|
except (TypeError, ValueError):
|
|
page, rows = 1, 20
|
|
if page < 1:
|
|
page = 1
|
|
if rows < 1:
|
|
rows = 20
|
|
if rows > 200:
|
|
rows = 200
|
|
keyword = (data.get('keyword') or '').strip()
|
|
cond = ''
|
|
params = {}
|
|
if keyword:
|
|
cond = ' where script_name like ${keyword}$ '
|
|
params['keyword'] = '%' + keyword + '%'
|
|
offset = (page - 1) * rows
|
|
try:
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
cnt_recs = await sor.sqlExe('select count(*) as cnt from ' + TABLE + cond, params)
|
|
cnt = cnt_recs[0].cnt if cnt_recs else 0
|
|
sql = ('select id, script_name, script_type, description, status, '
|
|
'created_at, updated_at from ' + TABLE + cond +
|
|
' order by created_at desc limit ${offset}$, ${rows}$')
|
|
q = dict(params)
|
|
q['offset'] = offset
|
|
q['rows'] = rows
|
|
recs = await sor.sqlExe(sql, q)
|
|
except Exception as e:
|
|
exception('script_engine.list_scripts error: %s' % e)
|
|
return _err(1, 'list failed: %s' % e)
|
|
items = [{
|
|
'id': r.id,
|
|
'script_name': r.script_name,
|
|
'script_type': r.script_type,
|
|
'description': r.description,
|
|
'status': r.status,
|
|
'created_at': r.created_at,
|
|
'updated_at': r.updated_at,
|
|
} for r in recs]
|
|
return {'code': 0, 'message': 'ok', 'data': {'list': items, 'total': cnt}}
|
|
|
|
|
|
async def validate_script_api(request, ns):
|
|
"""Validate only -- never persists, never executes."""
|
|
data = _clean_ns(ns)
|
|
stype = data.get('script_type') or '0'
|
|
content = data.get('content') or ''
|
|
if not content or not content.strip():
|
|
return _err(1, 'content is required', 'content')
|
|
v = engine.validate(stype, content)
|
|
if v.get('code') != 0:
|
|
return _err(1, v['message'], 'content')
|
|
return _ok({'script_type': stype, 'valid': True})
|
|
|
|
|
|
async def execute_script(request, ns):
|
|
"""Execute a script by id OR by inline content+script_type.
|
|
|
|
Python scripts receive the business params (everything except control
|
|
fields) as a ``params`` dict in the namespace, so scripts can read input
|
|
via ``params.get('key')``.
|
|
"""
|
|
data = _clean_ns(ns)
|
|
dbname = _dbname()
|
|
sid = (data.get('id') or '').strip()
|
|
if sid:
|
|
try:
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
recs = await sor.R(TABLE, {'id': sid})
|
|
except Exception as e:
|
|
exception('script_engine.execute_script query error: %s' % e)
|
|
return _err(1, 'query failed: %s' % e)
|
|
if not recs:
|
|
return _err(1, 'script not found', 'id')
|
|
r = recs[0]
|
|
content = r.content
|
|
stype = r.script_type
|
|
else:
|
|
content = data.get('content') or ''
|
|
stype = data.get('script_type') or '0'
|
|
if not content or not content.strip():
|
|
return _err(1, 'content is required', 'content')
|
|
if stype not in ('0', '1'):
|
|
return _err(1, 'invalid script_type, only 0=Python or 1=SQL allowed', 'script_type')
|
|
v = engine.validate(stype, content)
|
|
if v.get('code') != 0:
|
|
return _err(1, v['message'], 'content')
|
|
if stype == '0':
|
|
try:
|
|
ctrl = {'id', 'content', 'script_type', 'page', 'rows', 'keyword'}
|
|
params = {k: v for k, v in data.items() if k not in ctrl}
|
|
out = engine.execute_python(content, input_ns={'params': params})
|
|
except Exception as e:
|
|
exception('script_engine.execute_script python error: %s' % e)
|
|
return _err(1, 'execute failed: %s' % e, 'content')
|
|
else:
|
|
try:
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
out = await sor.sqlExe(content, {})
|
|
except Exception as e:
|
|
exception('script_engine.execute_script sql error: %s' % e)
|
|
return _err(1, 'execute failed: %s' % e, 'content')
|
|
return _ok({'result': out})
|
|
|
|
|
|
def load_script_engine(env=None):
|
|
"""Register module functions with ServerEnv (the ONLY integration point)."""
|
|
env = env or ServerEnv()
|
|
env.create_script = create_script
|
|
env.create_scripts = create_script
|
|
env.update_script = update_script
|
|
env.update_scripts = update_script
|
|
env.delete_script = delete_script
|
|
env.delete_scripts = delete_script
|
|
env.get_script = get_script
|
|
env.list_scripts = list_scripts
|
|
env.execute_script = execute_script
|
|
env.validate_script_api = validate_script_api
|
|
return env
|