hscredit.database.types 源代码

"""数据库模块公共类型。

定义连接池配置、数据库能力、流式查询状态和写入结果等稳定契约。
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, FrozenSet, Mapping, Optional, Tuple

from ..exceptions import ValidationError

WRITE_MODES = frozenset({"a", "r", "o", "d"})
#: ``Database.query``、``stream_query`` 和 ``read_query`` 共用的结果类型。
RESULT_TYPES = frozenset({"dataframe", "records", "rows"})


def validate_result_type(value: Any) -> str:
    """校验并返回 Database 模块统一的查询结果类型。

    :param value: 期望的结果类型。
    :return: ``dataframe``、``records`` 或 ``rows``。
    :raises ValidationError: 输入不是受支持的字符串。
    """

    if not isinstance(value, str) or value not in RESULT_TYPES:
        raise ValidationError(f"result 只支持 {sorted(RESULT_TYPES)},收到 {value!r}")
    return value


[文档] class StreamState(str, Enum): """流式查询生命周期状态。 ``running`` 表示仍可读取;``completed`` 表示自然耗尽;``interrupted`` 表示主动停止 或键盘中断;``failed`` 表示读取失败;``closed`` 表示在耗尽前关闭。 """ RUNNING = "running" COMPLETED = "completed" INTERRUPTED = "interrupted" FAILED = "failed" CLOSED = "closed"
[文档] @dataclass(frozen=True) class PoolOptions: """DBUtils 兼容的连接池配置。 **参数** mincached、maxcached、maxshared、maxconnections 与 DBUtils ``PooledDB`` 含义一致,值为 0 时沿用 DBUtils 的“不限制”语义。 """ mincached: int = 0 maxcached: int = 0 maxshared: int = 0 maxconnections: int = 0 blocking: bool = False maxusage: Optional[int] = None setsession: Optional[Tuple[str, ...]] = None ping: int = 1
[文档] @classmethod def from_mapping(cls, value: Optional[Mapping[str, Any]] = None) -> "PoolOptions": """从映射创建并校验连接池配置。 :param value: ``PoolOptions``、参数映射或 ``None``。 :return: 已校验的不可变连接池配置。 :raises ValidationError: 包含未知参数或参数范围无效。 """ if value is None: return cls() if isinstance(value, cls): return value if not isinstance(value, Mapping): raise ValidationError("pool_options 必须是映射或 PoolOptions") allowed = set(cls.__dataclass_fields__) unknown = sorted(set(value) - allowed) if unknown: raise ValidationError(f"不支持的连接池参数: {unknown}") options = cls(**dict(value)) options._validate() return options
def _validate(self) -> None: integer_fields = ( "mincached", "maxcached", "maxshared", "maxconnections", "ping", ) for name in integer_fields: current = getattr(self, name) if isinstance(current, bool) or not isinstance(current, int) or current < 0: raise ValidationError(f"连接池参数 {name} 必须是非负整数") if self.maxusage is not None: if isinstance(self.maxusage, bool) or not isinstance(self.maxusage, int) or self.maxusage <= 0: raise ValidationError("连接池参数 maxusage 必须是正整数或 None") if not isinstance(self.blocking, bool): raise ValidationError("连接池参数 blocking 必须是布尔值") if self.maxcached and self.mincached > self.maxcached: raise ValidationError("连接池参数 mincached 不能大于 maxcached") if self.maxconnections and self.maxcached > self.maxconnections: raise ValidationError("连接池参数 maxcached 不能大于 maxconnections") if self.maxconnections and self.maxshared > self.maxconnections: raise ValidationError("连接池参数 maxshared 不能大于 maxconnections") if self.setsession is not None and not all(isinstance(sql, str) for sql in self.setsession): raise ValidationError("连接池参数 setsession 必须是 SQL 字符串序列")
[文档] def to_dbutils_kwargs(self) -> Dict[str, Any]: """转换为 DBUtils ``PooledDB`` 关键字参数。 :return: 可直接传给 ``PooledDB`` 的新字典。 :rtype: dict :raises ValidationError: 当前配置组合无效。 """ self._validate() result: Dict[str, Any] = { "mincached": self.mincached, "maxcached": self.maxcached, "maxshared": self.maxshared, "maxconnections": self.maxconnections, "blocking": self.blocking, "maxusage": self.maxusage, "ping": self.ping, } if self.setsession is not None: result["setsession"] = list(self.setsession) return result
[文档] @dataclass(frozen=True) class RedisPoolOptions: """Redis 原生连接池配置。""" max_connections: Optional[int] = None blocking: bool = False timeout: Optional[float] = None
[文档] @classmethod def from_mapping(cls, value: Optional[Mapping[str, Any]] = None) -> "RedisPoolOptions": """从映射创建 Redis 连接池配置。""" if value is None: return cls() if isinstance(value, cls): return value if not isinstance(value, Mapping): raise ValidationError("Redis pool_options 必须是映射或 RedisPoolOptions") unknown = sorted(set(value) - set(cls.__dataclass_fields__)) if unknown: raise ValidationError(f"不支持的 Redis 连接池参数: {unknown}") options = cls(**dict(value)) options._validate() return options
def _validate(self) -> None: if self.max_connections is not None: if ( isinstance(self.max_connections, bool) or not isinstance(self.max_connections, int) or self.max_connections <= 0 ): raise ValidationError("Redis 连接池参数 max_connections 必须是正整数或 None") if not isinstance(self.blocking, bool): raise ValidationError("Redis 连接池参数 blocking 必须是布尔值") if self.timeout is not None: if isinstance(self.timeout, bool) or not isinstance(self.timeout, (int, float)) or self.timeout < 0: raise ValidationError("Redis 连接池参数 timeout 必须是非负数或 None") if not self.blocking: raise ValidationError("Redis 连接池参数 timeout 仅在 blocking=True 时可用")
[文档] def to_redis_kwargs(self) -> Dict[str, Any]: """转换为 redis-py 连接池参数。""" self._validate() result: Dict[str, Any] = {} if self.max_connections is not None: result["max_connections"] = self.max_connections if self.blocking and self.timeout is not None: result["timeout"] = float(self.timeout) return result
[文档] @dataclass(frozen=True) class MongoPoolOptions: """PyMongo ``MongoClient`` 原生连接池配置。""" min_pool_size: int = 0 max_pool_size: int = 100 max_connecting: int = 2 wait_queue_timeout_ms: Optional[int] = None max_idle_time_ms: Optional[int] = None
[文档] @classmethod def from_mapping(cls, value: Optional[Mapping[str, Any]] = None) -> "MongoPoolOptions": """从映射创建 MongoDB 连接池配置。""" if value is None: return cls() if isinstance(value, cls): return value if not isinstance(value, Mapping): raise ValidationError("MongoDB pool_options 必须是映射或 MongoPoolOptions") unknown = sorted(set(value) - set(cls.__dataclass_fields__)) if unknown: raise ValidationError(f"不支持的 MongoDB 连接池参数: {unknown}") options = cls(**dict(value)) options._validate() return options
def _validate(self) -> None: for name in ("min_pool_size", "max_pool_size", "max_connecting"): current = getattr(self, name) if isinstance(current, bool) or not isinstance(current, int) or current < 0: raise ValidationError(f"MongoDB 连接池参数 {name} 必须是非负整数") if self.max_pool_size and self.min_pool_size > self.max_pool_size: raise ValidationError("MongoDB 连接池参数 min_pool_size 不能大于 max_pool_size") if self.max_connecting == 0: raise ValidationError("MongoDB 连接池参数 max_connecting 必须是正整数") for name in ("wait_queue_timeout_ms", "max_idle_time_ms"): current = getattr(self, name) if current is not None and ( isinstance(current, bool) or not isinstance(current, int) or current < 0 ): raise ValidationError(f"MongoDB 连接池参数 {name} 必须是非负整数或 None")
[文档] def to_mongo_kwargs(self) -> Dict[str, Any]: """转换为 ``MongoClient`` 驼峰连接池参数。""" self._validate() result: Dict[str, Any] = { "minPoolSize": self.min_pool_size, "maxPoolSize": self.max_pool_size, "maxConnecting": self.max_connecting, } if self.wait_queue_timeout_ms is not None: result["waitQueueTimeoutMS"] = self.wait_queue_timeout_ms if self.max_idle_time_ms is not None: result["maxIdleTimeMS"] = self.max_idle_time_ms return result
[文档] @dataclass(frozen=True) class DatabaseCapabilities: """数据库或目标表可保证的能力。 **参数** transactions : bool 是否支持事务提交和回滚。 streaming_read : bool 是否支持流式读取。 native_bulk_write : bool 是否具有后端原生批量写入通道。 metadata_export : bool 是否支持表结构扫描。 write_modes : frozenset[str] 可保证的 ``a/r/o/d`` 写入模式集合。 """ transactions: bool = True streaming_read: bool = True native_bulk_write: bool = False metadata_export: bool = True write_modes: FrozenSet[str] = field(default_factory=lambda: frozenset({"o", "d"})) def __post_init__(self) -> None: normalized = frozenset(self.write_modes) invalid = sorted(normalized - WRITE_MODES) if invalid: raise ValidationError(f"不支持的写入模式: {invalid}") object.__setattr__(self, "write_modes", normalized)
[文档] @dataclass class WriteResult: """流式写入结果。 **参数** mode : {"a", "r", "o", "d"} 本次写入模式。 completed : bool 是否完成全部批次和适配器收尾。 rows_received、rows_inserted、rows_updated、rows_skipped : int, optional 输入及后端报告的行数统计。 batches_committed : int 已成功提交的批次数。 failed_batch : int, optional 失败批次编号;准备阶段为 0,收尾阶段为已提交批次数加 1。 consistency : str, optional 后端最终一致性说明。 details : dict 适配器附加的原始统计信息。 """ mode: str completed: bool rows_received: int = 0 rows_inserted: Optional[int] = None rows_updated: Optional[int] = None rows_skipped: Optional[int] = None batches_committed: int = 0 failed_batch: Optional[int] = None consistency: Optional[str] = None details: Dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: if self.mode not in WRITE_MODES: raise ValidationError(f"mode 只支持 {sorted(WRITE_MODES)},收到 {self.mode!r}") for name in ("rows_received", "batches_committed"): value = getattr(self, name) if isinstance(value, bool) or not isinstance(value, int) or value < 0: raise ValidationError(f"{name} 必须是非负整数")
[文档] @dataclass(frozen=True) class NoSQLWriteResult: """Redis 与 MongoDB 共用的写入、更新和删除结果。""" operation: str acknowledged: bool affected_count: Optional[int] = None matched_count: Optional[int] = None modified_count: Optional[int] = None identifiers: Tuple[Any, ...] = field(default_factory=tuple) details: Mapping[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: if self.operation not in {"write", "delete"}: raise ValidationError("NoSQL operation 只支持 'write' 或 'delete'") if not isinstance(self.acknowledged, bool): raise ValidationError("NoSQL acknowledged 必须是布尔值") for name in ("affected_count", "matched_count", "modified_count"): value = getattr(self, name) if value is not None and (isinstance(value, bool) or not isinstance(value, int) or value < 0): raise ValidationError(f"NoSQL {name} 必须是非负整数或 None") object.__setattr__(self, "identifiers", tuple(self.identifiers)) object.__setattr__(self, "details", dict(self.details))
__all__ = [ "WRITE_MODES", "RESULT_TYPES", "validate_result_type", "StreamState", "PoolOptions", "RedisPoolOptions", "MongoPoolOptions", "DatabaseCapabilities", "WriteResult", "NoSQLWriteResult", ]