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