308 lines
11 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.api —— 公共内核对外契约面(重导出契约符号全集)。
【人工介入答复 ③(a)】下游 11+ 模块pbl_validation / pbl_compiler / pbl_assessment /
pbl_blueprint / pbl_appcodes / pbl_agent_runtime / pbl_evidence ...)历史上从
``pbl_common.api`` 取符号。本版把**契约符号全集**在此重导出,任何一处 import 都能命中:
from pbl_common.api import (
PBLError, err, fail, ErrorCode, CODE_TO_HTTP,
TenantMissingError, NotFoundError, DbError, ParamInvalidError,
assert_not_write_protected, WRITE_PROTECTED_MODULES,
TenantContext, with_tenant, tenant_scope, assert_tenant, normalize_tenant,
check_tenant_column, get_tenant, set_tenant,
Db, get_db, get_module_dbname, new_id, now_str, sqlExe,
CrudBase, tenant_crud, crud_factory,
write_audit, write_audit_batch, audit_trail, flush_memory_audit,
ok, fail_body, paged, api_guard,
)
【QC 退回意见 #2 / #5】旧 api.py:39 从 errors 导入 ``ParamInvalidError`` /
``ErrorCode``,而半迁移后的 errors.py 无这些名字 → import 即崩。现在 errors.py 已补齐
(规范类 + 兼容别名 + ErrorCode 常量面),本文件 import 闭包闭合。
本文件**只做重导出 + 统一响应体 helper**,不含业务逻辑,因此不引入新的依赖边:
依赖方向恒为 api → {errors, context, dbutil, crud_factory, audit},无环。
"""
from __future__ import annotations
import functools
import logging
# ---- 1. 错误码与异常(规范 + 兼容全集) ---------------------------------
from pbl_common.errors import (
ALL_CODES,
CODE_TO_HTTP,
DEFAULT_MESSAGES,
LEGACY_CODE_MAP,
PBL_ERR,
PBL_E_APPEND_ONLY,
PBL_E_COMPILE,
PBL_E_CONFLICT,
PBL_E_DB,
PBL_E_DB_UNAVAILABLE,
PBL_E_FORBIDDEN,
PBL_E_INTERNAL,
PBL_E_NEED_INFO,
PBL_E_NOT_FOUND,
PBL_E_OK,
PBL_E_PARAM,
PBL_E_TENANT,
PBL_E_TENANT_INVALID,
PBL_E_TENANT_MISSING,
PBL_E_TOOL_DENIED,
PBL_E_VALIDATION,
PBL_E_WRITE_LOCK,
PBL_E_WRITE_PROTECTED,
AppendOnlyError,
CompileError,
ConflictError,
DbError,
DbUnavailableError,
ErrorCode,
ForbiddenError,
NeedInfoError,
NotFoundError,
PBLError,
ParamInvalidError,
PblConflict,
PblError,
PblForbidden,
PblNotFound,
PblValidationError,
TenantError,
TenantInvalidError,
TenantMissingError,
ToolDeniedError,
ValidationError,
WriteLockError,
WriteProtectedError,
as_error,
default_message,
err,
error_body,
fail,
http_status_of,
is_write_protected,
normalize_code,
normalize_module,
)
from pbl_common.errors import (
READ_ACTIONS,
WRITE_ACTIONS,
WRITE_PROTECTED_MODULES,
assert_append_only,
assert_not_write_protected,
)
# ---- 2. 租户上下文 ------------------------------------------------------
from pbl_common.context import (
INVALID_TENANT_VALUES,
SYSTEM_TENANT_IDS,
TENANT_ID_PATTERN,
TenantContext,
assert_same_tenant,
assert_tenant,
check_tenant_column,
current_context,
current_tenant,
get_tenant,
has_tenant,
normalize_tenant,
reset_tenant,
run_in_tenant,
set_tenant,
tenant_scope,
tenant_scope_ctx,
with_tenant,
)
# ---- 3. DB 适配 ---------------------------------------------------------
from pbl_common.dbutil import (
Db,
cur_date_string,
fallback_enabled,
fetch_all,
fetch_one,
get_db,
get_module_dbname,
get_server_env,
insert_row,
new_id,
now_str,
quote_ident,
safe_ident,
sqlExe,
sqlor_available,
today_str,
update_row,
)
from pbl_common.dbutil import delete_row, query
# ---- 4. CRUD 工厂 -------------------------------------------------------
from pbl_common.crud_factory import (
AUDIT_TABLE_MARKERS,
CrudBase,
bulk_create,
bulk_delete,
clear_registry,
crud_factory,
is_audit_table,
make_crud,
registered_tables,
tenant_crud,
)
# ---- 5. 审计 ------------------------------------------------------------
from pbl_common.audit import (
AUDIT_ACTIONS,
AUDIT_TABLE,
audit_stats,
audit_trail,
build_audit_record,
flush_memory_audit,
forbid_audit_mutation,
write_audit,
write_audit_batch,
write_audit_sync,
)
log = logging.getLogger('pbl_common.api')
# ==========================================================================
# 统一响应体 helper
# ==========================================================================
def ok(data=None, message='ok', **extra):
"""成功响应体:{'ok': True, 'code': 'PBL_E_OK', 'data': ..., 'message': ...}。"""
body = {'ok': True, 'code': PBL_E_OK, 'legacy_code': LEGACY_CODE_MAP[PBL_E_OK],
'message': message, 'data': data, 'http_status': 200}
if extra:
body.update(extra)
return body
def fail_body(code=None, message=None, detail=None, **extra):
"""失败响应体(不抛异常,供 dspy 直接 return"""
body = err(code, message, detail, **extra).to_dict()
body['data'] = None
return body
def paged(rows, total=None, page=1, rows_per_page=20, **extra):
"""分页响应体。"""
body = ok({'total': len(rows) if total is None else total,
'rows': list(rows or []),
'page': int(page or 1),
'rows_per_page': int(rows_per_page or 20)},
message='ok')
if extra:
body.update(extra)
return body
def from_exception(exc):
"""异常 → 响应体(非 PBLError 归一为 PBL_E_INTERNAL"""
return as_error(exc).to_dict()
def api_guard(func=None, tenant_required=True, audit_action=None, audit_table=None):
"""API 层装饰器:租户门禁 + 写保护门禁 + 异常归一 + 可选审计。
被装饰函数抛出的 PBLError → 统一响应体http_status 由错误码映射);
非 PBLError → PBL_E_INTERNALfail-closed不泄露内部栈到前端栈进日志
"""
def deco(fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
tenant_id = kwargs.get('tenant_id')
try:
if tenant_required:
kwargs['tenant_id'] = assert_tenant(tenant_id)
if audit_table:
assert_not_write_protected(audit_table, audit_action or 'write')
result = await fn(*args, **kwargs)
if isinstance(result, dict) and 'ok' in result:
return result
return ok(result)
except Exception as exc: # noqa: BLE001
error = as_error(exc)
if error.code == PBL_E_INTERNAL:
log.exception('api_guard 未预期异常 in %s', getattr(fn, '__name__', fn))
if audit_action and audit_table:
try:
await write_audit(audit_action, table=audit_table,
row_id=kwargs.get('id'),
detail={'error': error.code,
'message': error.message},
tenant_id=kwargs.get('tenant_id'),
result='error')
except Exception: # noqa: BLE001
log.warning('api_guard 失败审计写入未成功')
return error.to_dict()
return wrapper
if func is not None:
return deco(func)
return deco
# ==========================================================================
# 契约自检import 期即验证符号面完整,失败直接抛 → 不允许半迁移状态被交付)
# ==========================================================================
CONTRACT_SYMBOLS = (
# errors
'PBLError', 'PblError', 'err', 'fail', 'as_error', 'error_body',
'ErrorCode', 'PBL_ERR', 'CODE_TO_HTTP', 'LEGACY_CODE_MAP', 'ALL_CODES',
'DbError', 'NotFoundError', 'ParamInvalidError', 'TenantMissingError',
'TenantInvalidError', 'ConflictError', 'ForbiddenError', 'WriteProtectedError',
'AppendOnlyError', 'ValidationError', 'CompileError', 'ToolDeniedError',
'NeedInfoError', 'PblValidationError', 'PblNotFound', 'PblConflict', 'PblForbidden',
'WRITE_PROTECTED_MODULES', 'assert_not_write_protected', 'assert_append_only',
'is_write_protected', 'normalize_code', 'http_status_of',
# context
'TenantContext', 'with_tenant', 'tenant_scope', 'assert_tenant',
'normalize_tenant', 'check_tenant_column', 'get_tenant', 'set_tenant',
'reset_tenant', 'current_context', 'assert_same_tenant',
# dbutil
'Db', 'get_db', 'get_module_dbname', 'get_server_env', 'new_id', 'now_str',
'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',
'bulk_delete', 'is_audit_table',
# audit
'write_audit', 'write_audit_batch', 'audit_trail', 'flush_memory_audit',
'build_audit_record', 'write_audit_sync', 'audit_stats', 'AUDIT_TABLE',
# response helper
'ok', 'fail_body', 'paged', 'from_exception', 'api_guard',
)
def verify_contract():
"""校验契约符号全集在本模块命名空间内可见。返回缺失清单(空=通过)。"""
ns = globals()
return [name for name in CONTRACT_SYMBOLS if name not in ns or ns[name] is None]
_MISSING = verify_contract()
if _MISSING: # pragma: no cover - 门禁
raise ImportError('pbl_common.api 契约符号缺失(半迁移状态禁止交付):%s'
% ', '.join(_MISSING))
__all__ = list(CONTRACT_SYMBOLS) + [
'CONTRACT_SYMBOLS', 'verify_contract', 'LEGACY_CODE_MAP', 'DEFAULT_MESSAGES',
'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',
'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',
'PBL_E_CONFLICT', 'PBL_E_FORBIDDEN', 'PBL_E_WRITE_PROTECTED',
'PBL_E_WRITE_LOCK', 'PBL_E_APPEND_ONLY', 'PBL_E_DB', 'PBL_E_DB_UNAVAILABLE',
'PBL_E_VALIDATION', 'PBL_E_COMPILE', 'PBL_E_TOOL_DENIED', 'PBL_E_NEED_INFO',
'PBL_E_INTERNAL', 'TenantError', 'WriteLockError', 'DbUnavailableError',
]