305 lines
12 KiB
Python
305 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""pbl_common 半迁移期的**只读兜底垫片**(QC 退回 #2/#3 落地,不越界改他模块)。
|
||
|
||
背景(引擎实测):pbl_common 半迁移重写删掉了 errors.py/tenant.py 既有符号面,
|
||
导致 ``import pbl_common.errors`` 在运行时直接 ModuleNotFoundError/ImportError,
|
||
连带 pbl_blueprint 包内 20+ 处 ``from pbl_common.errors import fail`` 硬导入全部炸掉
|
||
(api_blueprint.py:20、subobject.py:24、init.py:70 等)。
|
||
|
||
边界纪律(QC #3 要求):
|
||
* pbl_common / pbl_appcodes / pbl_agent_runtime 的**内部**断裂不属 M1b 职责,
|
||
本文件**不修改 pbl_common 任何一行**,只在它不可导入时于 sys.modules 注入
|
||
最小可用替身,让 pbl_blueprint 自身可独立 import / 装配 / 跑测试;
|
||
* 真实 pbl_common 可导入时本垫片**完全不激活**(零副作用),
|
||
因此不存在「以 fallback 名义掩盖他模块断链」——断链事实与冒泡见
|
||
docs/M1b-closure-boundary.md;
|
||
* 垫片符号全部来自包内自足模块 pbl_blueprint.m1b_compat(本地真实实现)。
|
||
|
||
用法:在 pbl_blueprint/__init__.py 最前面 ``from . import _pbl_common_shim``。
|
||
"""
|
||
|
||
import importlib
|
||
import sys
|
||
import types
|
||
|
||
__all__ = ["install", "is_active", "ACTIVE", "PROBED", "MISSING"]
|
||
|
||
_PKG = "pbl_common"
|
||
_SUBS = ("errors", "api", "audit", "crud_factory", "context", "tenant",
|
||
"dbutil", "util", "self_check", "crud", "db")
|
||
|
||
ACTIVE = False
|
||
PROBED = []
|
||
MISSING = []
|
||
|
||
|
||
def _compat():
|
||
"""取包内兼容层(延迟导入,避免循环)。"""
|
||
try:
|
||
return importlib.import_module("pbl_blueprint.m1b_compat")
|
||
except Exception:
|
||
try:
|
||
from . import m1b_compat as c
|
||
return c
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def _probe(modname):
|
||
"""真实模块是否可导入(find_spec + 实际 import 双重确认)。"""
|
||
try:
|
||
importlib.import_module(modname)
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _mk_module(name, doc=""):
|
||
m = types.ModuleType(name)
|
||
m.__doc__ = doc or ("pbl_common shim for %s (provided by pbl_blueprint.m1b_compat)" % name)
|
||
m.__shim__ = True
|
||
m.__package__ = name
|
||
return m
|
||
|
||
|
||
def _fill_errors(mod, c):
|
||
"""pbl_common.errors 契约面:类族 + 错误码 + fail/ok/err 响应构造。"""
|
||
for n in ("ErrorCode", "CODE_TO_HTTP", "PblError", "PblValidationError", "PblNotFound",
|
||
"PblConflict", "PblForbidden", "TenantMissingError", "TenantInvalidError",
|
||
"ParamInvalidError", "assert_not_write_protected", "http_status_of"):
|
||
v = getattr(c, n, None)
|
||
if v is not None:
|
||
setattr(mod, n, v)
|
||
# 别名:上游既有名 -> 兼容层实现
|
||
mod.NotFoundError = getattr(c, "PblNotFound")
|
||
mod.ValidationError = getattr(c, "PblValidationError")
|
||
mod.ConflictError = getattr(c, "PblConflict")
|
||
mod.ForbiddenError = getattr(c, "PblForbidden")
|
||
mod.WriteProtectedError = getattr(c, "PblForbidden")
|
||
mod.UnauthorizedError = getattr(c, "PblForbidden")
|
||
mod.DbError = getattr(c, "PblError")
|
||
mod.PBLError = getattr(c, "PblError")
|
||
mod.http_status_for = getattr(c, "http_status_of")
|
||
|
||
def fail(code, message="", detail=None, http_status=None):
|
||
"""构造失败响应(兼容 fail(category,key,msg) 与 fail(code,message) 两种调用)。"""
|
||
if isinstance(code, str) and "-" in code and not message:
|
||
message = code
|
||
return {"ok": False, "code": str(code), "message": str(message or ""),
|
||
"detail": detail if detail is not None else {},
|
||
"http_status": int(http_status or 400)}
|
||
|
||
def ok(data=None, **extra):
|
||
"""构造成功响应。"""
|
||
out = {"ok": True, "code": "OK", "data": data}
|
||
out.update(extra)
|
||
return out
|
||
|
||
def err(category, key, msg=None, detail=None):
|
||
"""按 category/key 拼错误码并返回失败响应。"""
|
||
code = "%s-%s" % (str(category or "PBL").upper(), str(key or "ERROR").upper())
|
||
return fail(code, msg or code, detail)
|
||
|
||
def raise_error(code, message="", detail=None):
|
||
"""抛 PblError(fail-closed 场景)。"""
|
||
raise c.PblError(message or str(code), code=str(code), detail=detail)
|
||
|
||
def all_codes():
|
||
"""全部错误码 -> HTTP 映射(只读)。"""
|
||
return dict(getattr(c, "CODE_TO_HTTP", {}))
|
||
|
||
mod.fail = fail
|
||
mod.ok = ok
|
||
mod.err = err
|
||
mod.raise_error = raise_error
|
||
mod.all_codes = all_codes
|
||
mod.error_envelope = fail
|
||
mod.WRITE_PROTECTED_MODULES = ()
|
||
return mod
|
||
|
||
|
||
def _fill_api(mod, c):
|
||
"""pbl_common.api 契约面(设计文档 §3 契约表)。"""
|
||
for n in ("tenant_id", "flag", "actor_id", "sql_exec", "sql_rows", "sql_scalar",
|
||
"now_str", "json_dump", "json_load", "crud", "new_id", "require_tenant",
|
||
"normalize_tenant", "write_audit", "query_audit", "AUDIT_ACTIONS",
|
||
"get_env", "esc", "table_exists", "get_conn"):
|
||
v = getattr(c, n, None)
|
||
if v is not None:
|
||
setattr(mod, n, v)
|
||
mod.sor = getattr(c, "_sor")
|
||
mod.get_sor = getattr(c, "get_sor")
|
||
mod.sqlor = getattr(c, "get_sor")
|
||
mod._sor = getattr(c, "_sor")
|
||
mod.api_ok = lambda data=None, **kw: dict({"ok": True, "code": "OK", "data": data}, **kw)
|
||
mod.api_fail = lambda code, message="", http_status=None, detail=None: {
|
||
"ok": False, "code": str(code), "message": str(message or ""),
|
||
"detail": detail or {}, "http_status": int(http_status or 400)}
|
||
mod.health = lambda: {"ok": True, "module": "pbl_common(shim)", "shim": True}
|
||
mod.self_check = lambda: {"ok": True, "module": "pbl_common(shim)", "shim": True}
|
||
mod.tenant_of = lambda ctx=None, default=None: (
|
||
getattr(c, "normalize_tenant")(
|
||
(ctx or {}).get("tenant_id") if isinstance(ctx, dict) else None) or default)
|
||
mod.load_pbl_common = lambda *a, **kw: {
|
||
"ok": True, "shim": True, "module": "pbl_common",
|
||
"note": "pbl_common 半迁移不可导入,由 pbl_blueprint.m1b_compat 提供契约面"}
|
||
return mod
|
||
|
||
|
||
def _fill_audit(mod, c):
|
||
"""pbl_common.audit 契约面(append-only)。"""
|
||
for n in ("write_audit", "query_audit", "AUDIT_ACTIONS"):
|
||
v = getattr(c, n, None)
|
||
if v is not None:
|
||
setattr(mod, n, v)
|
||
mod.AUDIT_TABLE = "pbl_audit_log"
|
||
mod.append = lambda ctx, event_type, obj_type="", obj_id=0, **kw: getattr(c, "write_audit")(
|
||
event_type, obj_type, obj_id, tenant=(ctx or {}).get("tenant_id") if isinstance(ctx, dict) else None,
|
||
detail=kw)
|
||
mod.query = lambda ctx, cond=None, limit=100, offset=0: getattr(c, "query_audit")(
|
||
tenant=(ctx or {}).get("tenant_id") if isinstance(ctx, dict) else None,
|
||
limit=limit, offset=offset)
|
||
mod.can_cross_tenant = lambda ctx: False
|
||
mod.write_audit_batch = lambda records, ctx=None, db_conn=None: [
|
||
getattr(c, "write_audit")((r or {}).get("action", ""), (r or {}).get("obj_type", ""),
|
||
(r or {}).get("obj_id", ""), detail=r) for r in (records or [])]
|
||
mod.flush_memory_audit = lambda db_conn=None: 0
|
||
return mod
|
||
|
||
|
||
def _fill_crud_factory(mod, c):
|
||
"""pbl_common.crud_factory 契约面(含 init.py:54 引用的 _sor)。"""
|
||
mod._sor = getattr(c, "_sor")
|
||
mod.tenant_crud = getattr(c, "tenant_crud")
|
||
mod.crud_factory = getattr(c, "crud_factory")
|
||
mod.crud = getattr(c, "crud")
|
||
mod.flag = getattr(c, "flag")
|
||
|
||
class TenantCrud(object):
|
||
"""租户隔离 CRUD 薄包装(委托 m1b_compat.tenant_crud 闭包集)。"""
|
||
|
||
def __init__(self, table, tenant_field="tenant_id", write_protected=True, dbname=None):
|
||
self.table = table
|
||
self.tenant_field = tenant_field
|
||
self._ops = getattr(c, "tenant_crud")(
|
||
table, tenant_field=tenant_field, write_protected=write_protected, dbname=dbname)
|
||
|
||
def list(self, tenant, where="", args=None, fields="*", order="", limit=50, offset=0):
|
||
return self._ops["list"](tenant, where, args, fields, order, limit, offset)
|
||
|
||
def get(self, tenant, pk, pk_field="id"):
|
||
return self._ops["get"](tenant, pk, pk_field)
|
||
|
||
def count(self, tenant, where="", args=None):
|
||
return self._ops["count"](tenant, where, args)
|
||
|
||
def create(self, tenant, data):
|
||
return self._ops["create"](tenant, data)
|
||
|
||
def update(self, tenant, pk, data, pk_field="id"):
|
||
return self._ops["update"](tenant, pk, data, pk_field)
|
||
|
||
def delete(self, tenant, pk, pk_field="id"):
|
||
return self._ops["delete"](tenant, pk, pk_field)
|
||
|
||
mod.TenantCrud = TenantCrud
|
||
return mod
|
||
|
||
|
||
def _fill_generic(mod, c, names):
|
||
for n in names:
|
||
v = getattr(c, n, None)
|
||
if v is not None:
|
||
setattr(mod, n, v)
|
||
return mod
|
||
|
||
|
||
def install(force=False):
|
||
"""探测 pbl_common 可用性;不可用时注入垫片。返回是否激活。"""
|
||
global ACTIVE
|
||
c = _compat()
|
||
if c is None:
|
||
MISSING.append("pbl_blueprint.m1b_compat(unavailable)")
|
||
return False
|
||
if _probe(_PKG) and not force:
|
||
# 真实包可导入:逐个探测子模块,只补缺失的那些
|
||
for sub in _SUBS:
|
||
full = "%s.%s" % (_PKG, sub)
|
||
PROBED.append(full)
|
||
if _probe(full):
|
||
continue
|
||
MISSING.append(full)
|
||
mod = _mk_module(full)
|
||
_dispatch(mod, sub, c)
|
||
sys.modules[full] = mod
|
||
setattr(sys.modules[_PKG], sub, mod)
|
||
ACTIVE = bool(MISSING)
|
||
return ACTIVE
|
||
# 整包不可导入:注入包 + 全部子模块
|
||
PROBED.append(_PKG)
|
||
MISSING.append(_PKG)
|
||
pkg = _mk_module(_PKG, "pbl_common shim package (pbl_blueprint.m1b_compat backed)")
|
||
pkg.__path__ = []
|
||
sys.modules[_PKG] = pkg
|
||
for sub in _SUBS:
|
||
full = "%s.%s" % (_PKG, sub)
|
||
MISSING.append(full)
|
||
mod = _mk_module(full)
|
||
_dispatch(mod, sub, c)
|
||
sys.modules[full] = mod
|
||
setattr(pkg, sub, mod)
|
||
pkg.errors = sys.modules["%s.errors" % _PKG]
|
||
pkg.api = sys.modules["%s.api" % _PKG]
|
||
pkg.audit = sys.modules["%s.audit" % _PKG]
|
||
pkg.crud_factory = sys.modules["%s.crud_factory" % _PKG]
|
||
pkg.__version__ = "0.0.0+shim"
|
||
ACTIVE = True
|
||
return True
|
||
|
||
|
||
def _dispatch(mod, sub, c):
|
||
if sub == "errors":
|
||
return _fill_errors(mod, c)
|
||
if sub == "api":
|
||
return _fill_api(mod, c)
|
||
if sub == "audit":
|
||
return _fill_audit(mod, c)
|
||
if sub in ("crud_factory", "crud"):
|
||
return _fill_crud_factory(mod, c)
|
||
if sub == "context":
|
||
return _fill_generic(mod, c, ("require_tenant", "normalize_tenant", "tenant_id",
|
||
"actor_id", "get_env", "TenantMissingError",
|
||
"TenantInvalidError", "assert_not_write_protected"))
|
||
if sub == "tenant":
|
||
return _fill_generic(mod, c, ("require_tenant", "normalize_tenant", "tenant_id",
|
||
"assert_not_write_protected", "TenantMissingError",
|
||
"TenantInvalidError"))
|
||
if sub in ("dbutil", "db"):
|
||
return _fill_generic(mod, c, ("get_conn", "sql_exec", "sql_rows", "sql_scalar",
|
||
"table_exists", "get_sor", "_sor", "esc", "clear_cache"))
|
||
if sub == "util":
|
||
return _fill_generic(mod, c, ("new_id", "now_str", "json_dump", "json_load",
|
||
"dumps", "loads", "esc", "flag"))
|
||
if sub == "self_check":
|
||
mod.run_self_check = lambda *a, **kw: {
|
||
"ok": True, "module": "pbl_common(shim)", "shim": True, "checks": []}
|
||
return mod
|
||
return _fill_generic(mod, c, ("require_tenant", "new_id", "now_str"))
|
||
|
||
|
||
def is_active():
|
||
"""垫片是否已激活(True = 真实 pbl_common 不可用,本模块靠包内兼容层运行)。"""
|
||
return bool(ACTIVE)
|
||
|
||
|
||
def status():
|
||
"""诊断信息(供自检/交付证据引用)。"""
|
||
return {"active": bool(ACTIVE), "probed": list(PROBED), "missing": list(MISSING)}
|
||
|
||
|
||
# 导入即安装:pbl_blueprint/__init__.py 首行引入本模块,保证后续硬导入不炸
|
||
try:
|
||
install()
|
||
except Exception: # pragma: no cover - 垫片自身绝不阻断导入
|
||
ACTIVE = False
|