pbl_blueprint/scripts/derive_artifacts.py

167 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_blueprint 派生物生成器(幂等)。
输入pbl_blueprint/models/{tblname}.json —— 四段式表定义summary/fields/indexes/codes
输出:
1) pbl_blueprint/json/{tblname}.json —— CRUD 契约定义(根键 = tblname + params(editable/browserfields)
2) pbl_blueprint/sql/pbl_blueprint.core.sql —— DDL 派生物engine=postgresql禁止手工维护
用法python3 scripts/derive_artifacts.py
QC 核对点json/*.json 与 sql/*.sql 均由本脚本机械派生,改表只需改 models/*.json 后重跑本脚本。
"""
import json
import os
import glob
HERE = os.path.dirname(os.path.abspath(__file__))
PKG = os.path.join(os.path.dirname(HERE), 'pbl_blueprint')
MODELS_DIR = os.path.join(PKG, 'models')
JSON_DIR = os.path.join(PKG, 'json')
SQL_DIR = os.path.join(PKG, 'sql')
# 表 -> dspy 接口前缀CRUD editable url 用)
API_PREFIX = {
'pbl_blueprint': 'blueprint',
'pbl_blueprint_version': 'version',
'pbl_blueprint_template': 'template',
'pbl_blueprint_template_item': 'template_item',
}
BROWSER_FIELDS = ('id', 'tenant_id', 'class_id', 'code', 'name', 'blueprint_code',
'blueprint_name', 'template_code', 'template_name', 'status',
'version_status', 'version_no', 'subobject_type', 'domain_code',
'grade_code', 'subject_code', 'complexity_level', 'seq_no',
'created_at', 'updated_at')
HEAVY_FIELDS = ('snapshot_json', 'content_json', 'item_payload_json',
'quality_criteria', 'knowledge_points', 'tags_json')
IMMUTABLE = ('id', 'tenant_id', 'created_by', 'created_at')
def api_prefix(tbl):
if tbl in API_PREFIX:
return API_PREFIX[tbl]
return 'subobject'
def load_models():
docs = {}
for p in sorted(glob.glob(os.path.join(MODELS_DIR, '*.json'))):
tbl = os.path.basename(p)[:-5]
doc = json.load(open(p, encoding='utf-8'))
for seg in ('summary', 'fields', 'indexes', 'codes'):
if seg not in doc:
raise SystemExit('models/%s.json 缺四段式必需段: %s' % (tbl, seg))
if not isinstance(doc['fields'], dict):
raise SystemExit('models/%s.json fields 必须是对象(列名->定义)' % tbl)
docs[tbl] = doc
return docs
def build_crud(tbl, doc):
fields = doc['fields']
names = list(fields.keys())
editable = [n for n in names if n not in IMMUTABLE]
browser = [n for n in names if n in BROWSER_FIELDS]
alters = {}
for n, spec in fields.items():
if spec.get('appcode'):
alters[n] = {'uitype': 'code', 'appcode': spec['appcode'],
'label': spec.get('label', n)}
a = api_prefix(tbl)
return {tbl: {
'summary': doc['summary'].get('label', tbl),
'alias': tbl + '_crud',
'params': {
'sortby': 'created_at',
'sortorder': 'desc',
'tenant_scoped': True,
'browserfields': {
'fields': browser,
'alters': alters,
'exclouded': list(HEAVY_FIELDS),
},
'editable': {
'new_data_url': "{{entire_url('../api/%s_save.dspy')}}" % a,
'update_data_url': "{{entire_url('../api/%s_save.dspy')}}" % a,
'delete_data_url': "{{entire_url('../api/%s_delete.dspy')}}" % a,
'list_data_url': "{{entire_url('../api/%s_list.dspy')}}" % a,
'get_data_url': "{{entire_url('../api/%s_get.dspy')}}" % a,
'editexclouded': list(IMMUTABLE),
},
'required': [n for n, s in fields.items() if s.get('required')],
'data_filter': {'AND': [{'field': 'tenant_id', 'op': '=', 'var': 'tenant_id'}]},
},
'codes': doc['codes'],
'table_meta': {
'pk': 'id',
'tenant_field': 'tenant_id',
'soft_delete_field': 'is_deleted',
'engine': doc['summary'].get('engine', 'postgresql'),
'indexes': list(doc['indexes'].keys()),
},
}}
def col_ddl(name, spec):
ty = spec['type']
if ty.startswith('str'):
return '%s varchar(%s)' % (name, ty[ty.index('(') + 1:ty.index(')')])
if ty == 'text':
return '%s text' % name
if ty == 'int':
return '%s integer' % name
if ty.startswith('double'):
return '%s numeric(18,2)' % name
if ty == 'datetime':
return '%s timestamp' % name
raise SystemExit('未知抽象类型: %s (%s)' % (ty, name))
def build_sql(docs):
lines = [
'-- pbl_blueprint 核心表 DDL',
'-- 本文件由 scripts/derive_artifacts.py 从 pbl_blueprint/models/*.json 机械派生,禁止手工编辑',
'-- engine: postgresql与 projects/pbls/env/*.json 的 db.engine 一致)',
'-- 主键 id 为应用层生成的 str(32):不使用 SERIAL / BIGSERIAL / nextval / AUTO_INCREMENT',
'',
]
for tbl in sorted(docs.keys()):
doc = docs[tbl]
fields = doc['fields']
lines.append('-- %s: %s' % (tbl, doc['summary'].get('label', '')))
cols = ',\n'.join(
' ' + col_ddl(k, v) + (' NOT NULL' if (v.get('required') or k == 'id') else '')
for k, v in fields.items())
lines.append('CREATE TABLE IF NOT EXISTS %s (' % tbl)
lines.append(cols)
lines.append(');')
for k, v in fields.items():
lines.append("COMMENT ON COLUMN %s.%s IS '%s';" % (tbl, k, v.get('label', '')))
lines.append('')
for iname, isp in doc['indexes'].items():
if iname == 'primary':
continue
uniq = 'UNIQUE ' if isp.get('unique') else ''
lines.append('CREATE %sINDEX IF NOT EXISTS %s_%s ON %s (%s);' %
(uniq, tbl, iname, tbl, ', '.join(isp['fields'])))
lines.append('')
return '\n'.join(lines)
def main():
docs = load_models()
os.makedirs(JSON_DIR, exist_ok=True)
os.makedirs(SQL_DIR, exist_ok=True)
for tbl, doc in docs.items():
out = os.path.join(JSON_DIR, '%s.json' % tbl)
with open(out, 'w', encoding='utf-8') as fp:
json.dump(build_crud(tbl, doc), fp, ensure_ascii=False, indent=2)
with open(os.path.join(SQL_DIR, 'pbl_blueprint.core.sql'), 'w', encoding='utf-8') as fp:
fp.write(build_sql(docs))
print('models=%d json=%d sql_tables=%d' %
(len(docs), len(glob.glob(os.path.join(JSON_DIR, '*.json'))), len(docs)))
if __name__ == '__main__':
main()