modify: 清除diskcache依赖

This commit is contained in:
2026-09-04 14:44:06 +08:00
parent 2b5a7b09eb
commit 08657fa166
11 changed files with 424 additions and 115 deletions
+22 -14
View File
@@ -1,6 +1,6 @@
# chongming-cache # chongming-cache
统一缓存封装,dict-like 接口,后端可切换 **diskcache** / **redis**,通过配置一行切换,业务代码零改动。 统一缓存封装,dict-like 接口,后端可切换 **sqlite**(本机)/ **redis**(跨主机),通过配置一行切换,业务代码零改动。
--- ---
@@ -24,11 +24,12 @@ from chongming_cache import Cache
cache = Cache.from_config(config["cache"]) cache = Cache.from_config(config["cache"])
# 方式二:显式参数 # 方式二:显式参数
cache = Cache("diskcache", cache_store_path="./cache_data") cache = Cache("sqlite", path="./cache_data/cache.db")
# cache = Cache("redis", redis_host="127.0.0.1", redis_port=6379, redis_prefix="chongming:") # cache = Cache("redis", redis_host="127.0.0.1", redis_port=6379, redis_prefix="chongming:")
# 历史后端名 "diskcache" 兼容映射为 sqlite
``` ```
### 映射协议(与 diskcache 用法一致) ### 映射协议(与 dict / diskcache 用法一致)
```python ```python
cache["key"] = {"a": 1} # 写入(无 TTL) cache["key"] = {"a": 1} # 写入(无 TTL)
@@ -51,7 +52,7 @@ cache.close() # 释放连接
with Cache.from_config(cfg) as cache: ... # 上下文管理器,退出自动 close with Cache.from_config(cfg) as cache: ... # 上下文管理器,退出自动 close
cache.client # 底层客户端逃生口(diskcache.Cache / redis.Redis) cache.client # 底层客户端逃生口(sqlite3.Connection / redis.Redis)
# 原子写(分布式锁原语):仅当 key 不存在时写入,成功返回 True,已存在返回 False,不覆盖 # 原子写(分布式锁原语):仅当 key 不存在时写入,成功返回 True,已存在返回 False,不覆盖
ok = cache.backend.set_nx("leader", {"node": "gw-1"}, expire=30) ok = cache.backend.set_nx("leader", {"node": "gw-1"}, expire=30)
@@ -85,7 +86,7 @@ asyncio.run(main())
- `acquire()`:SET NX,原子地仅在不存在时写入;key 带 TTL 租约,持有方宕机后到期自动让出,不会死锁。 - `acquire()`:SET NX,原子地仅在不存在时写入;key 带 TTL 租约,持有方宕机后到期自动让出,不会死锁。
- `start()` / `stop()`:后台续约任务;续约前校验 token,仅在自己仍持有时 `touch`,锁丢失即停止续约。 - `start()` / `stop()`:后台续约任务;续约前校验 token,仅在自己仍持有时 `touch`,锁丢失即停止续约。
- `release()`:受保护释放——仅当 token 仍匹配(仍是自己持有)才删除,绝不误删其他进程/主机抢到的锁。 - `release()`:受保护释放——仅当 token 仍匹配(仍是自己持有)才删除,绝不误删其他进程/主机抢到的锁。
- **后端适用性**:`redis` 真正跨主机共享;`diskcache` 进程安全(SQLite),可用于单机多进程或共享卷场景,不可跨无共享文件系统的主机。 - **后端适用性**:`redis` 真正跨主机共享;`sqlite` 基于共享文件(SQLite 多进程安全),可用于单机多进程或共享卷场景,不可跨无共享文件系统的主机。
--- ---
@@ -93,8 +94,8 @@ asyncio.run(main())
```toml ```toml
[cache] [cache]
backend = "diskcache" # "diskcache"(默认) | "redis" backend = "sqlite" # "sqlite"(默认,本机共享) | "redis"(跨主机)
cache_store_path = "./cache_data" path = "./cache_data/cache.db" # 兼容旧键 cache_store_path(指向目录时自动追加 cache.db)
# redis 后端专用(backend = "redis" 时生效,均有默认值) # redis 后端专用(backend = "redis" 时生效,均有默认值)
# redis_host = "127.0.0.1" # redis_host = "127.0.0.1"
@@ -104,22 +105,29 @@ cache_store_path = "./cache_data"
# redis_prefix = "chongming:" # redis_prefix = "chongming:"
``` ```
所有键均可缺省:旧配置(只有 `cache_store_path`)自动按 diskcache 后端工作。 所有键均可缺省:旧配置(只有 `cache_store_path`)自动按 sqlite 后端工作。
--- ---
## 双后端语义对齐 ## 双后端语义对齐
| 行为 | diskcache | redis | | 行为 | sqlite(本机) | redis(跨主机) |
| ---- | --------- | ----- | | ---- | ----------- | ----------- |
| 值序列化 | pickle | pickle(保持任意 Python 对象语义一致) | | 值序列化 | JSON(orjson) | JSON(orjson)(与 Rust 进程插件 serde_json 互通) |
| 未命中 `cache[key]` | `KeyError` | `KeyError` | | 未命中 `cache[key]` | `KeyError` | `KeyError` |
| `set(..., expire=N)` | 秒级 TTL,读时惰性淘汰 | `SET EX N`,到期即消失 | | `set(..., expire=N)` | 秒级 TTL,读时惰性淘汰 | `SET EX N`,到期即消失 |
| `touch(未命中)` | 返回 False | 返回 False | | `touch(未命中)` | 返回 False | 返回 False |
| `clear()` | 清空本地缓存 | 仅清 `redis_prefix` 命名空间(不 flushdb) | | `clear()` | 清空本地缓存 | 仅清 `redis_prefix` 命名空间(不 flushdb) |
| 多进程安全 | 是(SQLite + 锁) | 是(服务端原子性) | | 多进程安全 | 是(SQLite 文件 + WAL) | 是(服务端原子性) |
## 跨语言共享(Rust 插件 / sqlite)
sqlite 后端把值以 JSON 文本存进表 `cache(k TEXT PRIMARY KEY, v TEXT NOT NULL, expire REAL)`,
schema 与 Rust 进程插件(`chongming-worker-rs`,feature `local-cache`)完全一致——Python 与
Rust 读写**同一份 `.db` 文件**即可互通,无需 Redis。Rust 侧读侧不改变该 schema。
## 依赖 ## 依赖
- `diskcache` — 本地持久化后端 - `sqlite3` — 本地共享后端(Python 标准库,零第三方依赖)
- `redis` — 远程后端(懒加载,diskcache-only 打包产物不引入) - `redis` — 远程后端(懒加载,仅选择 redis 后端时才 import)
- `orjson` — 全后端统一 JSON 序列化(缺失时回退标准库 `json`)
+2 -2
View File
@@ -5,12 +5,12 @@ requires = ["hatchling"]
[project] [project]
name = "chongming-cache" name = "chongming-cache"
version = "0.2.0" version = "0.2.0"
description = "统一缓存封装:兼容 diskcache / redis 后端" description = "统一缓存封装:兼容 sqlite(本地) / redis(共享) 后端"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"diskcache>=5.6.3",
"redis>=5.0.0", "redis>=5.0.0",
"orjson>=3.9.0",
] ]
[dependency-groups] [dependency-groups]
+5 -1
View File
@@ -1,13 +1,17 @@
from chongming_cache.backends import CacheBackend, DiskBackend, RedisBackend from chongming_cache.backends import CacheBackend, RedisBackend, SqliteBackend
from chongming_cache.cache import Cache from chongming_cache.cache import Cache
from chongming_cache.lock import DistributedLock from chongming_cache.lock import DistributedLock
# 兼容旧名:原 diskcache 后端已由 SQLite 后端 (SqliteBackend) 直接替换
DiskBackend = SqliteBackend
__version__ = "0.2.0" __version__ = "0.2.0"
__all__ = [ __all__ = [
"Cache", "Cache",
"CacheBackend", "CacheBackend",
"DiskBackend", "DiskBackend",
"SqliteBackend",
"RedisBackend", "RedisBackend",
"DistributedLock", "DistributedLock",
"__version__", "__version__",
+2 -2
View File
@@ -1,5 +1,5 @@
from chongming_cache.backends.base import CacheBackend from chongming_cache.backends.base import CacheBackend
from chongming_cache.backends.disk import DiskBackend
from chongming_cache.backends.redis_ import RedisBackend from chongming_cache.backends.redis_ import RedisBackend
from chongming_cache.backends.sqlite_ import SqliteBackend
__all__ = ["CacheBackend", "DiskBackend", "RedisBackend"] __all__ = ["CacheBackend", "SqliteBackend", "RedisBackend"]
+75
View File
@@ -1,8 +1,83 @@
"""缓存后端抽象接口:dict-like 映射 + TTL。""" """缓存后端抽象接口:dict-like 映射 + TTL。"""
import base64
import pickle
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Iterator from typing import Any, Iterator
try:
import orjson
def _dumps(value: Any) -> bytes:
# default:bytes 无法原生 JSON 化,包成 {__b64__: base64},读取时还原为 bytes
def _default(obj: Any) -> Any:
if isinstance(obj, bytes):
return {_B64_MARKER: base64.b64encode(obj).decode("ascii")}
raise TypeError(f"无法序列化为 JSON: {type(obj)!r}")
return orjson.dumps(value, default=_default)
def _loads(raw) -> Any:
return orjson.loads(raw)
except ImportError: # pragma: no cover
import json
def _dumps(value: Any) -> bytes:
def _default(obj: Any) -> Any:
if isinstance(obj, bytes):
return {_B64_MARKER: base64.b64encode(obj).decode("ascii")}
raise TypeError(f"无法序列化为 JSON: {type(obj)!r}")
return json.dumps(value, ensure_ascii=False, default=_default).encode("utf-8")
def _loads(raw) -> Any:
return json.loads(raw.decode("utf-8"))
# bytes 的 base64 包裹标记:让 bytes(如 RPC 缓存回复)在 JSON 下也能无损往返。
_B64_MARKER = "__b64__"
# 所有后端统一以 JSON 持久化:跨后端(redis/本地 diskcache)语义一致,
# 且与 Rust 进程插件(serde_json / json)的共享协议互通。
def _restore_bytes(obj: Any) -> Any:
"""递归还原被包裹的 bytes。"""
if isinstance(obj, dict):
if len(obj) == 1 and _B64_MARKER in obj:
try:
return base64.b64decode(obj[_B64_MARKER])
except Exception:
pass
return {k: _restore_bytes(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_restore_bytes(v) for v in obj]
return obj
def encode_value(value: Any) -> bytes:
"""统一序列化:JSON(orjson)。bytes 以 base64 包裹;datetime/set/自定义对象不支持。"""
return _dumps(value)
def decode_value(raw: Any) -> Any:
"""统一反序列化。
- 字节值优先按 JSON 解析(orjson);bytes 从 base64 包裹还原;
- 解析失败回退 pickle(兼容历史 pickled 数据,跨后端切换窗口);
- 若后端已把对象透明还原(旧磁盘直存路径),非字节则原样返回。
"""
if not isinstance(raw, (bytes, bytearray)):
return raw
try:
return _restore_bytes(_loads(raw))
except Exception:
pass
try:
return pickle.loads(raw)
except Exception:
return raw
class CacheBackend(ABC): class CacheBackend(ABC):
"""缓存后端统一接口。 """缓存后端统一接口。
-64
View File
@@ -1,64 +0,0 @@
"""diskcache 后端:基于 SQLite 的本地持久化缓存,多进程安全。"""
from pathlib import Path
from typing import Any, Iterator
from diskcache import Cache as DiskCache
from chongming_cache.backends.base import CacheBackend
class DiskBackend(CacheBackend):
"""包装 diskcache.Cache,提供统一接口。
Args:
path: 缓存目录(SQLite 数据文件所在目录),父目录不存在时自动创建。
"""
def __init__(self, path: str | Path):
self._cache = DiskCache(path)
@property
def client(self) -> DiskCache:
"""底层 diskcache.Cache 实例(逃生口)。"""
return self._cache
def __contains__(self, key) -> bool:
return key in self._cache
def __getitem__(self, key) -> Any:
return self._cache[key]
def __setitem__(self, key, value) -> None:
self._cache[key] = value
def __delitem__(self, key) -> None:
del self._cache[key]
def __len__(self) -> int:
return len(self._cache)
def __iter__(self) -> Iterator:
return iter(self._cache)
def get(self, key, default=None) -> Any:
return self._cache.get(key, default)
def set(self, key, value, expire: float | None = None) -> None:
self._cache.set(key, value, expire=expire)
def set_nx(self, key, value, expire: float | None = None) -> bool:
# diskcache.add 仅在 key 不存在时写入,返回是否成功(SET NX 语义)且进程安全
return bool(self._cache.add(key, value, expire=expire))
def touch(self, key, expire: float | None = None) -> bool:
return self._cache.touch(key, expire=expire)
def delete(self, key) -> bool:
return self._cache.delete(key)
def clear(self) -> None:
self._cache.clear()
def close(self) -> None:
self._cache.close()
+30 -11
View File
@@ -2,12 +2,16 @@
redis-py 采用懒加载:仅在选择 redis 后端时才 import, redis-py 采用懒加载:仅在选择 redis 后端时才 import,
使用 diskcache 后端的打包产物不会引入 redis 依赖。 使用 diskcache 后端的打包产物不会引入 redis 依赖。
值统一使用 JSON(orjson)序列化,与本地 diskcache 后端及 Rust 进程插件
(serde_json)共享同一套跨语言协议;``prefix`` 作为 key 命名空间前缀。
""" """
import pickle import datetime
from typing import Any, Iterator from typing import Any, Iterator
from chongming_cache.backends.base import CacheBackend from chongming_cache.backends.base import CacheBackend, decode_value, encode_value
# SCAN 批大小与 clear() 管道批大小 # SCAN 批大小与 clear() 管道批大小
_SCAN_COUNT = 1000 _SCAN_COUNT = 1000
@@ -16,8 +20,8 @@ _SCAN_COUNT = 1000
class RedisBackend(CacheBackend): class RedisBackend(CacheBackend):
"""包装 redis-py 客户端,提供统一接口。 """包装 redis-py 客户端,提供统一接口。
值使用 pickle 序列化(与 diskcache 默认序列化语义对齐,支持任意 Python 对象); 值统一使用 JSON(orjson)序列化;``prefix`` 作为 key 命名空间前缀,
``prefix`` 作为 key 命名空间前缀,便于多实例共享同一 Redis 时互不干扰。 便于多实例共享同一 Redis 时互不干扰。
Args: Args:
host: Redis 地址 host: Redis 地址
@@ -66,6 +70,19 @@ class RedisBackend(CacheBackend):
"""去掉前缀并解码为 str。""" """去掉前缀并解码为 str。"""
return raw[len(self._prefix) :].decode() return raw[len(self._prefix) :].decode()
@staticmethod
def _expire_secs(expire):
"""把秒(可能是 float)规范为 redis-py 可接受的取值。
redis-py 的 ``SET EX`` / ``EXPIRE`` 只接受 ``int`` 或 ``datetime.timedelta``,
传入 float(如 DistributedLock 默认 ttl=30.0)会抛 ``DataError``。
"""
if isinstance(expire, datetime.timedelta):
return expire
if expire is not None:
return int(expire)
return None
def __contains__(self, key) -> bool: def __contains__(self, key) -> bool:
return bool(self._redis.exists(self._key(key))) return bool(self._redis.exists(self._key(key)))
@@ -73,10 +90,10 @@ class RedisBackend(CacheBackend):
raw = self._redis.get(self._key(key)) raw = self._redis.get(self._key(key))
if raw is None: if raw is None:
raise KeyError(key) raise KeyError(key)
return pickle.loads(raw) return decode_value(raw)
def __setitem__(self, key, value) -> None: def __setitem__(self, key, value) -> None:
self._redis.set(self._key(key), pickle.dumps(value)) self._redis.set(self._key(key), encode_value(value))
def __delitem__(self, key) -> None: def __delitem__(self, key) -> None:
if not self._redis.delete(self._key(key)): if not self._redis.delete(self._key(key)):
@@ -95,25 +112,27 @@ class RedisBackend(CacheBackend):
raw = self._redis.get(self._key(key)) raw = self._redis.get(self._key(key))
if raw is None: if raw is None:
return default return default
return pickle.loads(raw) return decode_value(raw)
def set(self, key, value, expire: float | None = None) -> None: def set(self, key, value, expire: float | None = None) -> None:
self._redis.set(self._key(key), pickle.dumps(value), ex=expire) self._redis.set(
self._key(key), encode_value(value), ex=self._expire_secs(expire)
)
def set_nx(self, key, value, expire: float | None = None) -> bool: def set_nx(self, key, value, expire: float | None = None) -> bool:
# redis SET key val NX EX ttl:原子地仅在不存在时写入,用于分布式锁 # redis SET key val NX EX ttl:原子地仅在不存在时写入,用于分布式锁
return bool( return bool(
self._redis.set( self._redis.set(
self._key(key), self._key(key),
pickle.dumps(value), encode_value(value),
ex=expire, ex=self._expire_secs(expire),
nx=True, nx=True,
) )
) )
def touch(self, key, expire: float | None = None) -> bool: def touch(self, key, expire: float | None = None) -> bool:
# expire=None 时 redis-py 发 PERSIST(永久),与 diskcache.touch 语义一致 # expire=None 时 redis-py 发 PERSIST(永久),与 diskcache.touch 语义一致
return bool(self._redis.expire(self._key(key), expire)) return bool(self._redis.expire(self._key(key), self._expire_secs(expire)))
def delete(self, key) -> bool: def delete(self, key) -> bool:
return bool(self._redis.delete(self._key(key))) return bool(self._redis.delete(self._key(key)))
+178
View File
@@ -0,0 +1,178 @@
"""sqlite 后端:基于标准库 SQLite 的本地共享键值缓存(替代原 diskcache 后端)。
值以 JSON(orjson) 存入 TEXT 列,序列化协议与 redis 后端 / Rust 进程插件(serde_json)
共享同一套 schema,因此 Rust 侧(sqlx)可直接读写同一份 .db 文件,实现真正的跨语言
本地共享(无需 Redis)。
- 本地、多进程安全:WAL + busy_timeout + SQLite 行锁(等价于原 diskcache 的多进程语义);
- TTL:``expire`` 列存到期 epoch 秒,读时惰性淘汰(对齐 diskcache/redis 的 get 语义)。
共享 schema(Ruby/Python 与 Rust 必须一致):
CREATE TABLE IF NOT EXISTS cache (
k TEXT PRIMARY KEY,
v TEXT NOT NULL, -- JSON 文本
expire REAL -- 到期 epoch 秒;NULL 表示永不过期
);
"""
import sqlite3
import threading
import time
from pathlib import Path
from typing import Any, Iterator
from chongming_cache.backends.base import CacheBackend, decode_value, encode_value
_SCHEMA = """
CREATE TABLE IF NOT EXISTS cache (
k TEXT PRIMARY KEY,
v TEXT NOT NULL,
expire REAL
);
"""
class SqliteBackend(CacheBackend):
"""包装 sqlite3,提供统一接口(与 Rust 侧共享同一份 schema)。
Args:
path: SQLite 数据库文件路径(父目录不存在时自动创建)。
"""
def __init__(self, path: str | Path):
p = Path(path)
if p.parent and str(p.parent) != ".":
p.parent.mkdir(parents=True, exist_ok=True)
self._path = str(p)
self._lock = threading.RLock()
self._conn = sqlite3.connect(self._path, check_same_thread=False)
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.execute("PRAGMA busy_timeout=5000")
self._conn.execute(_SCHEMA)
self._conn.commit()
@property
def client(self) -> sqlite3.Connection:
"""底层 sqlite3.Connection 实例(逃生口)。"""
return self._conn
@staticmethod
def _expire_secs(expire) -> float | None:
"""秒级 TTL -> 到期 epoch 时间戳;None 表示永不过期。"""
if expire is None:
return None
return time.time() + float(expire)
def _live_value(self, k: str) -> str | None:
"""取未过期的原始 JSON 文本;过期或缺失返回 None(过期时顺手删除)。"""
row = self._conn.execute(
"SELECT v, expire FROM cache WHERE k = ?", (k,)
).fetchone()
if row is None:
return None
v, expire = row
if expire is not None and expire <= time.time():
self._conn.execute("DELETE FROM cache WHERE k = ?", (k,))
self._conn.commit()
return None
return v
def __contains__(self, key) -> bool:
with self._lock:
return self._live_value(str(key)) is not None
def __getitem__(self, key) -> Any:
with self._lock:
raw = self._live_value(str(key))
if raw is None:
raise KeyError(key)
return decode_value(raw.encode("utf-8"))
def __setitem__(self, key, value) -> None:
with self._lock:
self._set(str(key), value, None)
def __delitem__(self, key) -> None:
with self._lock:
if not self.delete(key):
raise KeyError(key)
def __len__(self) -> int:
with self._lock:
now = time.time()
self._conn.execute(
"DELETE FROM cache WHERE expire IS NOT NULL AND expire <= ?", (now,)
)
self._conn.commit()
return self._conn.execute("SELECT COUNT(*) FROM cache").fetchone()[0]
def __iter__(self) -> Iterator:
with self._lock:
now = time.time()
self._conn.execute(
"DELETE FROM cache WHERE expire IS NOT NULL AND expire <= ?", (now,)
)
self._conn.commit()
rows = self._conn.execute("SELECT k FROM cache").fetchall()
return iter(row[0] for row in rows)
def get(self, key, default=None) -> Any:
with self._lock:
raw = self._live_value(str(key))
if raw is None:
return default
return decode_value(raw.encode("utf-8"))
def _set(self, k: str, value: Any, expire) -> None:
v = encode_value(value).decode("utf-8")
self._conn.execute(
"INSERT INTO cache(k, v, expire) VALUES(?, ?, ?) "
"ON CONFLICT(k) DO UPDATE SET v=excluded.v, expire=excluded.expire",
(k, v, expire),
)
self._conn.commit()
def set(self, key, value, expire: float | None = None) -> None:
with self._lock:
self._set(str(key), value, self._expire_secs(expire))
def set_nx(self, key, value, expire: float | None = None) -> bool:
with self._lock:
k = str(key)
v = encode_value(value).decode("utf-8")
cur = self._conn.execute(
"INSERT OR IGNORE INTO cache(k, v, expire) VALUES(?, ?, ?)",
(k, v, self._expire_secs(expire)),
)
self._conn.commit()
return cur.rowcount == 1
def touch(self, key, expire: float | None = None) -> bool:
with self._lock:
k = str(key)
if self._live_value(k) is None:
return False
self._conn.execute(
"UPDATE cache SET expire=? WHERE k=?", (self._expire_secs(expire), k)
)
self._conn.commit()
return True
def delete(self, key) -> bool:
with self._lock:
cur = self._conn.execute("DELETE FROM cache WHERE k = ?", (str(key),))
self._conn.commit()
return cur.rowcount > 0
def clear(self) -> None:
with self._lock:
self._conn.execute("DELETE FROM cache")
self._conn.commit()
def close(self) -> None:
with self._lock:
try:
self._conn.close()
except Exception:
pass
+23 -14
View File
@@ -1,10 +1,14 @@
"""统一缓存门面:dict-like 接口,后端可切换 diskcache / redis。""" """统一缓存门面:dict-like 接口,后端可切换 sqlite(本地) / redis(共享)。"""
from pathlib import Path
from typing import Any, Iterator, Mapping from typing import Any, Iterator, Mapping
from chongming_cache.backends import CacheBackend, DiskBackend, RedisBackend from chongming_cache.backends import CacheBackend, RedisBackend, SqliteBackend
_KNOWN_BACKENDS = {"diskcache", "redis"} _KNOWN_BACKENDS = {"sqlite", "redis"}
# 旧后端名 diskcache → sqlite(新本地后端直接替换了原 diskcache 实现)
_BACKEND_ALIASES = {"diskcache": "sqlite"}
_DEFAULT_PATH = "./cache_data/cache.db"
class Cache: class Cache:
@@ -24,11 +28,11 @@ class Cache:
value = cache["k"] value = cache["k"]
""" """
def __init__(self, backend: str = "diskcache", **params): def __init__(self, backend: str = "sqlite", **params):
"""创建缓存实例。 """创建缓存实例。
Args: Args:
backend: "diskcache" 或 "redis" backend: "sqlite" 或 "redis"(旧名 "diskcache" 兼容映射到 sqlite)
**params: 后端参数,见 ``from_config`` 说明 **params: 后端参数,见 ``from_config`` 说明
""" """
self._backend = self._create_backend(backend, params) self._backend = self._create_backend(backend, params)
@@ -37,24 +41,29 @@ class Cache:
def from_config(cls, config: Mapping) -> "Cache": def from_config(cls, config: Mapping) -> "Cache":
"""从配置字典创建缓存实例,读取形如主项目 config.toml 的 ``[cache]`` 段: """从配置字典创建缓存实例,读取形如主项目 config.toml 的 ``[cache]`` 段:
- ``backend``: "diskcache"(默认)或 "redis" - ``backend``: "sqlite"(默认,本地共享)或 "redis"(跨主机)
- ``cache_store_path``: diskcache 缓存目录(默认 "./cache_data") - ``path`` / ``cache_store_path``: sqlite 数据库文件路径(默认 "./cache_data/cache.db")
- ``redis_host`` / ``redis_port`` / ``redis_db`` / ``redis_password`` / ``redis_prefix``: - ``redis_host`` / ``redis_port`` / ``redis_db`` / ``redis_password`` / ``redis_prefix``:
redis 后端连接参数(仅 backend = "redis" 时生效) redis 后端连接参数(仅 backend = "redis" 时生效)
所有键均可缺省,旧配置(只有 cache_store_path)直接兼容。 所有键均可缺省,旧配置(只有 cache_store_path)直接兼容。
""" """
cfg = dict(config) cfg = dict(config)
return cls(cfg.pop("backend", "diskcache"), **cfg) return cls(cfg.pop("backend", "sqlite"), **cfg)
@staticmethod @staticmethod
def _create_backend(backend: str, params: Mapping) -> CacheBackend: def _create_backend(backend: str, params: Mapping) -> CacheBackend:
name = (backend or "diskcache").strip().lower() name = _BACKEND_ALIASES.get((backend or "sqlite").strip().lower(), (backend or "sqlite").strip().lower())
if name not in _KNOWN_BACKENDS: if name not in _KNOWN_BACKENDS:
raise ValueError(f"不支持的缓存后端: {backend!r}(可选 diskcache / redis)") raise ValueError(f"不支持的缓存后端: {backend!r}(可选 sqlite / redis)")
if name == "diskcache": if name == "sqlite":
return DiskBackend(params.get("cache_store_path", "./cache_data")) path = params.get("path") or params.get("cache_store_path") or _DEFAULT_PATH
p = Path(path)
# 兼容旧 diskcache 配置:路径若指向目录则追加默认 db 文件名
if path.endswith(("/", "\\")) or p.is_dir():
p = p / "cache.db"
return SqliteBackend(p)
return RedisBackend( return RedisBackend(
host=params.get("redis_host", "127.0.0.1"), host=params.get("redis_host", "127.0.0.1"),
@@ -66,12 +75,12 @@ class Cache:
@property @property
def backend(self) -> CacheBackend: def backend(self) -> CacheBackend:
"""后端封装实例(DiskBackend / RedisBackend)。""" """后端封装实例(SqliteBackend / RedisBackend)。"""
return self._backend return self._backend
@property @property
def client(self): def client(self):
"""底层客户端(diskcache.Cache / redis.Redis),逃生口。""" """底层客户端(sqlite3.Connection / redis.Redis),逃生口。"""
return self._backend.client return self._backend.client
# ── 映射协议 ────────────────────────────────────────────── # ── 映射协议 ──────────────────────────────────────────────
+31 -5
View File
@@ -5,6 +5,7 @@ redis 后端优先连接真实服务(REDIS_HOST/REDIS_PORT),不可用时
""" """
import os import os
import sqlite3
import threading import threading
import time import time
@@ -57,7 +58,7 @@ def _shutdown_fake_redis():
@pytest.fixture @pytest.fixture
def disk_cache(tmp_path): def disk_cache(tmp_path):
cache = Cache.from_config( cache = Cache.from_config(
{"backend": "diskcache", "cache_store_path": str(tmp_path / "cache")} {"backend": "sqlite", "path": str(tmp_path / "cache.db")}
) )
yield cache yield cache
cache.close() cache.close()
@@ -116,7 +117,7 @@ def test_expire_disk(disk_cache):
disk_cache.set("e", 1, expire=1) disk_cache.set("e", 1, expire=1)
assert "e" in disk_cache assert "e" in disk_cache
time.sleep(1.2) time.sleep(1.2)
assert "e" not in disk_cache # diskcache 读时惰性淘汰 assert "e" not in disk_cache # sqlite 读时惰性淘汰
assert disk_cache.get("e", "miss") == "miss" assert disk_cache.get("e", "miss") == "miss"
@@ -130,14 +131,14 @@ def test_touch_disk(disk_cache):
def test_context_manager(tmp_path): def test_context_manager(tmp_path):
with Cache.from_config( with Cache.from_config(
{"backend": "diskcache", "cache_store_path": str(tmp_path / "cm")} {"backend": "sqlite", "path": str(tmp_path / "cm.db")}
) as cache: ) as cache:
cache["x"] = 1 cache["x"] = 1
assert cache["x"] == 1 assert cache["x"] == 1
def test_from_config_defaults(tmp_path, monkeypatch): def test_from_config_defaults(tmp_path, monkeypatch):
# 缺省 backend 时默认 diskcache;缺省 cache_store_path 时用 ./cache_data # 缺省 backend 时默认 sqlite;缺省 path 时用 ./cache_data/cache.db
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
cache = Cache.from_config({}) cache = Cache.from_config({})
try: try:
@@ -149,7 +150,32 @@ def test_from_config_defaults(tmp_path, monkeypatch):
def test_invalid_backend(tmp_path): def test_invalid_backend(tmp_path):
with pytest.raises(ValueError): with pytest.raises(ValueError):
Cache.from_config({"backend": "memcached", "cache_store_path": str(tmp_path)}) Cache.from_config({"backend": "memcached", "path": str(tmp_path)})
def test_sqlite_json_interop_and_multi_instance(tmp_path):
"""SQLite 落盘是纯 JSON 文本(Rust 可读),且两个后端实例共享同一文件。"""
db = str(tmp_path / "shared.db")
a = Cache.from_config({"backend": "sqlite", "path": db})
b = Cache.from_config({"backend": "sqlite", "path": db})
try:
# 待共享值:纯 JSON 类型,应原样以 JSON 文本落盘
a.set("app:latest", {"msg": "hello", "n": 42})
# 共享文件可跨实例读取(多进程安全语义)
assert b.get("app:latest") == {"msg": "hello", "n": 42}
# 直接查库:v 列为 JSON 文本而非 pickle 字节
row = sqlite3.connect(db).execute(
"SELECT v FROM cache WHERE k='app:latest'"
).fetchone()
assert row is not None
assert row[0] == '{"msg":"hello","n":42}'
# TTL 过期后跨实例同样读不到
a.set("exp", 1, expire=1)
time.sleep(1.1)
assert b.get("exp") is None
finally:
a.close()
b.close()
# ── redis 后端 ────────────────────────────────────────────────── # ── redis 后端 ──────────────────────────────────────────────────
Generated
+55 -1
View File
@@ -187,10 +187,11 @@ wheels = [
[[package]] [[package]]
name = "chongming-cache" name = "chongming-cache"
version = "0.1.0" version = "0.2.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "diskcache" }, { name = "diskcache" },
{ name = "orjson" },
{ name = "redis" }, { name = "redis" },
] ]
@@ -204,6 +205,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "diskcache", specifier = ">=5.6.3" }, { name = "diskcache", specifier = ">=5.6.3" },
{ name = "orjson", specifier = ">=3.9.0" },
{ name = "redis", specifier = ">=5.0.0" }, { name = "redis", specifier = ">=5.0.0" },
] ]
@@ -445,6 +447,58 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" },
] ]
[[package]]
name = "orjson"
version = "3.12.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.3" version = "26.3"