pbl_blueprint/pbl_blueprint/_pbl_common_shim.py
2026-09-17 16:59:58 +08:00

305 lines
12 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.

# -*- 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):
"""抛 PblErrorfail-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