"""
基本工具库
"""

import sys
import os
import time
import datetime
import random
import re
import urllib.parse as url
from loguru import logger
from rich import inspect as rich_inspect, print


__all__ = [
    "aes_encrypt",
    "aes_decrypt",
    "aes_keygen",
    "atexit_reg",
    "atexit_msg",
    "base64_decode",
    "base64_encode",
    "bson_encode",
    "bson_decode",
    "check_run",
    "clear",
    "clickhouse_client",
    "diskcache",
    "diskcache_deque",
    "cpu_count",
    "duckdb",
    "mongo",
    "mongodb",
    "mongo_create_index",
    "mongo_distinct",
    "mongo_dict",
    "mongo_find_one_and_update",
    "mongo_from_jsonl",
    "mongo_insert",
    "mongo_id",
    "mongo_list_collection_names",
    "mongo_queue",
    "mongo_sample",
    "mongo_tongji",
    "mongo_to_csv",
    "mongo_to_jsonl",
    "mysql",
    "mysql_client",
    "oss2_bucket",
    "oss2_find_file",
    "oss2_upload_content",
    "oss2_upload_file",
    "oss2_sign_url",
    "oss2_put_object_acl",
    "oss2_set_object_acl",
    "redisdb",
    "redis_queue",
    "cookies_dict2str",
    "cookies_str2dict",
    "copy_text",
    "copy_file",
    "dataclass",
    "deque",
    "ddddocr",
    "datetime",
    "defaultdict",
    "df",
    "faker",
    "filedir",
    "filepath",
    "filename",
    "fromtimestamp",
    "format_exception",
    "func_kv",
    "get_proxies",
    "getlock",
    "getpid",
    "gettid",
    "get_ip",
    "get_real_ip",
    "get_2fa_secret",
    "get_2fa_token",
    "headers",
    "image_from_base64",
    "image_to_base64",
    "ipython_extension",
    "ipython_embed",
    "iter_accumulate",
    "iter_count",
    "iter_batched",
    "iter_combinations",
    "iter_cycle",
    "iter_flatten",
    "iter_groupby",
    "iter_product",
    "iter_permutations",
    "iter_repeat",
    "iter_zip_longest",
    "js2py_eval",
    "jsonpath",
    "jsonpath_first",
    "jmespath",
    "listdir",
    "logger",
    "logger_init",
    "make_archive",
    "md2html",
    "moviepy_audio",
    "moviepy_video",
    "move_file",
    "md5",
    "now",
    "num2cn",
    "os",
    "open_url",
    "paste_text",
    "pdf2text",
    "pl",
    "pprint",
    "print_exception",
    "print",
    "print_json",
    "printx",
    "psutil_mem_keyword",
    "python",
    "queue",
    "random",
    "random_str",
    "re",
    "re_first",
    "re_findall",
    "read_csv",
    "read_json",
    "read_excel",
    "remove_file",
    "retry",
    "rich_columns",
    "rich_inspect",
    "rich_markdown",
    "rich_panel",
    "rich_padding",
    "rich_rule",
    "run_myself",
    "run_nohup",
    "run_process",
    "run_remote",
    "rsa_encrypt",
    "rsa_decrypt",
    "rsa_keygen",
    "send_bot",
    "send_mail",
    "set_title",
    "set_proxy",
    "sha1",
    "sha256",
    "sha512",
    "sleep",
    "sleep_progress",
    "status",
    "sys",
    "tempdir",
    "time_next",
    "time",
    "timeit",
    "time_fmt",
    "timeparse",
    "timeparse2",
    "timestamp",
    "to_excel",
    "to_gzip",
    "to_json",
    "today",
    "tongji_content",
    "track",
    "try_get",
    "ua",
    "url",
    "unzip_file",
    "unpack_archive",
    "unique_list",
    "uuid4",
    "wait",
    "which",
    "win11toast",
    "zip_file",
]

getpid = os.getpid
python = sys.executable


def filedir():
    """
    当前脚本文件所在目录
    """
    return os.path.dirname(os.path.abspath(sys.argv[0]))


def filepath(abs=True):
    """
    文件路径, 默认为绝对路径

    - abs: 是否返回绝对路径, 默认为 True; 如果为 False, 则返回相对路径
    """
    path = sys.argv[0]
    if not abs:
        return path
    return os.path.abspath(path)


def filename(path=None, ext=False):
    """
    获取当前文件名

    - path: 文件路径, 默认为 sys.argv[0]
    - ext: 是否返回扩展名, 默认为 False
    """
    if not path:
        path = sys.argv[0]
    if ext:
        return path.rsplit(os.path.sep, 1)[-1]
    return path.rsplit(os.path.sep, 1)[-1].rsplit(".", 1)[0]


def logger_init(
    log_file: str | None = None,
    level: str = "DEBUG",
    format: str = (
        "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
        "<level>{level: <8}</level> | "
        "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
    ),
    filter: str | None = None,
    colorize: bool | None = None,
    serialize: bool = False,
    backtrace: bool = True,
    diagnose: bool = True,
    enqueue: bool = False,
    catch: bool = True,
    rotation: str = "00:00",
    retention: str = "7 days",
    compression: str | None = None,
    **kwargs,
):
    """
    添加日志文件
    - log_file: 日志文件路径
    - level: 日志级别
    - format: 日志格式
    - filter: 日志过滤器
    - colorize: 是否启用颜色化
    - serialize: 是否序列化
    - backtrace: 是否显示回溯信息
    - diagnose: 是否显示诊断信息
    - enqueue: 是否启用队列
    - catch: 是否捕获异常
    - rotation: 日志文件分割条件, 支持以下格式:
        - "00:00": 每天 00:00 分割
        - "10 MB": 每 10 MB 分割
        - "1 week": 每周分割
        - "1 month": 每月分割
        - "1 year": 每年分割
        - "1 day": 每天分割
        - "1 hour": 每小时分割
        - "1 minute": 每分钟分割
        - "1 second": 每秒分割
        - "1 week, 00:00": 每周 00:00 分割
        - "1 month, 00:00": 每月 00:00 分割
        - "1 year, 00:00": 每年 00:00 分割
        - "1 day, 00:00": 每天 00:00 分割
        - "1 hour, 00:00": 每小时 00:00 分割
        - "1 minute, 00:00": 每分钟 00:00 分割
        - "1 second, 00:00": 每秒 00:00 分割
    - retention: 日志文件保留时间
    - compression: 日志文件压缩格式, 支持 "zip", "tar", "gz", "bz2", "xz" 等
    """
    import sys

    if not log_file:
        if "IPython" in sys.modules:
            return
        log_file = os.path.join(filedir(), "log", f"{{time:YYYYMMDD}}_{filename()}.log")
    logger.add(
        log_file,
        level=level,
        format=format,
        filter=filter,
        colorize=colorize,
        serialize=serialize,
        backtrace=backtrace,
        diagnose=diagnose,
        enqueue=enqueue,
        catch=catch,
        rotation=rotation,
        retention=retention,
        compression=compression,
        **kwargs,
    )


def queue(maxsize=0):
    """
    队列, 默认为 0 表示不限制。

    - maxsize: 最大长度
    """
    import queue

    return queue.Queue(maxsize=maxsize)


def deque(maxlen=None):
    """
    队列, 默认为 None 表示不限制。

    - maxlen: 最大长度
    """
    import collections

    return collections.deque(maxlen=maxlen)


def timeparse(
    timestr: str,
    parserinfo: None = None,
    dayfirst: bool | None = None,
    yearfirst: bool | None = None,
    ignoretz: bool = True,
    fuzzy: bool = False,
    fuzzy_with_tokens: bool = False,
    default: None = None,
    tzinfos: None = None,
):
    """
    ## 基于 dateutil.parser 封装

    - timestr: 时间字符串
    - parserinfo: 解析器信息
    - dayfirst: 无法判断时, 第一个数字是否为日
    - yearfirst: 无法判断时, 第一个数字是否为年份
    - ignoretz: 是否忽略时区信息, 默认忽略时区信息
    - fuzzy: 是否启用模糊匹配, 启动后可以从字符串中提取出时间字符串中的日期和时间信息
    - fuzzy_with_tokens: 是否返回模糊匹配的结果
    - default: 当解析失败时返回的默认值
    - tzinfos: 时区信息
    """
    from dateutil.parser import parse

    return parse(
        timestr,
        parserinfo=parserinfo,
        dayfirst=dayfirst,
        yearfirst=yearfirst,
        ignoretz=ignoretz,
        fuzzy=fuzzy,
        fuzzy_with_tokens=fuzzy_with_tokens,
        default=default,
        tzinfos=tzinfos,
    )


def timeparse2(
    date_string,
    date_formats=None,
    languages=None,
    locales=None,
    region=None,
    settings=None,
    detect_languages_function=None,
):
    """
    ## dateparser 封装

    - date_string: 时间字符串
    - date_formats: 时间格式列表, 例如 ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"]
    - languages: 语言列表, 例如 ["zh", "en"]
    - locales: 地区列表, 例如 ["zh", "en"]
    - region: 地区
    - settings: 设置
    - detect_languages_function: 检测语言函数
    """
    from dateparser import parse

    return parse(
        date_string,
        date_formats=date_formats,
        languages=languages,
        locales=locales,
        region=region,
        settings=settings,
        detect_languages_function=detect_languages_function,
    )


def get_2fa_secret():
    """
    2FA(双因素身份验证)密钥生成
    """
    secret = "".join(random.sample("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", 32))
    return secret


def get_2fa_token(secret):
    """
    2FA(双因素身份验证)通过密钥获取实时 token

    - secret: 2FA(双因素身份验证)密钥, 可以调用 get_2fa_secret() 获取
    """
    import base64
    import hmac
    import time

    secret_bytes = base64.b32decode(secret)
    time_interval = int(time.time()) // 30
    hmac_value = hmac.new(secret_bytes, time_interval.to_bytes(8), "sha1").digest()
    offset = hmac_value[-1] & 0x0F
    dynamic_code = (
        (hmac_value[offset] & 0x7F) << 24
        | (hmac_value[offset + 1] & 0xFF) << 16
        | (hmac_value[offset + 2] & 0xFF) << 8
        | (hmac_value[offset + 3] & 0xFF)
    )
    return f"{dynamic_code % 10**6:06d}"


def sha1(string: str | bytes) -> str:
    """
     hashlib.sha1 封装

    - string: 字符串或字节
    """
    import hashlib

    if not isinstance(string, bytes):
        string = f"{string}".encode()
    return hashlib.sha1(string).hexdigest()


def sha256(string: str | bytes) -> str:
    """
     hashlib.sha256 封装

    - string: 字符串或字节
    """
    import hashlib

    if not isinstance(string, bytes):
        string = f"{string}".encode()
    return hashlib.sha256(string).hexdigest()


def sha512(string: str | bytes) -> str:
    """
     hashlib.sha512 封装

    - string: 字符串或字节
    """
    import hashlib

    if not isinstance(string, bytes):
        string = f"{string}".encode()
    return hashlib.sha512(string).hexdigest()


def md5(string: bytes | str):
    """
    md5 加密

    - string: 字符串或字节
    """
    import hashlib

    if isinstance(string, str):
        string = string.encode()
    result = hashlib.md5(string).hexdigest()
    return result


def gettid():
    """
    获取线程ID
    """
    import threading

    return threading.get_ident()


def getlock():
    """
    获取线程锁
    """
    import threading

    return threading.Lock()


def ipython_extension():
    """
    ipython 插件, 全局 pretty print, 全局 traceback
    """
    from rich.pretty import install
    from rich.traceback import install as tr_install

    install()
    tr_install()


def ipython_embed(header=""):
    """
    ipython 插件, 用于嵌入 ipython 环境
    """
    from IPython import embed

    embed(header=header)


def faker():
    """
    faker 封装，增加 faker().zh 为中文
    """
    if hasattr(faker, "fake"):
        return faker.fake

    from faker import Faker

    fake = Faker()
    fake.zh = Faker("zh")
    faker.fake = fake
    return fake


def base64_encode(string: bytes | str, decode: bool = True):
    """
    base64编码

    - string: 字符串或字节
    - decode: 是否解码, 默认为 True
    """
    import base64

    if isinstance(string, str):
        string = string.encode()
    if decode:
        return base64.b64encode(string).decode()
    return base64.b64encode(string)


def base64_decode(string: bytes | str, decode: bool = True):
    """
    base64解码

    - string: 字符串或字节
    - decode: 是否解码, 默认为 True
    """
    import base64

    if isinstance(string, str):
        string = string.encode()
    if decode:
        return base64.b64decode(string).decode()
    return base64.b64decode(string)


def clear():
    """
    清屏
    """
    if os.name == "nt":
        os.system("cls")
    else:
        os.system("clear")


def get_proxies(
    n: int | str | list = 1,
    httpx: bool = False,
    proxies_dict: dict | None = None,
):
    """
    ## 随机返回代理

    如果未设置 proxies_dict，则检测 get_proxies.proxies_dict

    - n: 代理选择, 数字会自动转换为对应的字符串, 列表会随机选择一个
    - httpx: 是否返回 httpx 格式的代理, 默认为 False
    - proxies_dict: 代理字典，{'1': ['https://localhost:7890'], '2': ['https://localhost:7891']}
    """
    if not proxies_dict:
        if hasattr(get_proxies, "proxies_dict"):
            proxies_dict = get_proxies.proxies_dict
        if not proxies_dict:
            print("[red]未配置 proxies_dict 或 get_proxies.proxies_dict[/]")
            raise Exception()
    if isinstance(n, list):
        n = random.choice(n)
    proxyurl = proxies_dict.get(str(n), [None])
    if isinstance(proxyurl, list):
        proxyurl = random.choice(proxyurl)
    if type(proxyurl).__name__ == "function":
        proxyurl = proxyurl()
    if httpx:
        proxies = proxyurl
    else:
        proxies = {
            "http": proxyurl,
            "https": proxyurl,
        }
    return proxies


def listdir(
    path=None,
    key=None,
    reverse: bool = False,
    glob: bool = False,
    *,
    root_dir: str | None = None,
    dir_fd: int | None = None,
    recursive: bool = False,
    include_hidden: bool = False,
):
    """
    文件夹下文件列表, 基于 os.listdir 和 glob 封装。

    - path: 目录路径, 默认当前目录。
    - key: 排序方式, 默认为 None。
    - reverse: 指定是否反转, 默认否。
    - glob: 是否使用 glob 方式, 默认否, 。
    - root_dir: 根目录, 默认当前目录。
    - dir_fd: 目录描述符, 默认 None。
    - recursive: 是否递归, 默认否。
    - include_hidden: 是否包含隐藏文件, 默认否。
    """
    import os

    if glob or root_dir or dir_fd or recursive or include_hidden:
        from glob import glob as _glob

        if not path:
            path = os.getcwd()

        return sorted(
            _glob(
                path,
                root_dir=root_dir,
                dir_fd=dir_fd,
                recursive=recursive,
                include_hidden=include_hidden,
            ),
            key=key,
            reverse=reverse,
        )

    return sorted(
        os.listdir(path),
        key=key,
        reverse=reverse,
    )


def now(fmt_type: int | None = None, fmt: str | None = None):
    """
    默认返回当前时间, 精度到秒。

    - fmt_type: 格式化类型。
    - fmt: 通过 strformat 格式化。

    - fmt=None -> datetime.datetime(2024, 1, 18, 11, 44, 57)
    - fmt_type=1 -> "2024-01-18 11:44:57"
    - fmt_type=2 -> 1705549497 # 10位时间戳
    - fmt_type=3 -> 1705555472772 # 13位时间戳
    - fmt_type=4 -> datetime.date(2024, 1, 18)
    - fmt_type=5 -> "2024-01-18"
    - fmt_type=6 -> "2024/01/18"
    - fmt_type=7 -> "20240118"
    """
    if fmt:
        return datetime.datetime.now().strftime(fmt)
    if fmt_type == 1:
        return f"{datetime.datetime.now().replace(microsecond=0)}"
    elif fmt_type == 2:
        return int(time.time())
    elif fmt_type == 3:
        return int(time.time() * 1000)
    elif fmt_type == 4:
        return datetime.date.today()
    elif fmt_type == 5:
        return f"{datetime.date.today()}"
    elif fmt_type == 6:
        return f"{datetime.date.today():%Y/%m/%d}"
    elif fmt_type == 7:
        return f"{datetime.date.today():%Y%m%d}"
    else:
        return datetime.datetime.now().replace(microsecond=0)


def timestamp(n: int = 10):
    """
    返回时间戳

    - n: 时间戳位数, 常用 10位、13位、14位，最大支持19位。
    """
    if n != 10:
        return int(str(time.time_ns())[:n])
    return int(time.time())


def today(fmt_type=None):
    """
    返回今天日期

    - fmt_type: 格式化类型

    ```
    1 -> "2024-01-18"
    2 -> "2024/01/18"
    3 -> "20240118"
    ```
    """
    if not fmt_type:
        return datetime.date.today()
    if fmt_type == 1:
        return f"{datetime.date.today()}"
    elif fmt_type == 2:
        return f"{datetime.date.today():%Y/%m/%d}"
    elif fmt_type == 3:
        return f"{datetime.date.today():%Y%m%d}"


def tongji_content(
    content: str | list,
    keyword: str | list | None = None,
    to_list: bool = False,
) -> dict[str, int] | list[str]:
    """
    统计关键词出现次数

    - content: 需要统计的文本
    - keyword: 关键词, 不指定默认为按字符统计; 可以指定关键词, 例如 ["a", "b", "c"]
    - to_list: 是否导出为列表, 默认为 False, 导出为字典, 例如 {"a": 1, "b": 2, "c": 3}
    """
    from collections import Counter

    if keyword:
        tongji_dict = {k: v for k in set(keyword) if (v := content.count(k))}
        if not to_list:
            return dict(tongji_dict)
        return [
            f"{k}({v})"
            for k, v in sorted(
                tongji_dict.items(),
                key=lambda x: x[-1],
                reverse=True,
            )
        ]
    tongji_dict = Counter(content)
    if not to_list:
        return dict(tongji_dict)
    return [
        f"{k[0]}({k[1]})"
        for k in sorted(
            tongji_dict.items(),
            key=lambda x: x[-1],
            reverse=True,
        )
    ]


def time_next(
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    weeks: int = 0,
    months: int = 0,
    years: int = 0,
    weekday: int | None = None,
    start: tuple | datetime.datetime | None = None,
    fmt: str | None = None,
):
    """
    返回下一个的时间, 默认为0, 负数为过去, 正数为未来。先算月份再算年, 然后依次计算。

    - days: 偏移天数
    - hours: 偏移小时数
    - minutes: 偏移分钟数
    - seconds: 偏移秒数
    - weeks: 偏移周数
    - months: 偏移月数
    - years: 偏移年数
    - weekday: 下一个周几，1到7表示周一到周日。-1到-7表示上一个周一到周日。可以配合 start 参数来指定开始时间。
    - start: 开始时间, (2024, 1, 20, 10, 12, 23) 或者 datetime.datetime(2024, 1, 20, 10, 12, 23)
    - fmt: 指定输出格式。
    """
    import calendar
    import datetime

    if start:
        if isinstance(start, datetime.datetime):
            start = start
        elif isinstance(start, datetime.date):
            start = datetime.datetime(*start.timetuple()[:6])
        else:
            start = datetime.datetime(*start)
    else:
        start = datetime.datetime.now().replace(microsecond=0)
    if months:
        # 获取下个时间的年份和月份
        next_month = start.month + months
        next_year = start.year + (next_month == 12)
        if next_month > 12:
            next_year += next_month // 12
            next_month = next_month % 12
        if next_month < 1:
            next_year -= abs(next_month) // 12 + 1
            next_month = 12 - abs(next_month) % 12

        # 获取下个时间的最大天数
        next_month_max_day = calendar.monthrange(next_year, next_month)[1]
        start = start.replace(
            year=next_year, month=next_month, day=min(start.day, next_month_max_day)
        )
    if years:
        real_next_year = start.year + years
        next_month_max_day = calendar.monthrange(real_next_year, start.month)[1]
        start = start.replace(
            year=real_next_year,
            month=start.month,
            day=min(start.day, next_month_max_day),
        )
    if weekday:
        days = days + abs(weekday) - 1 - start.weekday()
        if weekday > 0:
            days += 7 * (days < 0)
        if weekday < 0:
            days -= 7 * (days > 0)
    results = start + datetime.timedelta(
        days=days,
        hours=hours,
        minutes=minutes,
        seconds=seconds,
        weeks=weeks,
    )

    if fmt:
        return results.strftime(fmt)
    else:
        return results


def sleep(seconds: float):
    """
    等待n秒, 模拟阻塞

    - seconds: 等待秒数
    """
    return time.sleep(seconds)


def sleep_progress(
    n: int = 1,
    info: str | None = None,
    transient=False,
):
    """
    等待n秒, 模拟阻塞, 进度条。

    - n: 等待秒数
    - info: 定制输出信息
    - transient: 运行完是否隐藏, 默认为 False
    """
    if not info:
        info = f"等待{n}秒"
    for _ in track(range(int(n // 0.02)), info, transient=transient):
        time.sleep(0.02)


def fromtimestamp(timestamp: int | str | float, split: bool = True):
    """
    从时间戳返回 datetime 格式, 自动判断字符串和数字, 不符合格式要求会原样返回。

    - timestamp: 时间戳
    - split: 是否截取10位时间戳
    """
    try:
        if split:
            return datetime.datetime.fromtimestamp(float(str(timestamp)[:10]))
        else:
            return datetime.datetime.fromtimestamp(float(timestamp))
    except Exception:
        return timestamp


def read_json(path_string, encoding="utf-8") -> dict:
    """
    JSON 反序列化, 读取 JSON 文件

    - path_string: JSON 文件路径或字符串
    - encoding: 编码
    """
    import json

    if not os.path.isfile(path_string):
        try:
            return json.loads(path_string)
        except Exception:
            try:
                import ast

                return ast.literal_eval(path_string)
            except Exception:
                import demjson3

                return demjson3.decode(path_string)
    with open(path_string, "r", encoding=encoding) as f:
        try:
            q = json.load(f)
        except Exception:
            try:
                import ast

                q = ast.literal_eval(f.read())
            except Exception:
                import demjson3

                q = demjson3.decode(f.read())
        return q


def read_excel(
    io,
    sheet_name: str | int | list | None = 0,
    **kwargs,
):
    """
    读取 Excel 文件

    - io: 文件路径或文件对象
    - sheet_name: 工作表名称
    ---
    其他参数
    - header: 表头所在行
    - names: 列名
    - index_col: 索引列
    - usecols: 列选择
    - dtype: 数据类型
    - engine: 引擎
    - converters: 转换器
    - true_values: 真值
    - false_values: 假值
    - skiprows: 跳过行数
    - nrows: 读取行数
    - na_values: 空值
    - keep_default_na: 默认空值
    - na_filter: 空值过滤
    - verbose: 详细信息
    - parse_dates: 解析日期
    - date_format: 日期格式
    - thousands: 千位分隔符
    - decimal: 小数点分隔符
    - comment: 注释
    - skipfooter: 跳过尾部行数
    - storage_options: 存储选项
    - dtype_backend: "pyarrow" | "numpy_nullable"
    - engine_kwargs: 引擎参数
    """
    import pandas as pd

    return pd.read_excel(
        io,
        sheet_name=sheet_name,
        **kwargs,
    )


def read_csv(
    filepath_or_buffer,
    encoding="utf-8",
    sep=",",
    header: int | list[int] | None = 0,
    names: list[str] | None = None,
    **kwargs,
):
    """
    CSV 序列化, 读取 CSV 文件

    - filepath_or_buffer: 文件路径或文件对象
    - encoding: 编码
    - sep: 分隔符
    - header: 表头所在行
    - names: 列名
    """
    import pandas as pd

    return pd.read_csv(
        filepath_or_buffer,
        encoding=encoding,
        sep=sep,
        header=header,
        names=names,
        **kwargs,
    )


def to_json(
    obj,
    filename=None,
    encoding="utf-8",
    ensure_ascii: bool = False,
    indent: int | None = None,
    separators: tuple | None = None,
    default_dict: dict | None = None,
):
    """
    JSON 序列化, 将Python 对象写入文件或转换为字符串。

    - filename: JSON 文件路径
    - indent: 缩进
    - ensure_ascii: 默认不转为unicode
    - separators: 分隔符, 默认为 (", ", ": ")
    - default_dict: 默认会转换成字符串,支持自定义转换类型。例如: {numpy.ndarray: lambda x: x.tolist()}
    """
    import json

    def default_func(obj):
        if default_dict:
            for i in default_dict:
                if isinstance(obj, i):
                    return default_dict[i](obj)
            else:
                return str(obj)
        elif isinstance(obj, set):
            return list(obj)
        else:
            return str(obj)

    if not filename:
        return json.dumps(
            obj,
            indent=indent,
            ensure_ascii=ensure_ascii,
            separators=separators,
            default=default_func,
        )
    else:
        with open(filename, "w", encoding=encoding) as f:
            json.dump(
                obj,
                f,
                indent=indent,
                ensure_ascii=ensure_ascii,
                separators=separators,
                default=default_func,
            )


def get_ip():
    """
    获取本机 IP
    """
    import socket

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.connect(("8.8.8.8", 80))
    ip = s.getsockname()[0]
    s.close()
    return ip


def get_real_ip(proxies=None):
    """
    获取本机真实 IP

    - proxies: 代理
    """
    import requests

    if proxies:
        if isinstance(proxies, str):
            proxies = {"all": proxies}
        elif isinstance(proxies, dict):
            proxies = {
                "all": (
                    proxies.get("http")
                    or proxies.get("https")
                    or proxies.get("all")
                    or proxies.get("http://")
                    or proxies.get("https://")
                )
            }

    ip = requests.get("https://httpbin.org/ip", proxies=proxies).json()
    ip.update({"proxies": proxies})
    return ip


def df(
    data,
    index: list | None = None,
    columns: list | None = None,
    dtype: dict | None = None,
    copy: bool = False,
):
    """
    转换为 DataFrame

    - data: 数据
    - index: 索引
    - columns: 列名
    - dtype: 数据类型
    - copy: 是否复制
    """
    import pandas as pd

    return pd.DataFrame(
        data,
        index=index,
        columns=columns,
        dtype=dtype,
        copy=copy,
    )


def pl(data, **kwargs):
    """
    转换为 Polars DataFrame

    - data: 数据
    """
    import polars as pl

    return pl.DataFrame(data, **kwargs)


def md2html(text: str, **kwargs):
    """
    将 markdown 转换为 html

    - text: markdown 字符串
    """
    import markdown

    return markdown.markdown(text, **kwargs)


def to_excel(
    df,
    path: str,
    mode: str = "w",
    index: bool = False,
    engine: str = "xlsxwriter",
):
    """
    ## 导出 excel

    - df: dataframe 对象, 多个sheet可以通过字典传入, 键为sheet名称。`{"sheet_name1": df1, "sheet_name2": df2}`
    - path: 文件保存路径
    - mode: 默认 `w` 为全新写入; 如果为 `a` 则为插入, 引擎会改为 openpyxl。
    - strings_to_urls: 默认不转换url
    - index: 默认不导出索引
    - engine: 默认引擎使用xlsxwriter, 其他选项有 'auto'、'openpyxl'、'odf'
    """
    import pandas as pd

    if engine == "xlsxwriter":
        engine_kwargs = {"options": {"strings_to_urls": False}}

    if mode == "a":
        engine = "openpyxl"
        engine_kwargs = None

    with pd.ExcelWriter(
        path,
        engine=engine,  # type: ignore
        mode=mode,  # type: ignore
        engine_kwargs=engine_kwargs,
    ) as writer:
        if isinstance(df, dict):
            for sheet in df:
                df.get(sheet).to_excel(
                    writer,
                    index=index,
                    sheet_name=sheet,
                )
        else:
            df.to_excel(writer, index=index)


def print_exception(limit=None, file=None, chain=True) -> None:
    """
    打印异常信息

    - limit: 限制打印行数
    - file: 文件
    - chain: 是否打印异常链
    """
    from traceback import print_exc

    print_exc(limit=limit, file=file, chain=chain)


def format_exception(limit=None, chain=True) -> str:
    """
    # 格式化异常信息

    - limit: 限制打印行数
    - chain: 是否打印异常链
    """
    from traceback import format_exc

    return format_exc(limit=limit, chain=chain)


def retry(
    nums: int = 3,
    log: bool = False,
    show_error: bool = False,
    sleep: float = 0.1,
):
    """
    ## 重试, 支持同步和异步函数

    - nums: 默认重试 3 次。
    - log: 日志, 开启后会打印详细的错误日志。
    - show_error: 重试周期内未成功是否抛出错误, 默认不抛出, 多线程池建议打开该选项, 可以更好的统计运行情况。
    - sleep: 每次重试间隔时间, 默认 1 秒。

    ```python
    import jsz

    @jsz.retry(3, log=True)
    def crawl():
        url = "https://www.google.com"
        res = jsz.requests(url, timeout=2)
        print(res)

    crawl()
    ```
    """
    from functools import wraps
    import asyncio

    def wrap(f):
        @wraps(f)
        def func(*args, **kwargs):
            error = Exception
            for num in range(nums):
                try:
                    return f(*args, **kwargs)
                except Exception as e:
                    error = e
                    if log:
                        logger.error(
                            f"函数 {f.__name__} | 第 {num + 1}/{nums} 次运行失败: {format_exception()}"
                        )
                    time.sleep(sleep)
            if show_error:
                raise error

        @wraps(f)
        async def async_func(*args, **kwargs):
            error = Exception
            for num in range(nums):
                try:
                    return await f(*args, **kwargs)
                except Exception as e:
                    error = e
                    if log:
                        logger.error(
                            f"函数 {f.__name__} | 第 {num + 1}/{nums} 次运行失败: {format_exception()}"
                        )
                    await asyncio.sleep(sleep)

            if show_error:
                raise error

        if asyncio.iscoroutinefunction(f):
            return async_func
        else:
            return func

    return wrap


def send_bot(
    content: str = "测试",
    bot_key: str = "",
    msgtype: str = "markdown",
    filepath: str | None = None,
    mentioned_list: list = [],
    proxies=None,
):
    """
    ## 企业微信群机器人

    ```
    <font color="info">绿色</font>
    <font color="comment">灰色</font>
    <font color="warning">橙红色</font>
    ```

    - content: 文本
    - bot_key: 微信机器人key, 也可以设置环境变量 BOT_KEY 自动读取, 参数权重高于环境变量。
    - msgtype: 类型, 包括 markdown, markdown_v2, text, file, voice, image
    - filepath: 文件路径，非文本类型使用。
    - mentioned_list: 提醒用户列表, 填入手机号或 "@all", 仅支持 text 类型。
    - proxies: 代理
    """
    import requests

    if proxies:
        if isinstance(proxies, str):
            proxies = {"all": proxies}
        elif isinstance(proxies, dict):
            proxies = {
                "all": (
                    proxies.get("http")
                    or proxies.get("https")
                    or proxies.get("all")
                    or proxies.get("http://")
                    or proxies.get("https://")
                )
            }

    msg = {}
    if msgtype not in {"markdown", "markdown_v2", "text", "file", "voice", "image"}:
        raise NameError(
            '类型仅支持 "markdown", "markdown_v2", "text", "file", "voice", "image"'
        )
    if not bot_key:
        bot_key = os.getenv("BOT_KEY", "")
        if not bot_key:
            print("[red]未配置全局变量 BOT_KEY，也未填写参数 bot_key[/]")
            raise Exception("请填写 bot_key")
    if "key=" in bot_key:
        raise Exception("bot_key 只需要填入 `key=` 后面的部分")
    if mentioned_list:
        msgtype = "text"
    if msgtype == "file" or msgtype == "voice":
        file_upload_url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={bot_key}&type=file"
        files = {filepath.rsplit(".", 1)[0]: open(filepath, "rb")}
        for _ in range(3):
            try:
                resp = requests.post(
                    file_upload_url,
                    files=files,
                    proxies=proxies,
                    timeout=100,
                )
                msg = {"media_id": resp.json().get("media_id")}
                break
            except Exception:
                time.sleep(0.5)
        else:
            logger.error("上传文件失败")
            return
    elif msgtype == "image":
        content = open(filepath, "rb").read()
        msg = {"base64": base64_encode(content), "md5": md5(content)}
    elif msgtype == "text":
        msg = {"content": content, "mentioned_mobile_list": mentioned_list}
    else:
        msg = {"content": content}
    url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={bot_key}"
    for _ in range(3):
        try:
            response = requests.post(
                url,
                json={"msgtype": msgtype, msgtype: msg},
                proxies=proxies,
                timeout=100,
            )
            return response
        except Exception:
            time.sleep(0.5)
    logger.error("发送失败")


def pdf2text(filepath, strict=False, password=None):
    """
    从 pdf 提取文本内容

    - filepath: 文件路径
    - strict: 是否严格模式
    - password: 密码
    """
    from pypdf import PdfReader

    reader = PdfReader(filepath, strict=strict, password=password)
    content = "".join([i.extract_text() for i in reader.pages])
    return content


def timeit(function):
    """
    计时器, 支持同步和异步函数

    ```
    @timeit
    def hello():
        time.sleep(5)
    ```
    """
    from functools import wraps
    import asyncio

    @wraps(function)
    def func(*args, **kwargs):
        t0 = time.time() * 1000
        result = function(*args, **kwargs)
        print(f"运行 {function.__name__} 耗时: {time.time() * 1000 - t0:.2f} ms")
        return result

    @wraps(function)
    async def async_func(*args, **kwargs):
        t0 = time.time() * 1000
        result = await function(*args, **kwargs)
        print(f"运行 {function.__name__} 耗时: {time.time() * 1000 - t0:.2f} ms")
        return result

    if asyncio.iscoroutinefunction(function):
        return async_func
    else:
        return func


def ua(version_from=101, version_to=133, close=True):
    """
    随机ua

    - versison_from: chrome 版本大于101
    - version_to: chrome 版本小于132
    - close: 是否关闭连接, 默认为 True
    """
    ua_list = [
        f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{random.randint(version_from, version_to)}.0) Gecko/20100101 Firefox/{random.randint(version_from, version_to)}.0",
        f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{random.randint(version_from, version_to)}.0.0.0 Safari/537.36 Edg/{random.randint(version_from, version_to)}.0.0.0",
    ]

    return {
        "connection": "close" if close else "keep-alive",
        "user-agent": random.choice(ua_list),
    }


headers = ua()


def wait(n: int = 0, daemon: bool = True):
    """
    后台运行, 异步不阻塞。

    - n: 等待秒数
    - daemon: 是否为守护线程, 默认为 True。设置为 False 时, 主线程会等待定时器运行结束。

    ```python
    @jsz.wait(2)
    def hello():
        print('运行完毕')

    hello()
    ```
    """
    from threading import Timer
    from functools import wraps

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            def delayed_call():
                return func(*args, **kwargs)

            t = Timer(n, delayed_call)
            t.daemon = daemon
            t.start()
            return t

        return wrapper

    return decorator


def ddddocr(
    img,
    png_fix: bool = False,
    old: bool = False,
    new: bool = False,
    use_gpu: bool = False,
    onnx_path: str = "",
    charsets_path: str = "",
):
    """
    ## 使用ddddocr识别验证码

    - img: 图片对象，支持传入文件路径
    - png_fix: 是否修复图片，修复后支持黑色透明图片
    - old: 老模型
    - new: 新模型
    - use_gpu: 使用GPU
    - onnx_path: onnx 模型路径
    - charsets_path: 字符集路径
    """
    import ddddocr

    if os.path.exists(img):
        img = open(img, "rb").read()
    d = ddddocr.DdddOcr(
        show_ad=False,
        old=old,
        beta=new,
        use_gpu=use_gpu,
        import_onnx_path=onnx_path,
        charsets_path=charsets_path,
    )
    return d.classification(img=img, png_fix=png_fix)


def win11toast(
    title: str = "通知",
    message: str = "测试一下",
    app_name="通知",
    app_icon="",
    timeout=10,
    ticker="",
    toast=False,
):
    """
    Windows 通知

    - title: 标题
    - msg: 内容
    - app_name: 应用名称
    - app_icon: 应用图标, 支持路径
    - timeout: 超时时间, 默认为 10 秒
    - ticker: 通知提示
    - toast: 是否为 toast 通知, 默认为 False
    """
    from plyer import notification

    notification.notify(
        title,
        message=message,
        app_name=app_name,
        app_icon=app_icon,
        timeout=timeout,
        ticker=ticker,
        toast=toast,
    )


def run_process(
    command: str,
    n: int = 1,
    stdout: bool = False,
):
    """
    ## 多个进程运行程序
    脚本中可以使用 getpid() 获取进程号, gettid() 获取线程号, 使用 python 建议使用 sys.executable

    - command: 命令, 字符串
    - n: 并发运行的实例数量
    - stdout: 默认不输出, True 输出
    """
    import subprocess

    if stdout:
        return subprocess.getoutput(command)

    processes = []

    for _ in range(n):
        process = subprocess.Popen(
            command,
            shell=True if isinstance(command, str) else False,
        )
        processes.append(process)

    for process in processes:
        process.wait()


def run_nohup(command: str, *args, **kwargs):
    """
    后台运行命令

    - command: 命令, 字符串
    """
    import subprocess

    process = subprocess.Popen(
        command,
        shell=True if isinstance(command, str) else False,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        *args,
        **kwargs,
    )
    return process


def to_gzip(input_path, dir_format="gztar"):
    """
    ## 压缩文件或目录

    压缩目录时，需要配置 dir_format 参数。

    - input_path: 输入路径，文件路径或目录
    - dir_format: 压缩目录格式, 支持 "zip", "tar", "gztar", "bztar", or "xztar"
    """
    import os
    import gzip
    import shutil

    if os.path.isfile(input_path):
        with (
            open(input_path, "rb") as f_in,
            gzip.open(f"{input_path}.gz", "wb") as f_out,
        ):
            shutil.copyfileobj(f_in, f_out)
        print(f"文件 {input_path} 成功压缩为 {input_path}.gz")
    elif os.path.isdir(input_path):
        output_path = shutil.make_archive(
            f"{input_path.replace('/', '')}",
            dir_format,
            input_path,
        )
        print(f"目录 {input_path} 成功压缩为 {output_path}")
    else:
        print(f"提供的路径 ({input_path}) 无效.")


def jsonpath(obj, expr, result_type="VALUE", debug=0, use_eval=True):
    """
    jsonpath 解析器 https://goessner.net/articles/JsonPath/

    - obj: 目标对象
    - expr: 表达式
    - result_type: 结果类型, 支持 "VALUE", "IPATH", "PATH"
    - debug: 调试模式, 0 为关闭, 1 为开启
    - use_eval: 使用 eval 函数, 默认开启
    """
    from jsonpath import jsonpath

    return jsonpath(
        obj,
        expr,
        result_type=result_type,
        debug=debug,
        use_eval=use_eval,
    )


def run_remote(
    host,
    user,
    command: str | None = None,
    port=None,
    config=None,
    gateway=None,
    forward_agent=None,
    connect_timeout=None,
    connect_kwargs: dict | None = None,
    inline_ssh_env=None,
    hide=False,
    shell=False,
):
    """
    运行远程命令

    - host: 主机
    - user: 用户名
    - command: 命令
    - port: 端口
    - config: 配置文件
    - gateway: 网关
    - forward_agent: 转发代理
    - connect_timeout: 连接超时
    - connect_kwargs: 连接参数, 字典格式, 密码为 {"password": "password"}
    - inline_ssh_env: 内联 ssh 环境变量, 字典格式
    - hide: 隐藏命令
    - shell: 是否进入 shell, 当为 True 时, 进入 shell 模式, 不支持 command 参数
    """
    from fabric import Connection

    c = Connection(
        host,
        user=user,
        port=port,
        config=config,
        gateway=gateway,
        forward_agent=forward_agent,
        connect_timeout=connect_timeout,
        inline_ssh_env=inline_ssh_env,
        connect_kwargs=connect_kwargs,
    )
    if shell:
        return c.shell()
    if not command:
        return c
    return c.run(command, hide=hide)


def num2cn(num: int | float, precision: int = 2):
    """
    数字转中文

    - num: 数字, 必须是整数或浮点数
    - precision: 保留几位小数, 默认为 2, 必须是整数且大于等于0
    """
    if not isinstance(num, (int, float)):
        raise TypeError("必须是整数或浮点数")
    if not isinstance(precision, int) or precision < 0:
        raise Exception("精度必须是整数且大于等于0")
    int_len = len(str(int(abs(num))))
    if int_len < 5:
        if str(num) == str(int(num)):
            return f"{num}"
        return f"{num:.{precision}f}"
    else:
        if int_len < 9:
            return f"{num / 1e4:.{precision}f}万"
        elif 13 > int_len >= 9:
            return f"{num / 1e8:.{precision}f}亿"
        elif 17 > int_len >= 13:
            return f"{num / 1e12:.{precision}f}万亿"
        elif 21 > int_len >= 17:
            return f"{num / 1e16:.{precision}f}亿亿"
        else:
            return f"{num:.{precision}e}"


def check_run(run_word: str = "nostop", stop_word: None | str = None):
    """
    ## 检查当前程序是否正在运行

    当前程序运行前检测是否正在运行, 当 run_word 在 sys.argv 中时忽略检测; 当 stop_word 在 sys.argv 中时结束程序。忽略 sh -c 命令行参数。

    - run_word: 运行时的参数, 默认为 nostop
    - stop_word: 停止时的参数, 默认为 None
    """
    import os
    import sys
    import psutil
    import inspect

    if run_word in sys.argv:
        logger.info(f"检测到运行参数 '{run_word}', 忽略检测")
        return
    if stop_word and stop_word in sys.argv:
        logger.info(f"检测到停止参数 '{stop_word}', 程序退出")
        sys.exit()

    filename = inspect.stack()[1].filename.rsplit(os.sep, 1)[-1]
    username = psutil.Process().username()
    count = 0

    current_pid = os.getpid()
    for proc in psutil.process_iter(["pid", "username", "cmdline"]):
        try:
            info = proc.info
            if (
                username == info["username"]
                and info["pid"] != current_pid
                and info["cmdline"]
                and any(filename in part for part in info["cmdline"])
                and "-c" not in str(info["cmdline"])
            ):
                logger.info(
                    f"当前程序进程 {current_pid}, 检测到正在运行的进程 {info['pid']}, 命令为 {' '.join(info['cmdline'])}"
                )
                count += 1
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    if count > 0:
        logger.info("程序正在运行")
        exit()


def run_myself(word="nostop", python=None, nohup=True):
    """
    运行脚本自身

    - word: 运行时的参数, 默认为 nostop
    - python: python 路径, 默认为 sys.executable
    """
    import inspect

    if not python:
        python_path = sys.executable

    stack = inspect.stack()
    caller_frame_record = stack[1]
    caller_file = caller_frame_record.filename
    if nohup:
        run_nohup(f"{python_path} {caller_file} {word}")
    else:
        run_process(f"{python_path} {caller_file} {word}")


def cookies_str2dict(cookies_str: str, filter: list | None = None) -> dict[str, str]:
    """
    将 cookies 字符串转换为字典格式

    - cookies_str: 字符串格式的 cookies
    - filter: 过滤字段, 输出按照 filter 中的顺序; 默认为 None, 不进行过滤.
    """
    filter_list = list({i: 0 for i in filter}) if filter else None
    cookies_dict = dict(
        item.strip().split("=", 1) for item in cookies_str.strip("; ").split(";")
    )
    if filter_list:
        cookies_dict = {k: cookies_dict.get(k) for k in filter_list}
    return cookies_dict


def cookies_dict2str(cookies: dict[str, str], filter: list | None = None) -> str:
    """
    将 cookies 字典转换为字符串格式

    - cookies: 字典格式的 cookies
    - filter: 过滤字段, 输出按照 filter 中的顺序; 默认为 None, 不进行过滤.
    """
    filter_list = list({i: 0 for i in filter}) if filter else None
    if filter_list:
        cookies = {k: cookies.get(k) for k in filter_list}
    return "; ".join([f"{k}={v}" for k, v in cookies.items()]) + ";"


def aes_encrypt(
    data: str,
    key: str,
    iv: str | None = None,
    mode: str = "ECB",
) -> str:
    """
    AES 加密, ECB 模式下 iv 无效

    - data: 数据
    - key: 密钥
    - iv: 向量
    - mode: 模式, 支持 "ECB", "CBC", "CFB", "OFB", "CTR"
    """
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad
    from base64 import b64encode

    if iv:
        cipher = AES.new(
            key.encode(),
            getattr(AES, f"MODE_{mode}"),
            iv=iv.encode(),
        )
    else:
        cipher = AES.new(
            key.encode(),
            getattr(AES, f"MODE_{mode}"),
        )
    ct_bytes = cipher.encrypt(pad(data.encode(), AES.block_size))
    return b64encode(ct_bytes).decode()


def aes_decrypt(
    data: str,
    key: str,
    iv: str | None = None,
    mode: str = "ECB",
) -> str:
    """
    AES 解密, ECB 模式下 iv 无效

    - data: 数据
    - key: 密钥
    - iv: 向量
    - mode: 模式, 支持 "ECB", "CBC", "CFB", "OFB", "CTR"
    """
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import unpad
    from base64 import b64decode

    if iv:
        cipher = AES.new(
            key.encode(),
            getattr(AES, f"MODE_{mode}"),
            iv=iv.encode(),
        )
    else:
        cipher = AES.new(
            key.encode(),
            getattr(AES, f"MODE_{mode}"),
        )
    pt = unpad(cipher.decrypt(b64decode(data)), AES.block_size)
    return pt.decode()


def aes_keygen(key_size: int = 16, upper: bool = False, type: str = "hex") -> str:
    """
    AES 密钥生成, 返回密钥

    - key_size: 密钥长度, 默认为 16
    - upper: 是否大写, 默认为 False
    - type: 密钥类型, 支持 "hex", "base64"
    """
    from Crypto.Random import get_random_bytes
    from base64 import b64encode

    key = get_random_bytes(key_size)
    if type == "hex":
        new_key = key.hex()
    elif type == "base64":
        new_key = b64encode(key).decode()
    else:
        raise Exception("密钥类型错误")
    if upper:
        return new_key.upper()
    return new_key


def rsa_encrypt(data: str, key: str) -> str:
    """
    RSA 公钥加密

    - data: 数据
    - key: 公钥
    """
    from Crypto.PublicKey import RSA
    from Crypto.Cipher import PKCS1_v1_5
    from base64 import b64encode

    RsaKey = RSA.importKey(key)
    cipher = PKCS1_v1_5.new(RsaKey)
    cipher_text = b64encode(cipher.encrypt(data.encode())).decode()
    return cipher_text


def rsa_decrypt(data: str, key: str) -> str:
    """
    RSA 私钥解密

    - data: 数据
    - key: 私钥
    """
    from Crypto.PublicKey import RSA
    from Crypto.Cipher import PKCS1_v1_5
    import base64

    key = RSA.importKey(key)
    cipher = PKCS1_v1_5.new(key)
    text = cipher.decrypt(base64.b64decode(data), None)
    return text.decode()


def rsa_keygen(key_size: int = 1024) -> list[str, str]:
    """
    RSA 密钥生成, 返回公钥和私钥

    - key_size: 密钥长度, 默认为 1024
    """
    from Crypto.PublicKey import RSA

    key = RSA.generate(key_size)
    private_key = key.exportKey()
    public_key = key.publickey().exportKey()
    return [public_key.decode(), private_key.decode()]


def iter_accumulate(iterable, func=None):
    """
    累积迭代器

    iter_accumulate([1,2,3,4,5]) → 1 3 6 10 15

    iter_accumulate([1,2,3,4,5], func=lambda x, y: x * y) → 1 2 6 24 120

    - iterable: 可迭代对象
    - func: 累积函数, 默认为 operator.add
    """
    from itertools import accumulate

    return accumulate(iterable, func)


def iter_batched(iterable, n, *, strict=False):
    """
    批处理迭代器

    iter_batched('ABCDEFG', 3) → ('A', 'B', 'C') ('D', 'E', 'F') ('G',)

    - iterable: 可迭代对象
    - n: 批大小
    """
    from itertools import islice

    # batched('ABCDEFG', 3) → ABC DEF G
    if n < 1:
        raise ValueError("n must be at least one")
    iterator = iter(iterable)
    while batch := tuple(islice(iterator, n)):
        if strict and len(batch) != n:
            raise ValueError("batched(): incomplete batch")
        yield batch


def iter_combinations(iterable, r):
    """
    组合迭代器

    iter_combinations(range(4), 3) --> (0,1,2), (0,1,3), (0,2,3), (1,2,3)

    - iterable: 可迭代对象
    - r: 组合大小
    """
    from itertools import combinations

    return combinations(iterable, r)


def iter_cycle(iterable):
    """
    循坏迭代列表

    iter_cycle([1, 2, 3]) → 1 2 3 1 2 3 1 2 3 ...

    - iterable: 可迭代对象
    """
    from itertools import cycle

    return cycle(iterable)


def iter_product(*iterables, repeat=1):
    """
    笛卡尔积迭代器

    iter_product('ab', range(3)) --> ('a',0) ('a',1) ('a',2) ('b',0) ('b',1) ('b',2)

    iter_product((0,1), (0,1), (0,1)) --> (0,0,0) (0,0,1) (0,1,0) (0,1,1) (1,0,0) ...

    - iterables: 可迭代对象
    - repeat: 重复次数, 默认为 1
    """
    from itertools import product

    return product(*iterables, repeat=repeat)


def iter_permutations(iterable, r=None):
    """
    根据 iterable 返回连续的 r 长度元素的排列。

    - iterable: 可迭代对象
    - r: 如果 r 未指定或为 None ，r 默认设置为 iterable 的长度，这种情况下，生成所有全长排列。

    >>> c = iter_permutations([1, 2, 3])
    >>> next(c) # (1, 2, 3)
    """
    from itertools import permutations

    return permutations(iterable, r)


def iter_flatten(iterable):
    """
    展开列表, 支持嵌套列表

    - iterable: 可迭代对象
    """
    from itertools import chain

    return chain.from_iterable(iterable)


def iter_groupby(iterable, key=None):
    """
    分组迭代器

    - iterable: 可迭代对象, 必须是有序的
    - key: 分组键
    """
    from itertools import groupby

    return groupby(iterable, key)


def iter_zip_longest(*iterables, fillvalue=None):
    """
    并行迭代器, 生成一个元组，以最长可迭代对象为基准。

    - iterables: 可迭代对象
    - fillvalue: 填充值, 默认为 None
    """
    from itertools import zip_longest

    return zip_longest(*iterables, fillvalue=fillvalue)


def iter_count(start=0, step=1):
    """
    计数器迭代器

    - start: 起始值, 默认为 0
    - step: 步长, 默认为 1
    """
    from itertools import count

    return count(start, step)


def iter_repeat(object, times=None):
    """
    重复迭代器

    - object: 对象
    - times: 重复次数, 默认为 None
    """
    from itertools import repeat

    return repeat(object, times)


def func_kv(func) -> dict:
    """
    函数属性字典, 用于存储函数的属性, 支持嵌套字典。

    - func: 函数
    - key: 键
    - default: 默认值

    >>> def hello():
    ...    kv = func_kv(hello)
    ...    print(kv) # {}
    ...    kv["a"] = "b"
    ...    print(kv) # {"a": "b"}
    >>> hello()
    """
    func.__dict__["kv"] = func.__dict__.get("kv", {})
    kv = func.__dict__["kv"]
    return kv


def random_str(length: int = 16, prefix="", suffix="") -> str:
    """
    随机生成字符串
    - length: 长度
    - prefix: 前缀
    - suffix: 后缀
    """
    import random

    chars = "".join(
        random.sample(
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
            length,
        )
    )
    return prefix + chars + suffix


def set_title(title: str):
    """
    设置控制台标题

    - title: 标题
    """
    from builtins import print

    print(f"\x1b]0;{title}\x07")


def set_proxy(proxy: str = ""):
    """
    设置全局代理

    - proxy: 代理, 格式为 "http://host:port" 或 "https://host:port"
    """
    import os

    if proxy:
        if isinstance(proxy, dict):
            proxy = (
                proxy.get("http", "")
                or proxy.get("https", "")
                or proxy.get("all", "")
                or proxy.get("http://", "")
                or proxy.get("https://", "")
            )
        proxy = proxy.strip()
        os.environ["http_proxy"] = proxy
        os.environ["https_proxy"] = proxy


def re_first(pattern, string, default="", flags=0):
    """
    正则匹配第一个

    - pattern: 正则表达式
    - string: 字符串
    - default: 默认值
    - flags: 正则表达式标志, 也支持字符串格式, 如 "I", "A", "S", "M" 等。
    """
    import re

    if flags and isinstance(flags, str):
        flags = getattr(re, flags)
    return next(iter(re.findall(pattern, string, flags)), default)


def re_findall(pattern, string, flags=0):
    """
    正则匹配所有
    - pattern: 正则表达式
    - string: 字符串
    - flags: 正则表达式标志, 也支持字符串格式, 如 "I", "A", "S", "M" 等。
    """
    import re

    if flags and isinstance(flags, str):
        flags = getattr(re, flags)
    return re.findall(pattern, string, flags)


def jsonpath_first(obj, expr, default=""):
    """
    尝试获取字典数据, 失败返回默认值, 基于 jsonpath 实现。

    obj: 目标对象
    expr: 表达式
    default: 默认值
    """
    try:
        result = jsonpath(obj, expr)
        return result[0] if result and (result[0] or result[0] == 0) else default
    except Exception:
        return default


try_get = jsonpath_first


def moviepy_video(filepath):
    """
    通过 MoviePy 处理视频文件

    - filepath: 文件路径
    """
    from moviepy import VideoFileClip

    return VideoFileClip(filepath)


def moviepy_audio(filepath):
    """
    通过 MoviePy 处理音频文件

    - filepath: 文件路径
    """
    from moviepy import AudioFileClip

    return AudioFileClip(filepath)


def open_url(url, browser=None):
    """
    打开 URL

    - url: URL链接
    - browser: 浏览器名称或路径, 名称支持 chrome、firefox、edge.
        - chrome默认路径为 'C:/Program Files/Google/Chrome/Application/chrome.exe'，
        - firefox 浏览器默认路径为 'C:/Program Files/Mozilla Firefox/firefox.exe'
        - edge 浏览器默认路径为 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
    """
    import webbrowser
    import os

    if browser:
        if browser == "chrome":
            browser_path = "C:/Program Files/Google/Chrome/Application/chrome.exe"
        elif browser == "firefox":
            browser_path = "C:/Program Files/Mozilla Firefox/firefox.exe"
        elif browser == "edge":
            browser_path = (
                "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
            )
        else:
            browser_path = browser
        if not os.path.exists(browser_path):
            raise Exception("浏览器路径不存在，检查路径是否正确")
        webbrowser.register(
            "custom_browser", None, webbrowser.BackgroundBrowser(browser_path)
        )
        webbrowser.get("custom_browser").open(url)
    else:
        webbrowser.open(url)


def copy_text(text):
    """
    复制文本到剪贴板

    - text: 文本
    """
    import pyperclip

    pyperclip.copy(text)


def paste_text():
    """
    从剪贴板获取文本
    """
    import pyperclip

    return pyperclip.paste()


def zip_file(filepath, zipname):
    """
    将文件或文件夹压缩为 zip 格式
    - filepath: 文件路径, 可以是文件路径或文件夹路径
    - zipname: 压缩文件名
    """
    import zipfile

    with zipfile.ZipFile(zipname, "w") as f:
        if os.path.isfile(filepath):
            f.write(filepath)
        else:
            for root, dirs, files in os.walk(filepath):
                for file in files:
                    f.write(os.path.join(root, file))


def unzip_file(zipname, path=None):
    """
    解压 zip 文件
    - zipname: 压缩文件名
    - path: 解压路径, 默认为当前路径
    """
    import zipfile

    with zipfile.ZipFile(zipname, "r") as f:
        f.extractall(path=path)


def time_fmt(timestring, fmt="%Y-%m-%d %H:%M:%S", out_fmt="%Y-%m-%d %H:%M:%S"):
    """
    时间格式化
    - timestring: 时间字符串 或 datetime 对象
    - fmt: 输入时间字符串的格式, strptime 驱动
    - out_fmt: 输出时间字符串的格式, strftime 驱动
    """
    import datetime

    if isinstance(timestring, str):
        timestring = datetime.datetime.strptime(timestring, fmt)
        return timestring.strftime(out_fmt)
    elif isinstance(timestring, (datetime.datetime, datetime.date)):
        return timestring.strftime(out_fmt)
    else:
        raise timestring


def which(cmd):
    """
    查找命令的路径

    - cmd: 命令
    """
    import shutil

    return shutil.which(cmd)


def move_file(src, dst):
    """
    移动文件, 支持文件和文件夹

    - src: 源文件路径
    - dst: 目标文件路径
    """
    import shutil

    shutil.move(src, dst)


def copy_file(src, dst):
    """
    复制文件, 支持文件和文件夹

    - src: 源文件路径
    - dst: 目标文件路径
    """
    import shutil
    import os

    if os.path.isdir(src):
        shutil.copytree(src, dst)
    else:
        shutil.copy(src, dst)


def remove_file(filepath):
    """
    删除文件, 支持文件和文件夹

    - filepath: 文件路径
    """
    import shutil
    import os

    if os.path.isfile(filepath):
        os.remove(filepath)
    else:
        shutil.rmtree(filepath)


def unpack_archive(
    filepath: str,
    dst: str | None = None,
    format: str | None = None,
    filter=None,
):
    """
    解压文件, 支持 zip、tar、gz、bz2、xz 格式

    - filepath: 文件路径, 可以是文件路径或文件夹路径
    - dst: 目标路径, 可以是文件夹路径, 默认为当前路径
    - format: 压缩格式, 默认为 None, 自动识别, 支持 zip、tar、gz、bz2、xz 格式
    """
    import shutil

    shutil.unpack_archive(filepath, dst, format, filter=filter)


def make_archive(base_name, format, root_dir, **kwargs):
    """
    压缩文件, 支持 zip、tar、gz、bz2、xz 格式

    - base_name: 压缩文件名
    - format: 压缩格式
    - root_dir: 压缩文件路径
    - kwargs: 其他参数, 如 base_dir、verbose、exclude、dry_run、owner、group、logger
    """
    import shutil

    shutil.make_archive(base_name, format, root_dir, **kwargs)


def jmespath(query, data):
    """
    基于 jmespath 实现的查询
    - query: 查询表达式
    - data: 数据
    """
    import jmespath

    return jmespath.search(query, data)


def js2py_eval(js: str):
    """
    基于 js2py 实现的 js 表达式求值

    - js: js 表达式
    """
    import js2py

    return js2py.eval_js(js)


def atexit_reg(func, *args, **kwargs):
    """
    装饰器注册退出函数

    ```
    @atexit_reg
    def goodbye():
        print('欢迎下次再来。')
    ```
    """
    import atexit

    atexit.register(func, *args, **kwargs)


def atexit_msg(text):
    """
    退出打印文字
    """
    import atexit

    atexit.register(print, text)


def bson_encode(data: dict[str]) -> bytes:
    """
    将数据编码为 BSON 格式
    """
    import bson

    return bson.encode(data)


def bson_decode(data) -> dict[str]:
    """
    将 BSON 格式的数据解码为 Python 数据类型
    """
    import bson

    if not isinstance(data, bytes):
        data = data.encode()

    return bson.decode(data)


def image_to_base64(image_path, add_prefix=True):
    """
    将图片转换为 base64 编码

    - image_path: 图片路径
    - add_prefix: 是否添加前缀, 默认为 True
    """
    import base64

    with open(image_path, "rb") as f:
        image_data = f.read()
        base64_data = base64.b64encode(image_data).decode()

        if add_prefix:
            base64_data = "data:image/png;base64," + base64_data
        return base64_data


def image_from_base64(base64_data, image_path):
    """
    将 base64 编码的图片转换为图片
    """
    import base64

    if not isinstance(base64_data, bytes):
        base64_data = base64_data.encode()

    if base64_data.startswith(b"data:image/png;base64,"):
        base64_data = base64_data.removeprefix(b"data:image/png;base64,")

    with open(image_path, "wb") as f:
        f.write(base64.b64decode(base64_data))
        return image_path


def defaultdict(default_factory=None):
    """
    字典默认值, 支持嵌套字典

    - default_factory: 默认值, 默认为 None
    """
    from collections import defaultdict

    return defaultdict(default_factory)


def dataclass(
    cls=None,
    /,
    *,
    init=True,
    repr=True,
    eq=True,
    order=False,
    unsafe_hash=False,
    frozen=False,
    match_args=True,
    kw_only=False,
    slots=False,
    weakref_slot=False,
):
    """
    数据类装饰器

    - cls: 类
    - init: 初始化, 默认为 True
    - repr: 打印, 默认为 True
    - eq: 相等, 默认为 True
    - order: 排序, 默认为 False
    - unsafe_hash: 哈希, 默认为 False
    - frozen: 冻结, 默认为 False
    - match_args: 匹配参数, 默认为 True
    - kw_only: 关键字参数, 默认为 False
    - slots: 插槽, 默认为 False
    - weakref_slot: 弱引用插槽, 默认为 False
    """
    from dataclasses import dataclass

    return dataclass(
        cls,
        init=init,
        repr=repr,
        eq=eq,
        order=order,
        unsafe_hash=unsafe_hash,
        frozen=frozen,
        match_args=match_args,
        kw_only=kw_only,
        slots=slots,
        weakref_slot=weakref_slot,
    )


def send_mail(
    user: str | None = None,
    password: str | None = None,
    host: str = "smtp.gmail.com",
    port: int | None = None,
    smtp_starttls: bool | None = None,
    smtp_ssl: bool = True,
    smtp_set_debuglevel: int = 0,
    smtp_skip_login: bool = False,
    encoding: str = "utf-8",
    oauth2_file: str | None = None,
    soft_email_validation: bool = True,
    dkim: None = None,
    **kwargs,
):
    """
    ## 发送邮件

    ```python
    yag = send_mail()
    yag.send(to="对方邮箱", subject="主题", contents="文字内容")
    yag.send(
        to=["对方邮箱1", "对方邮箱2"],
        subject="主题",
        contents=["文字内容", '附件路径'],
    )
    ```

    - user: 邮箱账号
    - password: 邮箱密码
    - host: 邮箱主机, 默认为 smtp.gmail.com
    - port: 邮箱端口, 默认为 None, 自动识别
    - smtp_starttls: 是否开启 TLS, 默认为 None, 自动识别
    - smtp_ssl: 是否开启 SSL, 默认为 True
    - smtp_set_debuglevel: 调试等级, 默认为 0
    - smtp_skip_login: 是否跳过登录, 默认为 False
    - encoding: 编码, 默认为 utf-8
    - oauth2_file: oauth2 文件路径, 默认为 None
    - soft_email_validation: 软邮箱验证, 默认为 True
    - dkim: dkim 签名, 默认为 None
    """
    import yagmail

    yag = yagmail.SMTP(
        user=user,
        password=password,
        host=host,
        port=port,
        smtp_starttls=smtp_starttls,
        smtp_ssl=smtp_ssl,
        smtp_set_debuglevel=smtp_set_debuglevel,
        smtp_skip_login=smtp_skip_login,
        encoding=encoding,
        oauth2_file=oauth2_file,
        soft_email_validation=soft_email_validation,
        dkim=dkim,
        **kwargs,
    )

    return yag


def tempdir():
    """
    临时文件夹
    """
    import tempfile

    return tempfile.gettempdir()


def uuid4():
    """
    生成 uuid4
    """
    import uuid

    return uuid.uuid4()


def unique_list(list_data: list[dict], *keys):
    """
    列表去重, 保留最后一个

    list_data: list[dict]
    keys: 去重键
    """
    if not keys:
        keys = list_data[0].keys()
    return list(
        ({"".join([str(i.get(k, "")) for k in keys]): i for i in list_data}).values()
    )


def print_json(obj, indent: int = 4):
    from rich import print_json as rich_print_json

    if not isinstance(obj, str):
        obj = to_json(obj)
    rich_print_json(obj, indent=indent)


def track(
    sequence,
    description="正在处理...",
    total=None,
    auto_refresh=True,
    console=None,
    transient=False,
    get_time=None,
    refresh_per_second=10,
    style="bar.back",
    complete_style="bar.complete",
    finished_style="bar.finished",
    pulse_style="bar.pulse",
    update_period=0.1,
    disable=False,
    show_speed=True,
):
    """
    进度条, 封装 rich 的 track

    - sequence: 序列
    - description: 描述
    - total: 总数
    - auto_refresh: 是否自动刷新
    - console: 控制台
    - transient: 是否瞬态
    - get_time: 获取时间
    - refresh_per_second: 每秒刷新次数
    - style: 样式
    - complete_style: 完成样式
    - finished_style: 完成样式
    - pulse_style: 脉冲样式
    - update_period: 更新周期
    - disable: 是否禁用
    - show_speed: 是否显示速度

    >>> import jsz
    >>> for i in jsz.track(range(10)):
    >>>     jsz.print(i)
    >>>     jsz.sleep(0.2)
    """
    from rich.progress import track

    return track(
        sequence=sequence,
        description=description,
        total=total,
        auto_refresh=auto_refresh,
        console=console,
        transient=transient,
        get_time=get_time,
        refresh_per_second=refresh_per_second,
        style=style,
        complete_style=complete_style,
        finished_style=finished_style,
        pulse_style=pulse_style,
        update_period=update_period,
        disable=disable,
        show_speed=show_speed,
    )


ANSI_COLOR_NAMES = [
    # "black",
    "red",
    "green",
    "yellow",
    "blue",
    "magenta",
    "cyan",
    "white",
    "bright_black",
    "bright_red",
    "bright_green",
    "bright_yellow",
    "bright_blue",
    "bright_magenta",
    "bright_cyan",
    "bright_white",
    # "grey0",
    # "gray0",
    # "navy_blue",
    # "dark_blue",
    "blue3",
    "blue1",
    "dark_green",
    "deep_sky_blue4",
    "dodger_blue3",
    "dodger_blue2",
    "green4",
    "spring_green4",
    "turquoise4",
    "deep_sky_blue3",
    "dodger_blue1",
    "green3",
    "spring_green3",
    "dark_cyan",
    "light_sea_green",
    "deep_sky_blue2",
    "deep_sky_blue1",
    "spring_green2",
    "cyan3",
    "dark_turquoise",
    "turquoise2",
    "green1",
    "spring_green1",
    "medium_spring_green",
    "cyan2",
    "cyan1",
    "dark_red",
    "deep_pink4",
    "purple4",
    "purple3",
    "blue_violet",
    "orange4",
    "grey37",
    "gray37",
    "medium_purple4",
    "slate_blue3",
    "royal_blue1",
    "chartreuse4",
    "dark_sea_green4",
    "pale_turquoise4",
    "steel_blue",
    "steel_blue3",
    "cornflower_blue",
    "chartreuse3",
    "cadet_blue",
    "sky_blue3",
    "steel_blue1",
    "pale_green3",
    "sea_green3",
    "aquamarine3",
    "medium_turquoise",
    "chartreuse2",
    "sea_green2",
    "sea_green1",
    "aquamarine1",
    "dark_slate_gray2",
    "dark_magenta",
    "dark_violet",
    "purple",
    "light_pink4",
    "plum4",
    "medium_purple3",
    "slate_blue1",
    "yellow4",
    "wheat4",
    "grey53",
    "gray53",
    "light_slate_grey",
    "light_slate_gray",
    "medium_purple",
    "light_slate_blue",
    "dark_olive_green3",
    "dark_sea_green",
    "light_sky_blue3",
    "sky_blue2",
    "dark_sea_green3",
    "dark_slate_gray3",
    "sky_blue1",
    "chartreuse1",
    "light_green",
    "pale_green1",
    "dark_slate_gray1",
    "red3",
    "medium_violet_red",
    "magenta3",
    "dark_orange3",
    "indian_red",
    "hot_pink3",
    "medium_orchid3",
    "medium_orchid",
    "medium_purple2",
    "dark_goldenrod",
    "light_salmon3",
    "rosy_brown",
    "grey63",
    "gray63",
    "medium_purple1",
    "gold3",
    "dark_khaki",
    "navajo_white3",
    "grey69",
    "gray69",
    "light_steel_blue3",
    "light_steel_blue",
    "yellow3",
    "dark_sea_green2",
    "light_cyan3",
    "light_sky_blue1",
    "green_yellow",
    "dark_olive_green2",
    "dark_sea_green1",
    "pale_turquoise1",
    "deep_pink3",
    "magenta2",
    "hot_pink2",
    "orchid",
    "medium_orchid1",
    "orange3",
    "light_pink3",
    "pink3",
    "plum3",
    "violet",
    "light_goldenrod3",
    "tan",
    "misty_rose3",
    "thistle3",
    "plum2",
    "khaki3",
    "light_goldenrod2",
    "light_yellow3",
    "grey84",
    "gray84",
    "light_steel_blue1",
    "yellow2",
    "dark_olive_green1",
    "honeydew2",
    "light_cyan1",
    "red1",
    "deep_pink2",
    "deep_pink1",
    "magenta1",
    "orange_red1",
    "indian_red1",
    "hot_pink",
    "dark_orange",
    "salmon1",
    "light_coral",
    "pale_violet_red1",
    "orchid2",
    "orchid1",
    "orange1",
    "sandy_brown",
    "light_salmon1",
    "light_pink1",
    "pink1",
    "plum1",
    "gold1",
    "navajo_white1",
    "misty_rose1",
    "thistle1",
    "yellow1",
    "light_goldenrod1",
    "khaki1",
    "wheat1",
    "cornsilk1",
    "grey100",
    "gray100",
    # "grey3",
    # "gray3",
    # "grey7",
    # "gray7",
    # "grey11",
    # "gray11",
    # "grey15",
    # "gray15",
    # "grey19",
    # "gray19",
    # "grey23",
    # "gray23",
    # "grey27",
    # "gray27",
    # "grey30",
    # "gray30",
    # "grey35",
    # "gray35",
    "grey39",
    "gray39",
    "grey42",
    "gray42",
    "grey46",
    "gray46",
    "grey50",
    "gray50",
    "grey54",
    "gray54",
    "grey58",
    "gray58",
    "grey62",
    "gray62",
    "grey66",
    "gray66",
    "grey70",
    "gray70",
    "grey74",
    "gray74",
    "grey78",
    "gray78",
    "grey82",
    "gray82",
    "grey85",
    "gray85",
    "grey89",
    "gray89",
    "grey93",
    "gray93",
]


def printx(
    *objects,
    sep: str = " ",
    end: str = "\n",
    file=None,
    flush=True,
    style: str | None = None,
    justify: str | None = None,
    overflow: str | None = None,
    no_wrap: bool | None = None,
    emoji: bool | None = None,
    markup: bool | None = None,
    highlight: bool | None = None,
    width: int | None = None,
    height: int | None = None,
    crop: bool = True,
    soft_wrap: bool | None = None,
    new_line_start: bool = False,
):
    """
    增加随机颜色的print, 支持富文本


    - sep: 分隔符
    - end: 结尾
    - file: 文件
    - flush: 是否刷新, 默认是,不支持修改
    - style: 样式
    - justify: 对齐方式, 支持 "left", "center", "right", 默认是 None
    - overflow: 溢出方式, 支持 "fold", "crop", "ellipsis", 默认是 None
    - no_wrap: 是否不换行
    - emoji: 是否支持 emoji
    - markup: 是否支持 markup
    - highlight: 是否支持 highlight
    - width: 宽度
    - height: 高度
    - crop: 是否裁剪
    - soft_wrap: 是否软换行
    - new_line_start: 是否换行开始
    """
    from rich.console import Console

    console = Console(file=file) if file else Console()
    console.print(
        *(
            [
                (
                    f"[{random.choice(ANSI_COLOR_NAMES)}]{i}[/]"
                    if isinstance(i, str)
                    else i
                )
                for i in objects
            ]
            if not style
            else objects
        ),
        sep=sep,
        end=end,
        style=style,
        justify=justify,
        overflow=overflow,
        no_wrap=no_wrap,
        emoji=emoji,
        markup=markup,
        highlight=highlight,
        width=width,
        height=height,
        crop=crop,
        soft_wrap=soft_wrap,
        new_line_start=new_line_start,
    )


def rich_panel(
    renderable,
    title=None,
    title_align="center",
    subtitle=None,
    subtitle_align="center",
    safe_box=None,
    expand=False,
    style="none",
    border_style="none",
    width=None,
    height=None,
    padding=(0, 1),
    highlight=False,
):
    """
    rich panel 封装

    - renderable: 渲染对象
    - title: 标题
    - title_align: 标题对齐方式
    - subtitle: 副标题
    - safe_box: 安全盒子
    - expand: 是否展开, 默认不展开
    - style: 样式
    - border_style: 边框样式
    - width: 宽度
    - height: 高度
    - padding: 内边距
    - highlight: 是否高亮
    """
    from rich.panel import Panel

    return Panel(
        renderable=renderable,
        title=title,
        title_align=title_align,
        subtitle=subtitle,
        subtitle_align=subtitle_align,
        safe_box=safe_box,
        expand=expand,
        style=style,
        border_style=border_style,
        width=width,
        height=height,
        padding=padding,
        highlight=highlight,
    )


def rich_rule(
    title="",
    *,
    characters: str = "─",
    style: str = "rule.line",
    end: str = "\n",
    align: str = "center",
) -> None:
    """
    绘制一条线，可选择带有居中文本的标题。

    - title (str, 可选): 在线条上方渲染的文本。默认为 ""。
    - characters (str, 可选): 用于组成线条的字符。默认为 "─"。
    - style (str, 可选): 线条样式。默认为 "rule.line"。
    - align (str, 可选): 标题的对齐方式，可为 "left"、"center" 或 "right"。默认为 "center"。
    """
    from rich.rule import Rule
    from rich import print

    print(
        Rule(
            title=title,
            characters=characters,
            style=style,
            end=end,
            align=align,
        )
    )


def status(
    status="正在处理...",
    *,
    spinner: str = "dots",
    spinner_style="status.spinner",
    speed: float = 1.0,
    refresh_per_second: float = 12.5,
):
    """
    ## 显示进度，动画效果，基于 rich status 封装

    - status: 标题
    - spinner: 动画类型, random 为随机选择, 其他好看的样式有 "aesthetic", "weather", "moon", "earth", "clock", "bouncingBar", "line", "growHorizontal", "arrow2" 等。
    - spinner_style: 动画样式
    - speed: 动画速度
    - refresh_per_second: 每秒刷新次数, 默认为 12.5

    ```py
    import jsz

    with jsz.status():
        jsz.sleep(3)
        jsz.print("下载完成")
    ```
    """
    from rich.status import Status

    if spinner == "random":
        import random
        from rich.spinner import SPINNERS

        spinner = random.choice(list(SPINNERS))

    status_renderable = Status(
        status,
        spinner=spinner,
        spinner_style=spinner_style,
        speed=speed,
        refresh_per_second=refresh_per_second,
    )
    return status_renderable


def rich_padding(
    renderable,
    pad=(0, 0, 0, 0),
    style="none",
    expand=True,
):
    """
    给renderable对象添加padding

    - renderable: 渲染对象
    - pad: padding 大小
    - style: padding 样式
    - expand: 是否展开
    """
    from rich.padding import Padding

    return Padding(
        renderable,
        pad=pad,
        style=style,
        expand=expand,
    )


def rich_columns(
    renderables=None,
    padding=(0, 1),
    width=None,
    expand=False,
    equal=False,
    column_first=False,
    right_to_left=False,
    align=None,
    title=None,
):
    """
    rich 自定义列

    - renderables: 渲染对象
    - padding: padding 大小
    - width: 列宽
    - expand: 是否展开
    - equal: 是否相等
    - column_first: 列优先
    - right_to_left: 从右到左
    - align: 对齐方式
    - title: 标题
    """
    from rich.columns import Columns

    return Columns(
        renderables=renderables,
        padding=padding,
        width=width,
        expand=expand,
        equal=equal,
        column_first=column_first,
        right_to_left=right_to_left,
        align=align,
        title=title,
    )


def rich_markdown(
    markup,
    code_theme="monokai",
    justify=None,
    style="none",
    hyperlinks=True,
    inline_code_lexer=None,
    inline_code_theme=None,
):
    """
    显示 Markdown 文本

    - markup: Markdown 文本
    - code_theme: 代码块主题
    - justify: 文本对齐方式
    - style: 样式
    - hyperlinks: 是否启用超链接
    - inline_code_lexer: 内联代码词法分析器
    - inline_code_theme: 内联代码主题
    """
    from rich.markdown import Markdown

    return Markdown(
        markup=markup,
        code_theme=code_theme,
        justify=justify,
        style=style,
        hyperlinks=hyperlinks,
        inline_code_lexer=inline_code_lexer,
        inline_code_theme=inline_code_theme,
    )


def pprint(
    obj,
    console=None,
    indent_guides: bool = True,
    max_length: int | None = None,
    max_string: int | None = None,
    max_depth: int | None = None,
    expand_all: bool = False,
):
    """
    rich 美化打印对象

    - obj: 打印对象
    - console: 控制台
    - indent_guides: 是否启用缩进指南
    - max_length: 最大长度
    - max_string: 最大字符串
    - max_depth: 最大深度
    - expand_all: 是否展开所有对象
    """
    from rich.pretty import pprint

    pprint(
        obj,
        console=console,
        indent_guides=indent_guides,
        max_length=max_length,
        max_string=max_string,
        max_depth=max_depth,
        expand_all=expand_all,
    )


def duckdb(db: str = ":memory:", **kwargs):
    """
    连接 DuckDB 数据库

    - db: 数据库路径, 默认为内存连接
    - read_only: 是否只读, 默认值为 False
    - config: 配置
    """
    import duckdb

    return duckdb.connect(db, **kwargs)


def diskcache(directory: str | None = None, timeout: int = 60):
    """
    连接 diskcache 实现缓存

    - directory: 缓存目录, 默认值为 temp 目录
    - timeout: SQLite 连接超时时间, 默认值为60秒
    """
    from diskcache import Cache

    return Cache(directory, timeout)


def diskcache_deque(iterable=(), directory=None, maxlen=None):
    """
    通过 diskcache 实现 deque 序列化

    - iterable: 可迭代对象, 默认为空
    - directory: 缓存目录, 默认值为 temp 目录
    - maxlen: 最大长度, 默认值为 None
    """
    from diskcache import Deque

    return Deque(iterable, directory=directory, maxlen=maxlen)


def oss2_bucket(
    endpoint: str,
    bucket_name: str,
    access_key_id: str,
    access_key_secret: str,
    proxies: dict | None = None,
):
    """
    oss2 连接 bucket

    - endpoint: oss 地址
    - bucket_name: oss bucket 名称
    - access_key_id: oss 访问密钥
    - access_key_secret: oss 访问密钥
    - proxies: 代理
    """
    import oss2

    auth = oss2.Auth(access_key_id, access_key_secret)
    bucket = oss2.Bucket(auth, endpoint, bucket_name, proxies=proxies)
    return bucket


def oss2_put_object_acl(
    bucket,
    key,
    content,
    acl: str = "default",
):
    """
    上传 content 到 oss, 并设置 acl

    - bucket: oss2.Bucket 对象
    - key: 上传到OSS的文件名
    - content: 本地内容content
    - acl: 权限, 支持 default, public-read, public-read-write, private
    """
    import oss2

    if acl == "default":
        permission = oss2.OBJECT_ACL_DEFAULT
    if acl == "public-read":
        permission = oss2.OBJECT_ACL_PUBLIC_READ
    if acl == "public-read-write":
        permission = oss2.OBJECT_ACL_PUBLIC_READ_WRITE
    if acl == "private":
        permission = oss2.OBJECT_ACL_PRIVATE

    if not isinstance(bucket, oss2.Bucket):
        print("bucket 不符合要求")
        return
    try:
        z = bucket.put_object(key, content, headers={"x-oss-object-acl": permission})
        logger.success(f"文件上传成功, 文件路径: {key}")
        return z
    except Exception as e:
        logger.error(f"oss2_put_object_acl 发生错误: {e}")


def oss2_set_object_acl(
    bucket,
    key,
    acl: str = "default",
):
    """
    上传 content 到 oss, 并设置 acl

    - bucket: oss2.Bucket 对象
    - key: 上传到OSS的文件名
    - acl: 权限, 支持 default, public-read, public-read-write, private
    """
    import oss2

    if acl == "default":
        permission = oss2.OBJECT_ACL_DEFAULT
    if acl == "public-read":
        permission = oss2.OBJECT_ACL_PUBLIC_READ
    if acl == "public-read-write":
        permission = oss2.OBJECT_ACL_PUBLIC_READ_WRITE
    if acl == "private":
        permission = oss2.OBJECT_ACL_PRIVATE

    if not isinstance(bucket, oss2.Bucket):
        print("bucket 不符合要求")
        return
    try:
        z = bucket.put_object_acl(key, permission=permission)
        logger.success(f"{key} 权限设置为 {acl}")
        return z
    except Exception as e:
        logger.error(f"oss2_put_object_acl 发生错误: {e}")


def oss2_upload_file(bucket, key, filepath, proxies: dict | None = None):
    """
    上传文件到 oss

    - bucket: oss2.Bucket 对象
    - key: 上传到OSS的文件名
    - filepath: 本地文件路径
    - proxies: 代理, dict
    """
    import oss2

    if not isinstance(bucket, oss2.Bucket):
        print("bucket 不符合要求")
        return
    if proxies:
        bucket.proxies = proxies
    try:
        z = bucket.put_object_from_file(key, filepath)
        logger.success(f"文件 {filepath} 上传成功, 文件路径: {key}")
        return z
    except Exception as e:
        logger.error(f"oss2_upload_file 发生错误: {e}")
        return


def oss2_upload_content(bucket, key, content, proxies: dict | None = None):
    """
    上传 content 到 oss

    - bucket: oss2.Bucket 对象
    - key: 上传到OSS的文件名
    - content: 本地内容content
    - proxies: 代理
    """
    import oss2

    if not isinstance(bucket, oss2.Bucket):
        print("bucket 不符合要求")
        return
    if proxies:
        bucket.proxies = proxies
    try:
        z = bucket.put_object(key, content)
        logger.success(f"文件上传成功, 文件路径: {key}")
        return z
    except Exception as e:
        logger.error(f"oss2_upload_content 发生错误: {e}")
        return


def oss2_sign_url(
    bucket,
    key,
    method="GET",
    expires: int = 300,
    headers: None = None,
    params: None = None,
    slash_safe: bool = False,
    additional_headers: None = None,
):
    """
    oss 生成签名的 url

    - bucket: oss2.Bucket 对象
    - key: 上传到OSS的文件名
    - method: GET, PUT, POST, DELETE, HEAD
    - expires: 过期时间, 单位为秒, 默认 300 秒
    - headers: 请求头, 字典
    - params: 请求参数, 字典
    - slash_safe: 是否安全, 默认 False
    - additional_headers: 附加请求头, 字典
    """
    import oss2

    if not isinstance(bucket, oss2.Bucket):
        print("bucket 不符合要求")
        return

    try:
        url = bucket.sign_url(
            method,
            key,
            expires=expires,
            headers=headers,
            params=params,
            slash_safe=slash_safe,
            additional_headers=additional_headers,
        )
        logger.success(f"文件签名成功, url为: {url}")
        return url
    except Exception as e:
        logger.error(f"oss2_sign_url 发生错误: {e}")


def oss2_find_file(bucket, prefix: str, size1: int = 0, size2: int | None = None):
    """
    寻找 oss 文件, 指定 size1 和 size2 可以筛选特定大小的文件。默认单位为 KB。返回值为字典。

    可以通过 len 统计文件数量。可以通过 sum 统计返回值字典的 values 统计文件夹文件的总大小, 单位为KB。

    - bucket: oss2.Bucket 对象
    - prefix: 文件夹路径, 例如 'etl/atk'
    - size1: 文件大小下限, 单位为 KB, 默认0 KB
    - size2: 文件大小上限, 单位为 KB, 默认 None 表示不限制
    """
    import oss2

    if not isinstance(bucket, oss2.Bucket):
        print("bucket 不符合要求")
        return
    d = {}
    for obj in oss2.ObjectIterator(bucket, prefix):
        t = obj.size / 1024
        if size2 is None:
            if size1 < t and not obj.is_prefix():
                print(f"文件名:{obj.key}  文件大小: {t:.2f} KB")
                d[obj.key] = t
        else:
            if size1 < t < size2 and not obj.is_prefix():
                print(f"文件名:{obj.key}  文件大小: {t:.2f} KB")
                d[obj.key] = t
    return d


def redisdb(
    host="localhost",
    port=6379,
    db=0,
    username=None,
    password=None,
    decode_responses=True,
    url=None,
    async_mode=False,
    **kwargs,
):
    """
    ## 连接 redis 数据库

    - host: redis 主机
    - port: redis 端口
    - db: redis 数据库
    - password: redis 密码
    - decode_responses: 是否解码响应, 默认 True
    - url: 链接, 优先级高于 host, port, db, username, password
    - async_mode: 是否异步, 默认 False

    #### url example

    - redis://[[username]:[password]]@localhost:6379/0
    - rediss://[[username]:[password]]@localhost:6379/0
    - unix://[username@]/path/to/socket.sock?db=0[&password=password]


    """
    import redis

    if url:
        if async_mode:
            return redis.asyncio.from_url(
                url, decode_responses=decode_responses, **kwargs
            )
        else:
            return redis.Redis.from_url(
                url, decode_responses=decode_responses, **kwargs
            )

    if async_mode:
        return redis.asyncio.Redis(
            host=host,
            port=port,
            db=db,
            username=username,
            password=password,
            decode_responses=decode_responses,
            **kwargs,
        )
    else:
        return redis.Redis(
            host=host,
            port=port,
            db=db,
            username=username,
            password=password,
            decode_responses=decode_responses,
            **kwargs,
        )


@retry(log=True, show_error=True)
def mongo(host, database=None, port: int | None = None, async_mode=False, **kwargs):
    """
    连接 MongoDB, 有 database 时返回指定 Database, 否则返回 Client。

    - host: mongo 链接
    - database: 数据库名称
    - port: mongo 端口
    - async_mode: 是否异步, 默认 False, 异步为 True
    - kwargs: 其他参数, 例如 username, password

    host 有密码格式: "mongodb://username:password@192.168.0.1:27017/"
    host 无密码格式: "mongodb://192.168.0.1:27017/"
    """
    if not async_mode:
        from pymongo import MongoClient

        try:
            client = MongoClient(host, port, **kwargs)
            if database:
                db = client[database]
                db.list_collection_names()
                logger.success(f"MongoDB 成功连接到 {database}")
                return db
            else:
                client.list_database_names()
                logger.success(f"MongoDB 成功连接到 {host}")
                return client
        except Exception as e:
            logger.error("MongoDB 连接失败:", str(e))
            raise e
    else:
        from pymongo import AsyncMongoClient

        try:
            client = AsyncMongoClient(host, port, **kwargs)
            if database:
                db = client[database]
                # db.list_collection_names()
                logger.success(f"MongoDB 异步连接到 {database}")
                return db
            else:
                # client.list_database_names()
                logger.success(f"MongoDB 异步连接到 {host}")
                return client
        except Exception as e:
            logger.error("MongoDB 连接失败:", str(e))
            raise e


mongodb = mongo


def mongo_sample(
    table,
    match: dict = {},
    *,
    size: int = 1000,
    excel: bool = False,
) -> list:
    """
    mongodb 随机样本抽样

    - table: mongo 表(集合), Collection 对象
    - match: 匹配条件，默认不筛选
    - size: 随机样本数量
    - excel: 是否导出 excel文件, 默认 False
    """
    results = list(
        table.aggregate(
            [
                {"$match": match},
                {"$sample": {"size": size}},
            ]
        )
    )
    if excel:
        table_name = table.name
        filename = f"{now(7)}_{table_name}_sample_{size}.xlsx"
        to_excel(df(results), filename)

    return results


def mongo_tongji(
    mongodb,
    prefix: str = "",
    tongji_table=None,
) -> dict:
    """
    统计 mongodb 每个集合的`文档数量`

    - mongodb: 要统计的 mongo 库
    - prefix: mongo 集合前缀, 默认空字符串可以获取所有集合, 字段名称例如 `统计_20240101`。
    - tongji_table: 统计表, 必须为 Collection 对象, 默认为 None 使用 mongodb["tongji"] 集合
    """
    from pymongo import collection

    if tongji_table is None:
        tongji = mongodb["tongji"]
    else:
        if not isinstance(tongji_table, collection.Collection):
            print("tongji_table 不符合要求, 必须为 Collection 对象")
            return {}
        tongji = tongji_table

    key = prefix if prefix else f"统计_{now(7)}"
    collection_count_dict = {
        **(
            tongji.find_one({"key": key}).get("count")
            if tongji.find_one({"key": key})
            else {}
        ),
        **(
            {
                i: mongodb[i].estimated_document_count()
                for i in mongodb.list_collection_names()
                if i.startswith(prefix)
            }
        ),
    }
    tongji.update_one(
        {"key": prefix if prefix else f"统计_{now(7)}"},
        {"$set": {"count": collection_count_dict}},
        upsert=True,
    )
    return dict(sorted(collection_count_dict.items()))


def mongo_distinct(table, *fields):
    """
    mongo distinct 去重

    - table: mongo 表(集合), Collection 对象
    - fields: 字段名称，支持多个字段
    """
    pipeline = [{"$group": {"_id": {i: f"${i}" for i in fields}}}]
    agg_results = table.aggregate(pipeline)
    results = [i["_id"] for i in agg_results]
    return results


def mongo_id(oid: str | bytes | None = None):
    """
    Mongo 生成 id

    - oid: ObjectId, 默认为 None
    """
    from bson import ObjectId

    return ObjectId(oid)


def mongo_insert(
    table,
    data: list[dict] | dict,
    ordered=False,
    bypass_document_validation=False,
    session=None,
    comment=None,
    info="",
):
    """
    mongo 批量插入

    - table: mongo 表(集合), Collection 对象
    - data: 数据列表，每个元素为一个字典
    - ordered: 是否按顺序插入，默认为False。如果为True，按顺序插入数据，遇到错误时停止插入。
    - bypass_document_validation: 是否绕过文档验证，默认为False
    - session: 事务会话
    - comment: 注释
    - info: 日志追加打印信息
    """
    if info:
        info = f" | {info}"
    if not data:
        logger.warning(f"{table.name} 新增 0 条(共 0 条){info}")
        return
    try:
        if isinstance(data, dict):
            data = [data]
        results = table.insert_many(
            data,
            ordered=ordered,
            bypass_document_validation=bypass_document_validation,
            session=session,
            comment=comment,
        )
        logger.success(
            f"{table.name} 新增{len(results.inserted_ids)}(获取到{len(data)}){info}"
        )
        return results
    except Exception as e:
        logger.success(
            f"{table.name} 新增{e.details.get('nInserted')}(获取到{len(data)}){info}"
        )


def mongo_to_csv(table, output_file: str | None = None, batch_size: int = 1000):
    """
    从MongoDB集合中导出数据到CSV文件

    - table: MongoDB的集合名称或Collection对象
    - output_file: 导出的CSV文件路径, 默认值为`表名.csv`
    - batch_size: 批处理大小, 默认为1000
    """
    from pymongo import collection
    import csv

    if not isinstance(table, collection.Collection):
        raise ValueError("table 必须是 MongoDB Collection对象")

    table_name = table.name

    if not output_file:
        output_file = f"{table_name}.csv"

    try:
        count_documents = table.estimated_document_count()
        logger.info(f"开始导出 {table_name}, 总数据量 {count_documents}")
        with open(output_file, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f, quoting=1)
            cursor = table.find().batch_size(batch_size)
            header = table.find_one().keys() if table.find_one() else []
            writer.writerow(header)
            for doc in cursor:
                record = []
                for key in header:
                    if key not in doc:
                        record.append("")
                    else:
                        record.append(str(doc[key]))
                writer.writerow(record)
        logger.success(f"{table_name} 导出成功, 文件路径: {output_file}")
    except Exception as e:
        logger.error(f"{table_name} 导出发生错误: {e}")


def mongo_to_jsonl(table, output_file: str | None = None, batch_size: int = 1000):
    """
    mongo 导出 jsonl

    - table: mongo 表(集合), Collection 对象
    - output_file: 导出的 jsonl 文件名, 默认值为`表名.jsonl`
    - batch_size: 批处理大小, 默认为1000
    """
    from pymongo import collection
    import json

    if not isinstance(table, collection.Collection):
        raise ValueError("table 必须是 MongoDB Collection对象")

    table_name = table.name

    if not output_file:
        output_file = f"{table_name}.jsonl"
    try:
        count_documents = table.estimated_document_count()
        logger.info(f"开始导出 {table_name}, 总数据量 {count_documents}")
        with open(output_file, "w", encoding="utf-8") as f:
            cursor = table.find().batch_size(batch_size)
            for doc in cursor:
                doc["_id"] = str(doc["_id"])
                f.write(json.dumps(doc, ensure_ascii=False) + "\n")
        logger.success(f"{table_name} 导出成功, 文件路径: {output_file}")
    except Exception as e:
        logger.error(f"{table_name} 导出发生错误: {e}")


def mongo_from_jsonl(jsonl_file: str, table, batch_size: int = 1000):
    """
    从 JSONL 文件导入数据到 MongoDB 集合

    - jsonl_file: JSONL 文件路径
    - table: MongoDB 集合，Collection 对象
    - batch_size: 批处理大小，默认为 1000
    """
    from pymongo import collection
    import json

    if not isinstance(table, collection.Collection):
        raise ValueError("table 必须是 MongoDB Collection 对象")

    table_name = table.name

    try:
        logger.info(f"开始导入 {jsonl_file} 到集合 {table_name}")
        with open(jsonl_file, "r", encoding="utf-8") as f:
            batch = []
            for line in f:
                doc = json.loads(line)
                if "_id" in doc and isinstance(doc["_id"], str):
                    try:
                        doc["_id"] = mongo_id(doc["_id"])
                    except Exception:
                        pass
                batch.append(doc)
                if len(batch) >= batch_size:
                    mongo_insert(table, batch)
                    batch = []
            if batch:
                table.insert_many(batch)

        logger.info(f"导入完成，文件 {jsonl_file} 已成功导入到集合 {table_name}")
    except Exception as e:
        logger.error(f"导入发生错误: {e}")


def mysql(
    host="localhost",
    user="root",
    password="",
    database="",
    query="SELECT now()",
):
    """
    连接MySQL，查询返回结果

    - host: MySQL 主机
    - user: MySQL 用户名
    - password: MySQL 密码
    - database: MySQL 数据库名
    - query: 查询语句
    """
    import pymysql

    try:
        # 连接数据库
        connection = pymysql.connect(
            host=host,
            user=user,
            password=password,
            database=database,
        )
        cursor = connection.cursor()
        cursor.execute(query)
        results = cursor.fetchall()
        cursor.close()
        connection.close()
        return results
    except Exception as e:
        print(f"MySQL读取失败: {e}")
        return None


def mysql_client(
    host,
    user,
    password,
    database,
    port=3306,
    charset="utf8mb4",
    cursorclass="Cursor",
):
    """
    创建 MySQL 客户端实例

    - host: MySQL 主机
    - user: MySQL 用户名
    - password: MySQL 密码
    - database: MySQL 数据库名
    - port: MySQL 端口，默认 3306
    - charset: 字符集，默认 utf8mb4
    - cursorclass: 游标类，默认 "Cursor", 可选 "DictCursor", "Cursor", "SSCursor"
    """
    import pymysql
    from pymysql import OperationalError

    # 使用参数元组作为键的单例缓存
    _instances = func_kv(mysql_client)
    args_key = (host, user, password, database, port, charset, cursorclass)
    cursorclass_object = getattr(pymysql.cursors, cursorclass, pymysql.cursors.Cursor)

    if args_key not in _instances:

        class MySQLClient:
            """MySQL 客户端类"""

            def __init__(self):
                self.connection = None
                self.connect()

            def connect(self):
                """建立数据库连接"""
                try:
                    self.connection = pymysql.connect(
                        host=host,
                        user=user,
                        password=password,
                        database=database,
                        port=port,
                        charset=charset,
                        autocommit=True,
                        cursorclass=cursorclass_object,
                    )
                    print(f"成功连接到数据库: {host}:{port}/{database}")
                except Exception as e:
                    print(f"数据库连接失败: {e}")
                    raise

            def ensure_connection(self):
                """
                确保连接有效，如断开则重新连接
                """
                try:
                    if self.connection is None:
                        self.connect()
                    else:
                        self.connection.ping(reconnect=True)
                except OperationalError as e:
                    print(f"连接已断开，正在重连: {e}")
                    self.connect()

            def query(self, sql, params=None):
                """
                执行查询语句（SELECT等）
                """
                self.ensure_connection()
                try:
                    with self.connection.cursor() as cursor:
                        cursor.execute(sql, params)
                        return cursor.fetchall()
                except Exception as e:
                    print(f"查询执行失败: {e}")
                    raise

            def update(self, sql, params=None):
                """
                执行更新语句（INSERT/UPDATE/DELETE等）
                """
                self.ensure_connection()
                try:
                    with self.connection.cursor() as cursor:
                        cursor.execute(sql, params)
                        return cursor.rowcount
                except Exception as e:
                    print(f"更新执行失败: {e}")
                    self.connection.rollback()  # 发生错误时回滚
                    raise

            def update_many(self, sql, params_list: list | None = None):
                """
                执行批量更新语句（INSERT/UPDATE/DELETE等）
                """
                self.ensure_connection()
                try:
                    with self.connection.cursor() as cursor:
                        cursor.executemany(sql, params_list)
                        return cursor.rowcount
                except Exception as e:
                    print(f"批量更新执行失败: {e}")
                    self.connection.rollback()  # 发生错误时回滚
                    raise

            def close(self):
                """关闭数据库连接"""
                if self.connection:
                    self.connection.close()
                    print("数据库连接已关闭")
                    self.connection = None
                    # 从缓存中移除实例
                    if args_key in _instances and _instances[args_key] is self:
                        del _instances[args_key]

        # 创建并缓存实例
        _instances[args_key] = MySQLClient()

    return _instances[args_key]


def redis_queue(redisdb, name, use_existing: bool = True):
    """
    redis 队列

    - redisdb: redis 数据库, redis.Redis 对象, 不支持异步
    - name: 队列名称
    - use_existing: 是否使用已存在的队列, 默认为 True
    """

    class RedisQueue:
        def __init__(self, redisdb, name, use_existing):
            """
            redis 队列

            - redisdb: redis 数据库, redis.Redis 对象
            - name: 队列名称
            - use_existing: 是否使用已存在的队列, 默认为 True
            """
            self.redisdb = redisdb
            self.name = name
            self._index = 0
            self.use_existing = use_existing
            if not use_existing:
                self.redisdb.delete(self.name)

        def __len__(self):
            return self.redisdb.llen(self.name)

        def __getitem__(self, index):
            return read_json(self.redisdb.lindex(self.name, index))

        def push(self, item):
            self.redisdb.rpush(self.name, to_json(item))

        def pop(self):
            item = self.redisdb.lpop(self.name)
            return read_json(item) if item else None

        def clear(self):
            self.redisdb.delete(self.name)

        def __iter__(self):
            return self

        def __next__(self):
            if self._index >= len(self):
                self._index = 0
                raise StopIteration
            self._index += 1
            item = self[self._index - 1]
            if item is None:
                raise StopIteration
            return item

        def __repr__(self):
            return f"RedisQueue({self.name})"

        def __str__(self):
            return f"RedisQueue({self.name})"

    return RedisQueue(redisdb, name, use_existing)


def mongo_create_index(table, index_name, unique=False, background=True):
    """
    mongo 创建索引

    - table: mongo 表(集合), Collection 对象
    - index_name: 索引名称
    - unique: 是否唯一索引, 默认为 False
    - background: 是否后台创建索引, 默认为 True
    """
    return table.create_index(index_name, unique=unique, background=background)


def mongo_queue(table, use_existing: bool = True):
    """
    mongo 队列

    - table: mongo 表(集合), Collection 对象
    - use_existing: 是否使用已存在的队列, 默认为 True
    """
    from pymongo import collection

    class MongoQueue:
        def __init__(self, table, use_existing):
            """
            mongo 队列

            - table: mongo 表(集合), Collection 对象
            - use_existing: 是否使用已存在的队列, 默认为 True
            """
            self.table: collection.Collection = table
            self.name = table.name
            self.use_existing = use_existing
            if not use_existing:
                self.table.delete_many({})

        def __len__(self):
            return self.table.count_documents({})

        def push(self, item):
            _id = mongo_id()
            self.table.insert_one(
                {
                    "_id": _id,
                    "value": item,
                    "time": now(1),
                }
            )

        def pop(self):
            item = self.table.find_one_and_delete({})
            return item.get("value") if item else None

        def clear(self):
            self.table.delete_many({})

        def __iter__(self):
            cursor = self.table.find()
            for item in cursor:
                yield item

        def __repr__(self):
            return f"MongoQueue({self.name})"

        def __str__(self):
            return f"MongoQueue({self.name})"

    return MongoQueue(table, use_existing)


def mongo_dict(table):
    """
    mongo 字典

    - table: mongo 表(集合), Collection 对象
    """
    from pymongo import collection

    class MongoDict:
        def __init__(self, table):
            """
            mongo 字典

            - table: mongo 表(集合), Collection 对象
            """
            self.table: collection.Collection = table
            self.table.create_index("key", unique=True, background=True)

        def __getitem__(self, key):
            item = self.table.find_one({"key": key})
            if item is not None:
                return item["value"]
            else:
                raise KeyError(f"Key '{key}' not found in {self.table.name}")

        def get(self, key, default=None):
            """
            获取值，如果不存在则返回默认值

            - key: 键
            - default: 默认值
            """
            item = self.table.find_one({"key": key})
            if item is not None:
                return item["value"]
            else:
                return default

        def __setitem__(self, key, value):
            self.table.update_one(
                {"key": key},
                {"$set": {"value": value, "time": now(1)}},
                upsert=True,
            )

        def __delitem__(self, key):
            self.table.delete_one({"key": key})

        def pop(self, key, default=None):
            """
            删除并返回值，如果不存在则返回默认值

            - key: 键
            - default: 默认值
            """
            item = self.table.find_one_and_delete({"key": key})
            if item is not None:
                return item["value"]
            else:
                return default

        def clear(self):
            """
            删除所有键值对
            """
            self.table.delete_many({})

        def keys(self):
            """
            返回所有键
            """
            return [item["key"] for item in self.table.find()]

        def values(self):
            """
            返回所有值
            """
            return [item["value"] for item in self.table.find()]

        def items(self):
            """
            返回所有键值对
            """
            return [(item["key"], item["value"]) for item in self.table.find()]

        def __contains__(self, key):
            return self.table.find_one({"key": key}) is not None

        def __len__(self):
            return self.table.count_documents({})

        def __iter__(self):
            cursor = self.table.find()
            for item in cursor:
                yield item

        def __repr__(self):
            return f"MongoDict({self.table.name})"

        def __str__(self):
            return f"MongoDict({self.table.name})"

    return MongoDict(table)


def mongo_list_collection_names(db, startswith="", before=0):
    """
    列出表名, 支持前缀和后缀日期过滤

    - db: mongo 数据库, DataBase 对象
    - startswith: 表名前缀, 默认为空
    - before: 后缀 %Y%m%d 日期过滤几天前, 默认为 0, 不做任何过滤
    """
    from pymongo import database
    from pymongo import collection
    from ._tools import time_next

    if isinstance(db, collection.Collection):
        db = db.database

    if not isinstance(db, database.Database):
        raise ValueError("db 必须是 MongoDB DataBase对象")

    return sorted(
        [
            name
            for name in db.list_collection_names()
            if any(
                [
                    all([name.startswith(startswith), before == 0]),
                    all(
                        [
                            name.startswith(startswith),
                            name[-8:].isnumeric(),
                            name[-8:] <= time_next(-before, fmt="%Y%m%d"),
                        ]
                    ),
                ]
            )
        ]
    )


def mongo_find_one_and_update(
    db,
    filter,
    update,
    projection=None,
    sort=None,
    upsert: bool = False,
    size=1,
    **kwargs,
) -> list:
    """
    查找并更新文档, 支持批量查找

    - db: mongo 数据库, Collection 对象

    - filter: 查找条件, 字典对象
    - update: 更新操作, 字典对象
    - projection: 投影字段, 字典对象, 默认为 None, 表示不投影任何字段
    - sort: 排序字段, 列表对象, 默认为 None, 表示不排序
    - upsert: 是否插入, 布尔值, 默认为 False, 表示不插入
    - size: 批量查找数量, 整数, 默认为 1, 表示单条查找更新
    - kwargs: 其他参数
    """
    from pymongo import database

    if not isinstance(db, database.Collection):
        raise ValueError("db 必须是 MongoDB Database对象")

    return [
        seed
        for _ in range(size)
        if (
            seed := db.find_one_and_update(
                filter,
                update,
                projection,
                sort,
                upsert,
                **kwargs,
            )
        )
    ]


def clickhouse_client(
    host: str | None = None,
    username: str | None = None,
    password: str = "",
    access_token: str | None = None,
    database: str = "__default__",
    interface: str | None = None,
    port: int = 0,
    secure: bool | str = False,
    dsn: str | None = None,
    settings: dict | None = None,
    generic_args: dict[str] | None = None,
    executor_threads: int | None = None,
    async_mode: bool = False,
    **kwargs,
):
    """
    clickhouse 客户端

    - host: clickhouse 主机, 字符串对象
    - port: clickhouse 端口, 整数对象
    - username: clickhouse 用户, 字符串对象
    - password: clickhouse 密码, 字符串对象
    - database: clickhouse 数据库, 字符串对象
    - secure: 是否使用 https, 布尔值, 默认为 False
    - dsn: clickhouse dsn, 字符串对象, 默认为 None
    - settings: clickhouse 配置, 字典对象, 默认为 None
    - generic_args: clickhouse 泛型参数, 字典对象, 默认为 None
    - executor_threads: 执行线程数, 整数对象, 默认为 None
    - async_mode: 是否异步, 布尔值, 默认为 False
    - kwargs: 其他参数
    """

    if async_mode:
        from clickhouse_connect import get_async_client

        return get_async_client(
            host=host,
            username=username,
            password=password,
            database=database,
            interface=interface,
            port=port,
            secure=secure,
            dsn=dsn,
            settings=settings,
            generic_args=generic_args,
            executor_threads=executor_threads,
            **kwargs,
        )
    else:
        from clickhouse_connect import get_client

        return get_client(
            host=host,
            username=username,
            password=password,
            access_token=access_token,
            database=database,
            interface=interface,
            port=port,
            secure=secure,
            dsn=dsn,
            settings=settings,
            generic_args=generic_args,
            **kwargs,
        )


def cpu_count(logical: bool = False) -> int:
    """
    获取 cpu 核心数

    - logical: 是否获取逻辑核心数, 布尔值, 默认为 False, 表示获取物理核心数
    """
    import psutil

    return psutil.cpu_count(logical=logical)


def psutil_mem_keyword(keyword):
    """
    获取包含指定关键词的进程内存占用

    - keyword: 关键词, 字符串对象
    """
    import psutil

    total_rss = 0
    matched_procs = []
    for proc in psutil.process_iter(["pid", "name", "cmdline", "memory_info"]):
        try:
            info = proc.info
            cmdline = " ".join(info["cmdline"]) if info["cmdline"] else ""
            if (
                keyword.lower() in info["name"].lower()
                or keyword.lower() in cmdline.lower()
            ):
                rss_mb = info["memory_info"].rss / (1024 * 1024)  # 转成 MB
                matched_procs.append((info["pid"], cmdline, f"{rss_mb:.2f}MB"))
                total_rss += info["memory_info"].rss
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    total_mb = f"{total_rss / 1024 / 1024:.2f}MB"
    return total_mb, matched_procs
