diff --git a/pbl_common/api.py b/pbl_common/api.py index 1b403c8..00c4737 100644 --- a/pbl_common/api.py +++ b/pbl_common/api.py @@ -168,6 +168,82 @@ from pbl_common.audit import ( write_audit_sync, ) +# ========================================================================== +# crud —— dict 句柄适配工厂(下游 pbl_assessment / pbl_scense_ext 取用入口) +# ========================================================================== +def crud(table, module=None, columns=None, pk='id', **factory_kw): + """返回 dict 形态的租户安全 CRUD 句柄,把 CrudBase 的 (row, where) 双参语义 + 适配为下游模块使用的「全 **kw」单参语义。 + + 用法(与 pbl_assessment / pbl_scense_ext 现有调用一致):: + + RUB = crud('pbl_rubric', 'pbl', ['code', 'title', ...]) + row = await RUB['read'](id=rid) # 单行(one=True) + await RUB['create'](**payload) # 插入 + await RUB['update'](**payload) # 按 pk 更新 + await RUB['list'](**kw) # 分页 {'total','rows',...} + await sc['upsert'](['page_key'], page_key=k, ...) # 存在则更新否则插入 + + 参数 + ---- + table : 表名(必填) + module : 归属模块名(用于 get_module_dbname 解析库名,禁止硬编码 DBNAME) + columns : 可写/可查列白名单(非空时按白名单过滤 row/where,防未知列注入) + pk : 主键列名,缺省 'id' + 返回 dict:键 create/read/get/list/update/delete/upsert(及 C/R/U/D/I 别名) + → async callable(*args, **kw)。 + """ + base = tenant_crud(table, module=module, pk=pk, **factory_kw) + cols = tuple(columns) if columns else None + + def _pick(kw): + """按白名单过滤字段(始终保留主键);columns 为空则原样透传。""" + data = dict(kw or {}) + if not cols: + return data + return {k: v for k, v in data.items() if (k in cols) or (k == pk)} + + async def _create(*args, **kw): + return await base.C(_pick(kw)) + + async def _read(*args, **kw): + return await base.R(_pick(kw), one=True) + + async def _get(*args, **kw): + return await base.get(where=_pick(kw)) + + async def _list(*args, **kw): + kw = dict(kw or {}) + page = kw.pop('page', 1) + rows = kw.pop('rows', kw.pop('rows_per_page', 20)) + sort = kw.pop('sort', None) + order = kw.pop('order', 'asc') + return await base.list(_pick(kw), page=page, rows=rows, sort=sort, order=order) + + async def _update(*args, **kw): + row = _pick(kw) + where = {pk: kw[pk]} if pk in (kw or {}) else None + return await base.U(row, where=where) + + async def _delete(*args, **kw): + return await base.D(_pick(kw)) + + async def _upsert(keys=None, *args, **kw): + row = _pick(kw) + where = None + if keys: + where = {k: kw[k] for k in keys if k in (kw or {})} + elif pk in (kw or {}): + where = {pk: kw[pk]} + return await base.upsert(row, where=where) + + return { + 'create': _create, 'read': _read, 'get': _get, 'list': _list, + 'update': _update, 'delete': _delete, 'upsert': _upsert, + 'C': _create, 'R': _read, 'U': _update, 'D': _delete, 'I': _create, + } + + log = logging.getLogger('pbl_common.api') @@ -269,7 +345,7 @@ CONTRACT_SYMBOLS = ( 'sqlExe', 'query', 'fetch_one', 'fetch_all', 'insert_row', 'update_row', 'delete_row', 'safe_ident', 'sqlor_available', 'fallback_enabled', # crud - 'CrudBase', 'tenant_crud', 'crud_factory', 'make_crud', 'bulk_create', + 'crud', 'CrudBase', 'tenant_crud', 'crud_factory', 'make_crud', 'bulk_create', 'bulk_delete', 'is_audit_table', # audit 'write_audit', 'write_audit_batch', 'audit_trail', 'flush_memory_audit', @@ -296,7 +372,7 @@ __all__ = list(CONTRACT_SYMBOLS) + [ 'SYSTEM_TENANT_IDS', 'TENANT_ID_PATTERN', 'INVALID_TENANT_VALUES', 'WRITE_ACTIONS', 'READ_ACTIONS', 'normalize_module', 'default_message', 'cur_date_string', 'today_str', 'quote_ident', 'registered_tables', - 'clear_registry', 'AUDIT_ACTIONS', 'AUDIT_TABLE_MARKERS', + 'crud', 'clear_registry', 'AUDIT_ACTIONS', 'AUDIT_TABLE_MARKERS', 'forbid_audit_mutation', 'tenant_scope_ctx', 'run_in_tenant', 'has_tenant', 'current_tenant', 'PBL_E_OK', 'PBL_E_PARAM', 'PBL_E_TENANT', 'PBL_E_TENANT_MISSING', 'PBL_E_TENANT_INVALID', 'PBL_E_NOT_FOUND', diff --git a/pbl_common/db.py b/pbl_common/db.py index c02f275..0b951a6 100644 --- a/pbl_common/db.py +++ b/pbl_common/db.py @@ -45,7 +45,7 @@ def get_dialect(): def get_module_dbname(module_name): """模块 -> 库名。一律走 ServerEnv,失败回落应用主库名(不返回 None)。""" try: - from appbase.env import ServerEnv + from ahserver.serverenv import ServerEnv env = ServerEnv() fn = getattr(env, 'get_module_dbname', None) if callable(fn): diff --git a/pbl_common/dbutil.py b/pbl_common/dbutil.py index 7d83781..0e6e524 100644 --- a/pbl_common/dbutil.py +++ b/pbl_common/dbutil.py @@ -159,7 +159,7 @@ def get_server_env(): def _load_sqlor_pools(): """导入平台 sqlor 的 DBPools(主路径)。不可用返回 None。""" try: - from sqlor import DBPools # noqa: WPS433 - 平台包 + from sqlor.dbpools import DBPools # noqa: WPS433 - 平台包 except Exception: # noqa: BLE001 try: from sqlor.dbpools import DBPools # noqa: WPS433 @@ -550,7 +550,7 @@ def _conn_config(dbname=None): from ahserver.serverenv import ServerEnv except Exception: try: - from appbase.serverenv import ServerEnv + from ahserver.serverenv import ServerEnv except Exception: ServerEnv = None if ServerEnv is not None: diff --git a/pbl_common/kernel.py b/pbl_common/kernel.py index 7988db6..b53608a 100644 --- a/pbl_common/kernel.py +++ b/pbl_common/kernel.py @@ -223,10 +223,10 @@ def get_dbname(module): if module not in MODULE_DB_KEYS: raise PblError("PBL_E_PARAM", "模块未登记库名映射", {"module": module}) try: - from appbase.serverenv import ServerEnv # noqa: 平台基础模块 + from ahserver.serverenv import ServerEnv # noqa: 平台基础模块 except Exception: try: - from appbase import ServerEnv # noqa: 兼容不同导出层级 + from ahserver.serverenv import ServerEnv # noqa: 兼容不同导出层级 except Exception as e: raise PblError("PBL_E_DB", "ServerEnv 不可用,无法解析库名", {"err": str(e)}) try: