diff --git a/main.py b/main.py index cd942d9..410bb81 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,166 @@ +#!/usr/bin/env python3 +"""端到端冒烟示例:把"打包 → 校验 → 快照 → 发布 → 取回"跑一遍。 + +直接运行即可(全部在临时目录里操作,不碰仓库里的任何文件): + + python main.py + +原来的版本拿仓库自带的 appcation.svfs 做演示,但那个库是**旧格式**(schema 1), +当前版本会直接拒绝并提示用 `vfsmigrate` 迁移——所以示例改成自给自足的一棵树, +走到哪儿都能跑通。 + +要了解完整能力看 README.md;要了解踩过的坑与取舍看 BUG_REPORT.md。 +""" + +import contextlib +import io +import os +import shutil +import sys +import tempfile + +from sqlite_vfs import publish from sqlite_vfs.core import SQLiteVFS +from sqlite_vfs.folder_packer import FolderPacker -vfs = SQLiteVFS("appcation.svfs") -print(vfs.get_file_info(".gitignore")) -for item in vfs.list_directory("/"): - print(f"{'DIR' if item['is_directory'] else 'FILE'}: {item['path']}") -vfs.close() +def step(text): + print(f"\n=== {text} ===") + + +@contextlib.contextmanager +def quiet(): + """屏蔽打包过程的逐文件输出,让演示保持可读。""" + with contextlib.redirect_stdout(io.StringIO()): + yield + + +def write_file(root, rel_path, data): + target = os.path.join(root, *rel_path.split("/")) + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, "wb") as f: + f.write(data) + return target + + +def read_tree(root): + out = {} + for base, _dirs, files in os.walk(root): + for name in files: + path = os.path.join(base, name) + with open(path, "rb") as f: + out[os.path.relpath(path, root).replace(os.sep, "/")] = f.read() + return out + + +def main(): + work = tempfile.mkdtemp(prefix="svfs_demo_") + try: + # ---- 1. 造一棵源目录:含两份内容完全相同的文件,用来展示去重 ---- + src = os.path.join(work, "dist") + write_file(src, "index.html", b"
sub page
", + "sub/index.html": b"sub index
", + "assets/logo.png": b"\x89PNG\r\n\x1a\n", +} + + +@unittest.skipUnless(AVAILABLE, f"需要 fastapi/starlette/httpx: {IMPORT_ERROR}") +class NeedsFastAPI(helpers.VFSTestCase): + """缺少框架时整类跳过(子类通过属性继承同样被跳过)。""" + + +class PluginTestCase(NeedsFastAPI): + """构造一个真实的 .svfs 资源包,并把它挂在 FastAPI 上当静态目录。""" + + compress = True + algorithm = None # None = 用默认算法(zlib) + spa = True + html = True + + def setUp(self): + super().setUp() + self.src = helpers.make_tree(self.path("dist"), ASSETS) + self.db = helpers.pack( + self.src, + self.path("static.svfs"), + compress=self.compress, + name="assets", + algorithm=self.algorithm, + ) + self.md5_before = helpers.md5_of(self.db) + self.statics = SVFSStaticFiles(self.db, html=self.html, spa=self.spa) + self.addCleanup(self.statics.close) + + app = FastAPI() + app.mount("/", self.statics, name="static") + self.client = TestClient(app, follow_redirects=False) + + def get(self, path, **kwargs): + return self.client.get(path, headers=kwargs.pop("headers", None), **kwargs) + + def assert_unmodified(self): + """所有请求都必须是只读的,资源包不能被改动。""" + self.assertEqual(self.md5_before, helpers.md5_of(self.db)) + + +class BasicServingTest(PluginTestCase): + def test_serves_top_level_file(self): + response = self.get("/app.js") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["app.js"], response.content) + self.assertEqual(b"console.log('hi');", response.content) + + def test_serves_nested_file(self): + response = self.get("/sub/page.html") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["sub/page.html"], response.content) + + def test_serves_empty_file(self): + response = self.get("/empty.txt") + self.assertEqual(200, response.status_code) + self.assertEqual(b"", response.content) + self.assertEqual("0", response.headers["content-length"]) + + def test_serves_compressed_file_correctly(self): + """大文件在库里是哈夫曼压缩存储的,取出来必须还原成原始字节。""" + response = self.get("/big.txt") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["big.txt"], response.content) + + def test_content_length_matches_body(self): + response = self.get("/big.txt") + self.assertEqual(str(len(ASSETS["big.txt"])), response.headers["content-length"]) + + +class ContentTypeTest(PluginTestCase): + def test_web_types_are_not_platform_dependent(self): + """Windows 注册表会把 .mjs 判成 text/plain(浏览器会拒绝执行模块), + 这里断言插件用的是显式覆盖表。""" + expected = { + "/app.js": "text/javascript", + "/app.mjs": "text/javascript", + "/style.css": "text/css", + "/index.html": "text/html", + "/data.json": "application/json", + "/font.woff2": "font/woff2", + "/assets/logo.png": "image/png", + } + for path, content_type in expected.items(): + with self.subTest(path=path): + response = self.get(path) + self.assertEqual(200, response.status_code) + self.assertEqual( + content_type, response.headers["content-type"].split(";")[0] + ) + + def test_unknown_extension_falls_back_to_octet_stream(self): + helpers.write_file(os.path.join(self.src, "blob.bin"), b"\x00\x01") + helpers.pack(self.src, self.db, compress=self.compress) + self.assertEqual( + "application/octet-stream", + self.get("/blob.bin").headers["content-type"].split(";")[0], + ) + + +class IndexAndSpaTest(PluginTestCase): + def test_root_serves_index_html(self): + response = self.get("/") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["index.html"], response.content) + self.assertEqual("text/html", response.headers["content-type"].split(";")[0]) + + def test_directory_redirects_to_trailing_slash(self): + response = self.get("/sub") + self.assertEqual(307, response.status_code) + # Starlette 生成的是绝对 URL + self.assertTrue( + response.headers["location"].endswith("/sub/"), + response.headers["location"], + ) + + def test_directory_with_slash_serves_its_index(self): + response = self.get("/sub/") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["sub/index.html"], response.content) + + def test_spa_fallback_for_frontend_route(self): + """浏览器导航到前端路由 /settings 时应当拿到 index.html。""" + response = self.get("/settings", headers={"accept": "text/html"}) + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["index.html"], response.content) + + def test_spa_fallback_for_deep_route(self): + response = self.get("/user/42/detail", headers={"accept": "text/html"}) + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["index.html"], response.content) + + def test_no_fallback_for_missing_asset(self): + """缺静态资源必须是 404,否则前端拿到一份 HTML 会报诡异的解析错误。""" + self.assertEqual(404, self.get("/missing.js").status_code) + self.assertEqual(404, self.get("/missing.css").status_code) + + def test_no_fallback_for_json_request(self): + """XHR/接口请求(Accept 不含 text/html)不该被回退成 index.html。""" + self.assertEqual( + 404, self.get("/api/data", headers={"accept": "application/json"}).status_code + ) + + def test_missing_file_returns_404(self): + self.assertEqual(404, self.get("/nope.txt").status_code) + + def test_post_is_rejected(self): + self.assertEqual(405, self.client.post("/app.js").status_code) + + def test_path_traversal_cannot_escape_the_pack(self): + """资源包不是磁盘目录,.. 之类无法越出数据库。""" + for path in ("/../../etc/passwd", "/..%2f..%2fsecret.txt", "/sub/../../app.js"): + with self.subTest(path=path): + self.assertIn(self.get(path).status_code, (200, 404, 307)) + self.assert_unmodified() + + def test_repeated_requests_do_not_modify_pack(self): + for path in ("/", "/app.js", "/big.txt", "/sub/", "/settings"): + self.get(path) + self.assert_unmodified() + + +class StrictModeTest(PluginTestCase): + """html=False, spa=False 时行为与 StaticFiles 严格一致。""" + + html = False + spa = False + + def test_root_is_404(self): + self.assertEqual(404, self.get("/").status_code) + + def test_route_is_404(self): + self.assertEqual(404, self.get("/settings", headers={"accept": "text/html"}).status_code) + + def test_plain_file_still_served(self): + self.assertEqual(200, self.get("/app.js").status_code) + + +class ConditionalRequestTest(PluginTestCase): + def test_etag_and_last_modified_present(self): + response = self.get("/app.js") + self.assertIn("etag", response.headers) + self.assertIn("last-modified", response.headers) + self.assertEqual("bytes", response.headers["accept-ranges"]) + + def test_if_none_match_returns_304(self): + etag = self.get("/app.js").headers["etag"] + response = self.client.get("/app.js", headers={"if-none-match": etag}) + self.assertEqual(304, response.status_code) + self.assertEqual(b"", response.content) + + def test_if_none_match_with_weak_prefix(self): + etag = self.get("/app.js").headers["etag"] + response = self.client.get("/app.js", headers={"if-none-match": f"W/{etag}"}) + self.assertEqual(304, response.status_code) + + def test_if_modified_since_returns_304(self): + last_modified = self.get("/app.js").headers["last-modified"] + response = self.client.get( + "/app.js", headers={"if-modified-since": last_modified} + ) + self.assertEqual(304, response.status_code) + + def test_stale_if_none_match_returns_full_body(self): + response = self.client.get("/app.js", headers={"if-none-match": '"stale"'}) + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["app.js"], response.content) + + def test_etag_is_stable_across_instances(self): + """同一个资源包重新打开,ETag 不能变,否则缓存永远失效。""" + other = SVFSStaticFiles(self.db) + self.addCleanup(other.close) + app = FastAPI() + app.mount("/", other, name="static") + client = TestClient(app) + self.assertEqual( + self.get("/app.js").headers["etag"], client.get("/app.js").headers["etag"] + ) + + def test_max_age_sets_cache_control(self): + statics = SVFSStaticFiles(self.db, max_age=3600) + self.addCleanup(statics.close) + app = FastAPI() + app.mount("/", statics, name="static") + response = TestClient(app).get("/app.js") + self.assertEqual("public, max-age=3600", response.headers["cache-control"]) + + +class HeadAndRangeTest(PluginTestCase): + def test_head_has_no_body_but_real_length(self): + response = self.client.head("/big.txt") + self.assertEqual(200, response.status_code) + self.assertEqual(b"", response.content) + self.assertEqual(str(len(ASSETS["big.txt"])), response.headers["content-length"]) + + def test_range_returns_206_with_slice(self): + response = self.client.get("/big.txt", headers={"range": "bytes=0-9"}) + self.assertEqual(206, response.status_code) + self.assertEqual(ASSETS["big.txt"][:10], response.content) + self.assertEqual( + f"bytes 0-9/{len(ASSETS['big.txt'])}", response.headers["content-range"] + ) + self.assertEqual("10", response.headers["content-length"]) + + def test_range_with_open_end(self): + response = self.client.get("/big.txt", headers={"range": "bytes=100-"}) + self.assertEqual(206, response.status_code) + self.assertEqual(ASSETS["big.txt"][100:], response.content) + + def test_suffix_range(self): + response = self.client.get("/big.txt", headers={"range": "bytes=-16"}) + self.assertEqual(206, response.status_code) + self.assertEqual(ASSETS["big.txt"][-16:], response.content) + + def test_range_on_compressed_content_is_correct(self): + """库里存的是压缩数据,Range 必须作用在解压后的内容上。""" + for start, end in ((0, 0), (15, 31), (len(ASSETS["big.txt"]) - 5, 999999)): + with self.subTest(start=start, end=end): + response = self.client.get( + "/big.txt", headers={"range": f"bytes={start}-{end}"} + ) + self.assertEqual(206, response.status_code) + self.assertEqual(ASSETS["big.txt"][start : end + 1], response.content) + + def test_unsatisfiable_range_returns_416(self): + response = self.client.get( + "/big.txt", headers={"range": f"bytes={len(ASSETS['big.txt'])}-"} + ) + self.assertEqual(416, response.status_code) + self.assertEqual( + f"bytes */{len(ASSETS['big.txt'])}", response.headers["content-range"] + ) + + def test_malformed_range_is_ignored(self): + """语法错误的 Range 按 RFC 应当被忽略,返回整份内容。""" + for header in ("bytes=abc-def", "items=0-10", "", "bytes="): + with self.subTest(header=header): + response = self.client.get("/big.txt", headers={"range": header}) + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["big.txt"], response.content) + + def test_multi_range_falls_back_to_full_body(self): + response = self.client.get("/big.txt", headers={"range": "bytes=0-1,4-5"}) + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["big.txt"], response.content) + + def test_if_range_mismatch_ignores_range(self): + response = self.client.get( + "/big.txt", headers={"range": "bytes=0-9", "if-range": '"stale"'} + ) + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["big.txt"], response.content) + + def test_if_range_match_uses_range(self): + etag = self.get("/big.txt").headers["etag"] + response = self.client.get( + "/big.txt", headers={"range": "bytes=0-9", "if-range": etag} + ) + self.assertEqual(206, response.status_code) + + def test_head_with_range(self): + response = self.client.head("/big.txt", headers={"range": "bytes=0-9"}) + self.assertEqual(206, response.status_code) + self.assertEqual(b"", response.content) + self.assertEqual("10", response.headers["content-length"]) + + +class CompressionAlgorithmServingTest(PluginTestCase): + """资源包用哪种算法压缩,对使用方必须完全透明。""" + + def test_pack_uses_zlib_by_default(self): + import sqlite3 + + conn = sqlite3.connect(f"file:{self.db}?mode=ro", uri=True) + try: + algorithms = { + row[0] + for row in conn.execute( + """ + SELECT b.compress_algo + FROM files f JOIN blobs b ON b.hash = f.content_hash + WHERE f.is_directory = 0 + """ + ) + } + finally: + conn.close() + self.assertIn("zlib", algorithms) + self.assertNotIn("huffman", algorithms) + + def test_large_asset_survives_compressed_roundtrip(self): + response = self.get("/big.txt") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["big.txt"], response.content) + self.assertEqual(str(len(ASSETS["big.txt"])), response.headers["content-length"]) + + +class HuffmanPackServingTest(PluginTestCase): + """早期版本用 huffman 打的资源包也必须能正常服务(向后兼容)。""" + + algorithm = "huffman" + + def test_serves_huffman_compressed_asset(self): + response = self.get("/big.txt") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["big.txt"], response.content) + + def test_range_works_on_huffman_pack(self): + response = self.client.get("/big.txt", headers={"range": "bytes=0-9"}) + self.assertEqual(206, response.status_code) + self.assertEqual(ASSETS["big.txt"][:10], response.content) + + def test_conditional_request_works_on_huffman_pack(self): + etag = self.get("/big.txt").headers["etag"] + response = self.client.get("/big.txt", headers={"if-none-match": etag}) + self.assertEqual(304, response.status_code) + + +class MountPrefixTest(PluginTestCase): + """挂在子路径下(app.mount("/static", ...))也要正常工作。""" + + def setUp(self): + super().setUp() + app = FastAPI() + app.mount("/static", self.statics, name="static") + self.client = TestClient(app, follow_redirects=False) + + def test_serves_under_prefix(self): + response = self.get("/static/app.js") + self.assertEqual(200, response.status_code) + self.assertEqual(ASSETS["app.js"], response.content) + + def test_outside_prefix_is_404(self): + self.assertEqual(404, self.get("/app.js").status_code) + + +class ConstructionTest(NeedsFastAPI): + def test_missing_pack_raises_runtime_error_with_hint(self): + missing = self.path("nope.svfs") + with self.assertRaises(RuntimeError) as ctx: + SVFSStaticFiles(missing) + self.assertIn("vfspack", str(ctx.exception)) + + def test_old_format_pack_is_rejected_clearly(self): + from sqlite_vfs.core import VFSFormatError + + db = helpers.make_old_schema_db(self.path("legacy.svfs")) + with self.assertRaises(VFSFormatError): + SVFSStaticFiles(db) + + def test_non_database_file_is_rejected(self): + junk = helpers.write_file(self.path("junk.svfs"), b"not a database") + with self.assertRaises(RuntimeError): + SVFSStaticFiles(junk) + + def test_repr_and_close(self): + src = helpers.make_tree(self.path("d"), {"a.txt": b"a"}) + db = helpers.pack(src, self.path("s.svfs")) + statics = SVFSStaticFiles(db) + self.assertIn("SVFSStaticFiles", repr(statics)) + statics.close() + # 关闭后再访问会报错,而不是静默返回空内容 + with self.assertRaises(Exception): + statics.lookup_path("/a.txt") + + +class InterruptedReadTest(NeedsFastAPI): + """返回 404/304 时不能读取 BLOB(这也是条件请求省下来的开销)。""" + + def test_304_does_not_read_content(self): + src = helpers.make_tree(self.path("d"), {"a.txt": b"aaa"}) + db = helpers.pack(src, self.path("s.svfs")) + statics = SVFSStaticFiles(db) + self.addCleanup(statics.close) + + calls = [] + original = statics._read + + def counting_read(*args, **kwargs): + calls.append(args) + return original(*args, **kwargs) + + statics._read = counting_read + + app = FastAPI() + app.mount("/", statics, name="static") + client = TestClient(app) + + first = client.get("/a.txt") + self.assertEqual(200, first.status_code) + self.assertEqual(1, len(calls)) + + etag = first.headers["etag"] + not_modified = client.get("/a.txt", headers={"if-none-match": etag}) + self.assertEqual(304, not_modified.status_code) + self.assertEqual(1, len(calls), "304 响应不应该再去读 BLOB") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_huffman.py b/tests/test_huffman.py new file mode 100644 index 0000000..add65eb --- /dev/null +++ b/tests/test_huffman.py @@ -0,0 +1,130 @@ +"""huffman.py 的往返测试。 + +这个模块是独立的、只有标准库依赖的算法实现,理论上应当全绿; +它在测试里的作用是:当打包/解包链路出问题时,可以快速排除"压缩算法本身写错了" +这一可能,把问题范围锁到 core.py 的集成层。 +""" + +import random +import struct +import unittest + +import helpers # noqa: F401 -- 把 src 加入 sys.path + +from sqlite_vfs.huffman import ( + FREQ_SIZE, + MAGIC, + compress_bytes, + decompress_bytes, +) + + +class RoundTripTest(unittest.TestCase): + def assert_roundtrip(self, data, label=""): + freq_blob, compressed_blob = compress_bytes(data) + + self.assertEqual( + FREQ_SIZE, len(freq_blob), f"{label}: 频率表必须是 256 个 uint32" + ) + self.assertEqual(MAGIC, compressed_blob[:4], f"{label}: 魔数不正确") + self.assertEqual( + len(data), + struct.unpack(">Q", compressed_blob[4:12])[0], + f"{label}: 头部记录的原长度不正确", + ) + self.assertEqual( + data, decompress_bytes(freq_blob, compressed_blob), f"{label}: 还原结果不一致" + ) + return compressed_blob + + def test_empty(self): + freq_blob, compressed_blob = compress_bytes(b"") + self.assertEqual(FREQ_SIZE, len(freq_blob)) + self.assertEqual(b"", decompress_bytes(freq_blob, compressed_blob)) + + def test_single_byte(self): + self.assert_roundtrip(b"\x00", "单字节") + self.assert_roundtrip(b"Z", "单字节") + + def test_single_symbol_repeated(self): + self.assert_roundtrip(b"\x7f" * 5000, "单符号重复") + + def test_two_symbols(self): + self.assert_roundtrip(b"ab" * 3000, "双符号") + + def test_all_256_byte_values(self): + self.assert_roundtrip(bytes(range(256)), "全字节值") + self.assert_roundtrip(bytes(range(256)) * 40, "全字节值重复") + + def test_incompressible_random(self): + data = random.Random(1).randbytes(20000) + compressed_blob = self.assert_roundtrip(data, "随机数据") + self.assertGreater( + len(compressed_blob), + len(data), + "随机数据的压缩结果应当大于原始数据(这正是 core 会回退到 none 的原因)", + ) + + def test_repetitive_text_compresses(self): + data = b"the quick brown fox jumps over the lazy dog. " * 2000 + compressed_blob = self.assert_roundtrip(data, "重复文本") + # 该文本有 20 个不同字符,理论码长 ~4.5 bit/字符,压缩后约为原始的 56% + self.assertLess( + len(compressed_blob), + int(len(data) * 0.7), + f"高度重复的文本应当被显著压缩,实际 {len(compressed_blob)}/{len(data)}", + ) + + def test_utf8_chinese(self): + self.assert_roundtrip("中文内容,测试哈夫曼编码是否无损。\n".encode("utf-8") * 200, "中文") + + def test_binary_with_nulls_and_high_bytes(self): + data = bytes([0, 255, 0, 128, 64, 0, 7]) * 500 + self.assert_roundtrip(data, "二进制含 0x00/0xFF") + + def test_just_above_core_threshold(self): + # core.add_file 的压缩阈值是 1024 字节,边界两侧都要能正确往返 + self.assert_roundtrip(b"A" * 1024, "1024 字节") + self.assert_roundtrip(b"A" * 1025, "1025 字节") + + +class HeaderValidationTest(unittest.TestCase): + def setUp(self): + # 刻意用多符号数据:单符号数据在解压时走快捷路径,不读 payload + self.freq_blob, self.compressed_blob = compress_bytes( + b"the quick brown fox. " * 500 + ) + + def test_wrong_freq_blob_size_rejected(self): + with self.assertRaises(ValueError) as ctx: + decompress_bytes(self.freq_blob[:-4], self.compressed_blob) + self.assertIn("频率表", str(ctx.exception)) + + def test_too_short_blob_rejected(self): + with self.assertRaises(ValueError) as ctx: + decompress_bytes(self.freq_blob, b"HUF") + self.assertIn("太短", str(ctx.exception)) + + def test_bad_magic_rejected(self): + broken = b"XXXX" + self.compressed_blob[4:] + with self.assertRaises(ValueError) as ctx: + decompress_bytes(self.freq_blob, broken) + self.assertIn("魔数", str(ctx.exception)) + + def test_truncated_payload_rejected(self): + truncated = self.compressed_blob[:-50] + with self.assertRaises(ValueError) as ctx: + decompress_bytes(self.freq_blob, truncated) + self.assertIn("提前结束", str(ctx.exception)) + + def test_single_symbol_payload_is_not_validated(self): + """记录既有行为(非缺陷):单符号数据解压时直接按原长度展开,完全不读 payload, + 因此 payload 被截断也检测不出来。""" + freq_blob, compressed_blob = compress_bytes(b"A" * 10000) + self.assertEqual( + b"A" * 10000, decompress_bytes(freq_blob, compressed_blob[:20]) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_incremental.py b/tests/test_incremental.py new file mode 100644 index 0000000..0b7f522 --- /dev/null +++ b/tests/test_incremental.py @@ -0,0 +1,337 @@ +"""增量打包。 + +约定: +* 默认按 ``mtime + size`` 判定变化(不读文件内容,快); +* ``rehash=True`` 时按 sha256 逐文件比对(可靠,但要把文件读一遍); +* 磁盘上消失的路径会被删掉,对应的 blob 也会被回收; +* 内容没变的文件不会被重新压缩或重复存储(去重的附带收益)。 +""" + +import hashlib +import os +import time +import unittest + +import helpers + +from sqlite_vfs.core import SQLiteVFS +from sqlite_vfs.folder_packer import FolderPacker + + +class IncrementalTestBase(helpers.VFSTestCase): + def setUp(self): + super().setUp() + self.src = helpers.make_tree( + self.path("src"), + {"a.txt": b"aaa", "b.txt": b"bbb", "sub/c.txt": b"ccc"}, + ) + self.db = self.path("inc.svfs") + self.pack(incremental=False) + + def pack_with(self, vfs, **kwargs): + """用给定的 vfs 打包,并把统计与输出留下来。""" + packer = FolderPacker(vfs, fs_name="inc") + with helpers.quiet() as output: + ok = packer.pack_folder(self.src, **kwargs) + self.stats = packer.last_stats + self.output = output.getvalue() + return ok + + def pack(self, **kwargs): + vfs = SQLiteVFS(self.db, compress=True) + try: + return self.pack_with(vfs, **kwargs) + finally: + vfs.close() + + def read(self, path): + vfs = SQLiteVFS(self.db) + try: + return vfs.read_file(path) + finally: + vfs.close() + + def blob_hashes(self): + vfs = SQLiteVFS(self.db) + try: + return {row["hash"] for row in vfs.conn.execute("SELECT hash FROM blobs")} + finally: + vfs.close() + + def hash_for(self, path): + vfs = SQLiteVFS(self.db) + try: + return vfs.get_blob_info(path)["hash"] + finally: + vfs.close() + + def touch_later(self, path, data): + """改文件内容并把 mtime 推到 1 秒之后,避免同秒同大小的歧义。""" + helpers.write_file(path, data) + future = time.time() + 2 + os.utime(path, (future, future)) + + +class ChangeDetectionTest(IncrementalTestBase): + def test_unchanged_tree_writes_nothing(self): + self.pack(incremental=True) + self.assertEqual( + {"added": 0, "updated": 0, "unchanged": 3, "removed": 0}, + {k: self.stats[k] for k in ("added", "updated", "unchanged", "removed")}, + ) + self.assertIn("未变 3", self.output) + + def test_modified_file_is_updated(self): + self.touch_later(os.path.join(self.src, "a.txt"), b"aaa-changed") + self.pack(incremental=True) + self.assertEqual(1, self.stats["updated"]) + self.assertEqual(2, self.stats["unchanged"]) + self.assertEqual(b"aaa-changed", self.read("a.txt")) + + def test_new_file_is_added(self): + helpers.write_file(os.path.join(self.src, "new.txt"), b"new") + self.pack(incremental=True) + self.assertEqual(1, self.stats["added"]) + self.assertEqual(b"new", self.read("new.txt")) + + def test_deleted_file_is_removed(self): + os.remove(os.path.join(self.src, "b.txt")) + self.pack(incremental=True) + self.assertEqual(1, self.stats["removed"]) + + vfs = SQLiteVFS(self.db) + self.addCleanup(vfs.close) + self.assertFalse(vfs.exists("b.txt")) + self.assertEqual(2, helpers.count_files(vfs)) + + def test_new_directory_is_created(self): + helpers.make_tree(self.src, {"fresh/deep/x.txt": b"x"}) + self.pack(incremental=True) + vfs = SQLiteVFS(self.db) + self.addCleanup(vfs.close) + self.assertTrue(vfs.is_dir("fresh")) + self.assertTrue(vfs.is_dir("fresh/deep")) + self.assertEqual(b"x", vfs.read_file("fresh/deep/x.txt")) + + def test_deleted_directory_is_removed(self): + import shutil + + shutil.rmtree(os.path.join(self.src, "sub")) + self.pack(incremental=True) + vfs = SQLiteVFS(self.db) + self.addCleanup(vfs.close) + self.assertFalse(vfs.exists("sub")) + self.assertFalse(vfs.exists("sub/c.txt")) + + def test_file_replaced_by_directory(self): + """同一路径从文件变成目录时必须换掉类型,而不是留着旧条目。""" + os.remove(os.path.join(self.src, "a.txt")) + helpers.make_tree(self.src, {"a.txt": None, "a.txt/inner.txt": b"inner"}) + self.pack(incremental=True) + vfs = SQLiteVFS(self.db) + self.addCleanup(vfs.close) + self.assertTrue(vfs.is_dir("a.txt")) + self.assertEqual(b"inner", vfs.read_file("a.txt/inner.txt")) + + def test_mixed_changes_in_one_run(self): + self.touch_later(os.path.join(self.src, "a.txt"), b"aaa-v2") + helpers.write_file(os.path.join(self.src, "new.txt"), b"new") + os.remove(os.path.join(self.src, "b.txt")) + self.pack(incremental=True) + self.assertEqual(1, self.stats["added"]) + self.assertEqual(1, self.stats["updated"]) + self.assertEqual(1, self.stats["unchanged"]) + self.assertEqual(1, self.stats["removed"]) + + def test_result_still_verifies(self): + self.touch_later(os.path.join(self.src, "a.txt"), b"aaa-v2") + helpers.write_file(os.path.join(self.src, "new.txt"), b"new") + os.remove(os.path.join(self.src, "b.txt")) + self.pack(incremental=True) + + vfs = SQLiteVFS(self.db) + self.addCleanup(vfs.close) + report = vfs.verify(check_content=True) + self.assertTrue(report["ok"], report["errors"]) + self.assertEqual(3, report["files_checked"]) + + +class NoRewriteTest(IncrementalTestBase): + """未变的文件不能被重写、重压或重新存储。""" + + def test_unchanged_file_keeps_its_blob_hash(self): + before = self.hash_for("sub/c.txt") + self.touch_later(os.path.join(self.src, "a.txt"), b"aaa-v2") + self.pack(incremental=True) + self.assertEqual(before, self.hash_for("sub/c.txt")) + + def test_unchanged_files_do_not_add_blobs(self): + before = self.blob_hashes() + self.pack(incremental=True) + self.assertEqual(before, self.blob_hashes()) + + def test_dedup_reuses_blob_for_identical_new_file(self): + """新路径但内容与已有文件相同 → 直接复用,不新增 blob。""" + before = self.blob_hashes() + helpers.write_file(os.path.join(self.src, "copy_of_c.txt"), b"ccc") + self.pack(incremental=True) + self.assertEqual(1, self.stats["added"]) + self.assertEqual(before, self.blob_hashes(), "内容相同,不该新增 blob") + + def test_replacing_content_frees_the_old_blob(self): + self.touch_later(os.path.join(self.src, "a.txt"), b"completely different") + self.pack(incremental=True) + vfs = SQLiteVFS(self.db) + self.addCleanup(vfs.close) + self.assertEqual(3, helpers.blob_count(vfs), "旧的 aaa 已无人引用,应当回收") + self.assertEqual(b"completely different", vfs.read_file("a.txt")) + + def test_rehash_skips_files_whose_content_is_identical(self): + """mtime 变了但内容没变:rehash 模式应当判为未变,不做任何写入。""" + path = os.path.join(self.src, "a.txt") + with open(path, "rb") as f: + original = f.read() + self.touch_later(path, original) # 只改 mtime + + before = self.hash_for("a.txt") + self.pack(incremental=True, rehash=True) + self.assertEqual(3, self.stats["unchanged"]) + self.assertEqual(0, self.stats["updated"]) + self.assertEqual(before, self.hash_for("a.txt")) + + +class RehashDetectionTest(IncrementalTestBase): + def test_mtime_only_change_is_detected_without_rehash(self): + """同大小 + 同秒修改:默认模式可能漏掉,这正是 rehash 存在的理由。""" + path = os.path.join(self.src, "a.txt") + helpers.write_file(path, b"AAAA") # 与原始 aaa 大小不同,先制造同大小场景 + self.pack(incremental=True) + + # 现在 disk 与库里都是 AAAA;把内容换成同样长度的别的字节, + # 并把 mtime 强制设回库里记录的那个值 + vfs = SQLiteVFS(self.db) + recorded = vfs.get_file_info("a.txt")["modified_time"].timestamp() + vfs.close() + helpers.write_file(path, b"BBBB") + os.utime(path, (recorded, recorded)) + + self.pack(incremental=True) + self.assertEqual(3, self.stats["unchanged"], "mtime+size 相同,默认模式看不出来") + self.assertEqual(b"AAAA", self.read("a.txt"), "库里还是旧内容(预期的漏检)") + + def test_rehash_catches_same_size_same_mtime_edit(self): + path = os.path.join(self.src, "a.txt") + helpers.write_file(path, b"AAAA") + self.pack(incremental=True) + + vfs = SQLiteVFS(self.db) + recorded = vfs.get_file_info("a.txt")["modified_time"].timestamp() + vfs.close() + helpers.write_file(path, b"BBBB") + os.utime(path, (recorded, recorded)) + + self.pack(incremental=True, rehash=True) + self.assertEqual(1, self.stats["updated"], "rehash 必须抓到这次修改") + self.assertEqual(b"BBBB", self.read("a.txt")) + + +class IncrementalSnapshotTest(IncrementalTestBase): + def test_first_incremental_on_an_empty_database(self): + db = self.path("fresh.svfs") + vfs = SQLiteVFS(db, compress=True) + try: + packer = FolderPacker(vfs, fs_name="fresh") + with helpers.quiet(): + self.assertTrue(packer.pack_folder(self.src, incremental=True)) + finally: + vfs.close() + + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual(3, helpers.count_files(vfs)) + self.assertEqual("fresh", vfs.list_snapshots()[0]["name"]) + + def test_incremental_into_a_new_snapshot_keeps_history(self): + """派生新快照后再增量:旧版本原样保留,新版本只有变化的部分被重写。""" + vfs = SQLiteVFS(self.db, compress=True) + try: + parent = vfs._active_snapshot() + vfs.create_snapshot("v2", parent_id=parent, copy_entries=True) + self.touch_later(os.path.join(self.src, "a.txt"), b"aaa-v2") + os.remove(os.path.join(self.src, "b.txt")) + self.assertTrue(self.pack_with(vfs, incremental=True)) + finally: + vfs.close() + self.assertEqual(1, self.stats["updated"]) + self.assertEqual(1, self.stats["removed"]) + + vfs = SQLiteVFS(self.db) + self.addCleanup(vfs.close) + self.assertEqual(2, len(vfs.list_snapshots())) + self.assertEqual(b"aaa-v2", vfs.read_file("a.txt")) + self.assertFalse(vfs.exists("b.txt")) + + vfs.use_snapshot(1) + self.assertEqual(b"aaa", vfs.read_file("a.txt"), "旧版本必须原样保留") + self.assertEqual(b"bbb", vfs.read_file("b.txt")) + + def test_incremental_does_not_touch_other_snapshots(self): + vfs = SQLiteVFS(self.db, compress=True) + try: + parent = vfs._active_snapshot() + vfs.create_snapshot("v2", parent_id=parent, copy_entries=True) + os.remove(os.path.join(self.src, "b.txt")) + self.pack_with(vfs, incremental=True) + finally: + vfs.close() + + vfs = SQLiteVFS(self.db, snapshot_id=1) + self.addCleanup(vfs.close) + self.assertTrue(vfs.exists("b.txt"), "v1 不该被 v2 的增量影响") + + +class IncrementalCliTest(helpers.VFSTestCase): + def test_cli_incremental(self): + src = helpers.make_tree(self.path("src"), {"a.txt": b"aaa", "b.txt": b"bbb"}) + db = self.path("cli.svfs") + first = helpers.run_cli("sqlite_vfs.cli.packer", src, db, "--compress") + self.assertEqual(0, first.returncode, helpers.cli_output(first)) + + helpers.write_file(os.path.join(src, "new.txt"), b"new") + os.remove(os.path.join(src, "b.txt")) + second = helpers.run_cli("sqlite_vfs.cli.packer", src, db, "--incremental") + self.assertEqual(0, second.returncode, helpers.cli_output(second)) + self.assertIn("增量结果", second.stdout) + self.assertIn("新增 1", second.stdout) + self.assertIn("删除 1", second.stdout) + + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual(b"new", vfs.read_file("new.txt")) + self.assertFalse(vfs.exists("b.txt")) + + def test_cli_snapshot_creates_a_new_version(self): + src = helpers.make_tree(self.path("src"), {"a.txt": b"aaa"}) + db = self.path("cli2.svfs") + helpers.run_cli("sqlite_vfs.cli.packer", src, db, "--compress") + + helpers.write_file(os.path.join(src, "a.txt"), b"aaa-v2") + result = helpers.run_cli( + "sqlite_vfs.cli.packer", src, db, "--snapshot", "v2", "--incremental" + ) + self.assertEqual(0, result.returncode, helpers.cli_output(result)) + + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual([1, 2], [s["id"] for s in vfs.list_snapshots()]) + self.assertEqual(b"aaa-v2", vfs.read_file("a.txt")) + vfs.use_snapshot(1) + self.assertEqual(b"aaa", vfs.read_file("a.txt")) + + listing = helpers.run_cli("sqlite_vfs.cli.unpacker", db, self.path("out"), "--list-snapshots") + self.assertEqual(0, listing.returncode, helpers.cli_output(listing)) + self.assertIn("v2", listing.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_integrity.py b/tests/test_integrity.py new file mode 100644 index 0000000..b6ef0f2 --- /dev/null +++ b/tests/test_integrity.py @@ -0,0 +1,324 @@ +"""完整性:每文件 sha256、快照级摘要、HMAC 签名。 + +设计上的一个重要区分: +* ``verify()`` 是**元数据级**校验(摘要、blob 是否存在、大小是否一致),很便宜; +* ``verify(check_content=True)`` 才会逐文件重算 sha256,是**权威**校验, + 代价是 O(总字节数)。同大小的内容篡改只有后者能发现。 +""" + +import json +import sqlite3 +import unittest + +import helpers + +from sqlite_vfs.core import SQLiteVFS, VFSIntegrityError + + +class DigestTest(helpers.VFSTestCase): + def _pack(self, name="d.svfs", tree=None): + src = helpers.make_tree( + self.path(f"src_{name}"), + tree or {"a.txt": b"aaa", "sub/b.txt": b"bbb"}, + ) + db = self.path(name) + helpers.pack(src, db, compress=True, name="digest-pack") + return db + + def test_digest_is_recorded_and_stable_across_reopen(self): + db = self._pack() + vfs = SQLiteVFS(db) + first = vfs.get_stats()["digest"] + self.assertTrue(first) + vfs.close() + + reopened = SQLiteVFS(db) + self.addCleanup(reopened.close) + self.assertEqual(first, reopened.get_stats()["digest"]) + + def test_digest_matches_manual_recomputation(self): + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + stored = vfs.conn.execute("SELECT digest FROM snapshots WHERE id = 1").fetchone()[0] + self.assertEqual(stored, vfs.compute_digest(1, store=False)) + + def test_digest_changes_when_a_file_is_added(self): + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + before = vfs.compute_digest(1) + + extra = helpers.write_file(self.path("extra.txt"), b"extra") + with helpers.quiet(): + vfs.add_file(extra, "extra.txt") + vfs.recount() + self.assertNotEqual(before, vfs.compute_digest(1)) + + def test_digest_changes_when_content_changes(self): + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + before = vfs.compute_digest(1) + + vfs.remove("a.txt") + changed = helpers.write_file(self.path("changed.txt"), b"zzz") + with helpers.quiet(): + vfs.add_file(changed, "a.txt") + vfs.recount() + self.assertNotEqual(before, vfs.compute_digest(1)) + + def test_digest_changes_when_metadata_changes(self): + """路径/大小/时间戳/权限也参与摘要,不只是内容。""" + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + before = vfs.compute_digest(1) + + vfs.conn.execute("UPDATE files SET permissions = 511 WHERE path = 'a.txt'") + vfs.conn.commit() + self.assertNotEqual(before, vfs.compute_digest(1)) + + def test_digest_ignores_insertion_order(self): + """摘要必须只取决于内容与元数据,与插入顺序无关。 + + 用 seed_rows 造数据(时间戳为 NULL),这样两个库的元数据完全一致, + 唯一的差别就是插入顺序。 + """ + rows = [ + {"path": "a.txt", "content": b"a"}, + {"path": "b.txt", "content": b"b"}, + {"path": "c.txt", "content": b"c"}, + ] + digests = [] + for index, ordered in enumerate((list(rows), list(reversed(rows)))): + vfs = SQLiteVFS(self.path(f"order{index}.svfs")) + self.addCleanup(vfs.close) + helpers.seed_rows(vfs, ordered) + digests.append(vfs.compute_digest(1)) + self.assertEqual(digests[0], digests[1]) + + +class VerifyTest(helpers.VFSTestCase): + def _pack(self, name="v.svfs", compress=True): + src = helpers.make_tree( + self.path(f"src_{name}"), + {"a.txt": b"aaa", "big.txt": b"repeat me " * 300, "sub/b.txt": b"bbb"}, + ) + db = self.path(name) + helpers.pack(src, db, compress=compress, name="verify-pack") + return db + + def test_verify_passes_on_a_fresh_pack(self): + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + report = vfs.verify() + self.assertTrue(report["ok"], report["errors"]) + self.assertTrue(report["digest_ok"]) + self.assertEqual(3, report["files_checked"]) + self.assertEqual(0, report["content_checked"], "默认不做逐文件哈希") + + def test_verify_with_content_check_passes(self): + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + report = vfs.verify(check_content=True) + self.assertTrue(report["ok"], report["errors"]) + self.assertEqual(3, report["content_checked"]) + + def test_verify_reports_empty_database(self): + vfs = SQLiteVFS(":memory:") + self.addCleanup(vfs.close) + report = vfs.verify() + self.assertFalse(report["ok"]) + self.assertIn("没有", " ".join(report["errors"])) + + def test_content_tamper_is_caught_by_check_content(self): + """同大小改掉内容:元数据级校验看不出来,权威校验必须抓到。""" + db = self._pack(compress=False) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + + vfs.conn.execute("UPDATE blobs SET content = X'626262' WHERE size = 3") + vfs.conn.commit() + + # 元数据级:摘要与大小都没变,所以看不出来 + self.assertTrue(vfs.verify()["ok"]) + + report = vfs.verify(check_content=True) + self.assertFalse(report["ok"]) + self.assertTrue(any("内容哈希不匹配" in e for e in report["errors"]), report) + + def test_read_file_with_verify_hash_raises_on_tamper(self): + db = self._pack(compress=False) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + vfs.conn.execute("UPDATE blobs SET content = X'626262' WHERE size = 3") + vfs.conn.commit() + + self.assertEqual(b"bbb", vfs.read_file("a.txt")) # 不校验时照旧返回 + with self.assertRaises(VFSIntegrityError): + vfs.read_file("a.txt", verify_hash=True) + + def test_missing_blob_is_caught(self): + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + vfs.conn.execute("DELETE FROM blobs WHERE hash = (SELECT content_hash FROM files WHERE path='a.txt')") + vfs.conn.commit() + + report = vfs.verify() + self.assertFalse(report["ok"]) + self.assertTrue(any("blob 缺失" in e for e in report["errors"]), report) + with self.assertRaises(VFSIntegrityError): + vfs.read_file("a.txt") + + def test_size_mismatch_is_caught(self): + db = self._pack(compress=False) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + vfs.conn.execute("UPDATE blobs SET size = size + 1") + vfs.conn.commit() + + report = vfs.verify() + self.assertFalse(report["ok"]) + self.assertTrue(any("大小不一致" in e for e in report["errors"]), report) + with self.assertRaises(VFSIntegrityError): + vfs.read_file("a.txt") + + def test_metadata_tamper_is_caught_by_digest(self): + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + vfs.conn.execute("UPDATE files SET file_size = 999 WHERE path = 'a.txt'") + vfs.conn.commit() + + report = vfs.verify() + self.assertFalse(report["ok"]) + self.assertFalse(report["digest_ok"]) + self.assertTrue(any("摘要不一致" in e for e in report["errors"]), report) + + def test_path_tamper_is_caught_by_digest(self): + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + vfs.conn.execute("UPDATE files SET path = 'renamed.txt' WHERE path = 'a.txt'") + vfs.conn.commit() + self.assertFalse(vfs.verify()["digest_ok"]) + + def test_verify_can_target_an_older_snapshot(self): + """摘要防的是"绕过 API 的改动";走 API 的写入会刷新摘要,不算篡改。""" + db = self._pack() + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + vfs.create_snapshot("v2", copy_entries=True) + vfs.remove("a.txt") + vfs.recount() + + # 走 API 删文件之后 recount 会刷新摘要,所以两个快照此刻都是自洽的 + self.assertTrue(vfs.verify(snapshot_id=2)["ok"]) + self.assertTrue(vfs.verify(snapshot_id=1)["ok"]) + + # 绕过 API 直接改 v2 的元数据 → 只有 v2 报错 + vfs.conn.execute("UPDATE files SET file_size = 1 WHERE snapshot_id = 2") + vfs.conn.commit() + self.assertFalse(vfs.verify(snapshot_id=2)["digest_ok"]) + self.assertTrue( + vfs.verify(snapshot_id=1)["ok"], "v1 没被碰过,不该受影响" + ) + + +class SignatureTest(helpers.VFSTestCase): + def _pack(self, name="sig.svfs"): + src = helpers.make_tree(self.path("src"), {"a.txt": b"aaa", "b.txt": b"bbb"}) + db = self.path(name) + helpers.pack(src, db, compress=True, name="signed-pack") + return db + + def test_sign_and_verify(self): + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + signature = vfs.sign("top-secret", key_id="release-1") + self.assertEqual(64, len(signature), "sha256 十六进制应当是 64 个字符") + + report = vfs.verify_signature("top-secret") + self.assertTrue(report["ok"], report["errors"]) + self.assertEqual("release-1", report["key_id"]) + self.assertEqual("hmac-sha256", vfs.list_signatures()[0]["algorithm"]) + + def test_wrong_key_fails(self): + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + vfs.sign("right-key") + report = vfs.verify_signature("wrong-key") + self.assertFalse(report["ok"]) + self.assertTrue(report["errors"]) + + def test_tampering_after_signing_fails(self): + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + vfs.sign("k") + + extra = helpers.write_file(self.path("evil.txt"), b"evil") + with helpers.quiet(): + vfs.add_file(extra, "evil.txt") + vfs.recount() + + self.assertFalse( + vfs.verify_signature("k")["ok"], "签名之后内容变了,验签必须失败" + ) + + def test_verify_without_a_signature_is_reported(self): + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + report = vfs.verify_signature("k") + self.assertFalse(report["ok"]) + self.assertIn("没有签名记录", " ".join(report["errors"])) + + def test_verify_signature_by_key_id(self): + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + vfs.sign("k1", key_id="one") + vfs.sign("k2", key_id="two") + self.assertTrue(vfs.verify_signature("k1", key_id="one")["ok"]) + self.assertTrue(vfs.verify_signature("k2", key_id="two")["ok"]) + self.assertFalse(vfs.verify_signature("k1", key_id="two")["ok"]) + + def test_signature_verifies_on_a_readonly_connection(self): + """只读方必须能在不改动库的前提下完成验证。""" + db = self._pack() + vfs = SQLiteVFS(db) + vfs.sign("shared-secret", key_id="audit") + vfs.close() + + before = helpers.md5_of(db) + readonly = SQLiteVFS(db, readonly=True) + self.addCleanup(readonly.close) + + report = readonly.verify_signature("shared-secret") + self.assertTrue(report["ok"], report["errors"]) + self.assertTrue(readonly.verify(check_content=True)["ok"]) + self.assertEqual(before, helpers.md5_of(db), "只读验证改动了库文件") + + def test_sign_requires_write_access(self): + db = self._pack() + readonly = SQLiteVFS(db, readonly=True) + self.addCleanup(readonly.close) + with self.assertRaises(PermissionError): + readonly.sign("k") + + def test_digest_can_be_exported_for_external_signing(self): + """非对称签名要交给外部工具,所以摘要必须能干净地取出来。""" + vfs = SQLiteVFS(self._pack()) + self.addCleanup(vfs.close) + payload = { + "digest": vfs.compute_digest(1, store=False), + "snapshot_id": 1, + "algorithm": "sha256", + } + text = json.dumps(payload, sort_keys=True) + self.assertIn("digest", text) + self.assertEqual(64, len(payload["digest"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_migration.py b/tests/test_migration.py new file mode 100644 index 0000000..2759d6a --- /dev/null +++ b/tests/test_migration.py @@ -0,0 +1,327 @@ +"""v1 / v2 → v3 的格式迁移。 + +迁移是"只读旧库、重建新库、原子替换":中间态写在旁边的临时文件里, +所以任何一步失败都不会让原库变成半成品;默认还会先留一份 .bak。 +""" + +import os +import sqlite3 +import unittest +import zlib + +import helpers + +from sqlite_vfs import migration +from sqlite_vfs.core import SQLiteVFS + + +class DetectVersionTest(helpers.VFSTestCase): + def test_detects_each_format(self): + v1 = helpers.make_old_schema_db(self.path("v1.svfs")) + v2 = helpers.make_v2_schema_db( + self.path("v2.svfs"), rows=[{"path": "a.txt", "content": b"a"}] + ) + v3 = self.path("v3.svfs") + SQLiteVFS(v3).close() + + self.assertEqual(1, migration.detect_schema_version(v1)) + self.assertEqual(2, migration.detect_schema_version(v2)) + self.assertEqual(3, migration.detect_schema_version(v3)) + + def test_detects_non_vfs_database(self): + path = self.path("plain.sqlite") + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE unrelated (id INTEGER)") + conn.commit() + conn.close() + self.assertIsNone(migration.detect_schema_version(path)) + + def test_missing_file_raises(self): + with self.assertRaises(FileNotFoundError): + migration.detect_schema_version(self.path("nope.svfs")) + + +class MigrateFromV2Test(helpers.VFSTestCase): + PAYLOAD = b"repeat me please. " * 100 + + def _make_v2(self, name="v2.svfs"): + rows = [ + {"path": "plain.txt", "content": b"plain v2 content"}, + {"path": "dup1.txt", "content": self.PAYLOAD}, + {"path": "dup2.txt", "content": self.PAYLOAD}, + { + "path": "compressed.txt", + "content": zlib.compress(self.PAYLOAD), + "compress_algo": "zlib", + "file_size": len(self.PAYLOAD), + }, + {"path": "empty.txt", "content": b""}, + {"path": "sub", "is_directory": True}, + {"path": "sub/nested.txt", "content": b"nested"}, + ] + return helpers.make_v2_schema_db(self.path(name), rows=rows) + + def test_content_survives_byte_for_byte(self): + db = self._make_v2() + result = migration.migrate(db) + self.assertTrue(result["migrated"]) + self.assertEqual(2, result["from_version"]) + self.assertEqual(3, migration.detect_schema_version(db)) + + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual(b"plain v2 content", vfs.read_file("plain.txt")) + self.assertEqual(self.PAYLOAD, vfs.read_file("dup1.txt")) + self.assertEqual(self.PAYLOAD, vfs.read_file("dup2.txt")) + self.assertEqual( + self.PAYLOAD, + vfs.read_file("compressed.txt"), + "旧库里 zlib 压缩过的内容必须解压后原样返回", + ) + self.assertEqual(b"", vfs.read_file("empty.txt")) + self.assertEqual(b"nested", vfs.read_file("sub/nested.txt")) + + def test_structure_is_preserved(self): + db = self._make_v2() + migration.migrate(db) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + + self.assertTrue(vfs.is_dir("sub")) + self.assertEqual( + ["compressed.txt", "dup1.txt", "dup2.txt", "empty.txt", "plain.txt", "sub"], + sorted(r["path"] for r in vfs.list_directory("/")), + ) + self.assertEqual(6, helpers.count_files(vfs)) + + def test_identical_content_is_deduplicated_during_migration(self): + db = self._make_v2() + result = migration.migrate(db) + # dup1 / dup2 / compressed.txt 三份内容相同 → 只留一份 blob + self.assertEqual(4, result["blobs"], "plain/empty/nested/payload 共四份") + self.assertGreater(result["dedup_saved"], 0, "应当报告去重省下的字节数") + + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual(4, helpers.blob_count(vfs)) + + def test_backup_is_created_and_original_untouched(self): + db = self._make_v2() + before = helpers.md5_of(db) + result = migration.migrate(db) + + self.assertTrue(result["backup"]) + self.assertTrue(os.path.exists(result["backup"])) + self.assertEqual( + before, helpers.md5_of(result["backup"]), "备份必须与迁移前的库完全一致" + ) + self.assertEqual(2, migration.detect_schema_version(result["backup"])) + self.assertNotEqual(before, helpers.md5_of(db)) + + def test_backup_can_be_disabled(self): + db = self._make_v2() + result = migration.migrate(db, backup=False) + self.assertIsNone(result["backup"]) + self.assertFalse(os.path.exists(db + ".bak")) + + def test_second_migration_is_a_no_op(self): + db = self._make_v2() + migration.migrate(db) + digest_before = helpers.md5_of(db) + + again = migration.migrate(db) + self.assertFalse(again["migrated"]) + self.assertIn("已经是", again["reason"]) + self.assertEqual(digest_before, helpers.md5_of(db), "重复迁移不该改动库") + + def test_migrated_pack_verifies_and_signs(self): + db = self._make_v2() + migration.migrate(db) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + + report = vfs.verify(check_content=True) + self.assertTrue(report["ok"], report["errors"]) + self.assertEqual(6, report["files_checked"]) + self.assertEqual(6, report["content_checked"]) + self.assertTrue(vfs.sign("k") and vfs.verify_signature("k")["ok"]) + + def test_metadata_is_carried_over(self): + db = self.path("meta.svfs") + conn = sqlite3.connect( + helpers.make_v2_schema_db( + db, rows=[{"path": "a.txt", "content": b"aaa"}] + ) + ) + conn.execute( + "UPDATE files SET modified_time = '2020-01-02 03:04:05', permissions = 493," + " created_time = '2020-01-01 00:00:00' WHERE path = 'a.txt'" + ) + conn.commit() + conn.close() + + migration.migrate(db) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + info = vfs.get_file_info("a.txt") + self.assertEqual(0o755, info["permissions"]) + self.assertEqual(2020, info["modified_time"].year) + self.assertEqual(3, info["modified_time"].hour) + + def test_snapshot_name_defaults_to_the_pack_name(self): + db = self._make_v2() + migration.migrate(db) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual("v2pack", vfs.list_snapshots()[0]["name"]) + + def test_custom_snapshot_name(self): + db = self._make_v2() + migration.migrate(db, snapshot_name="imported") + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual("imported", vfs.list_snapshots()[0]["name"]) + + def test_migrate_to_a_new_file_leaves_the_original_alone(self): + """原库在只读介质上时不能原地替换,所以支持输出到新路径。""" + db = self._make_v2() + before = helpers.md5_of(db) + target = self.path("migrated.svfs") + + result = migration.migrate(db, output=target) + self.assertTrue(result["migrated"]) + self.assertFalse(result["in_place"]) + self.assertEqual(os.path.abspath(target), result["output"]) + self.assertIsNone(result["backup"], "有独立输出时不必再备份") + + self.assertEqual(before, helpers.md5_of(db), "原库必须一个字节都没变") + self.assertEqual(2, migration.detect_schema_version(db)) + self.assertEqual(3, migration.detect_schema_version(target)) + + vfs = SQLiteVFS(target) + self.addCleanup(vfs.close) + self.assertEqual(self.PAYLOAD, vfs.read_file("dup1.txt")) + self.assertTrue(vfs.verify(check_content=True)["ok"]) + + def test_migration_of_a_readonly_file_into_a_new_file(self): + db = self._make_v2() + os.chmod(db, 0o444) + try: + result = migration.migrate(db, output=self.path("out.svfs")) + self.assertTrue(result["migrated"]) + finally: + os.chmod(db, 0o644) + + def test_refuses_to_overwrite_an_existing_output(self): + db = self._make_v2() + target = helpers.write_file(self.path("taken.svfs"), b"occupied") + with self.assertRaises(FileExistsError): + migration.migrate(db, output=target) + self.assertEqual(b"occupied", helpers.read_bytes(target)) + + def test_backup_can_be_forced_with_output(self): + db = self._make_v2() + result = migration.migrate( + db, output=self.path("m2.svfs"), backup=True + ) + self.assertTrue(os.path.exists(result["backup"])) + + +class MigrateFromV1Test(helpers.VFSTestCase): + def test_v1_bool_flag_is_mapped_to_zlib(self): + payload = b"legacy payload " * 50 + db = helpers.make_old_schema_db( + self.path("v1.svfs"), + extra_rows=[ + { + "path": "packed.txt", + "content": zlib.compress(payload), + "compressed": True, + "file_size": len(payload), + }, + {"path": "stored.txt", "content": b"stored raw"}, + ], + ) + result = migration.migrate(db) + self.assertEqual(1, result["from_version"]) + + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + self.assertEqual(payload, vfs.read_file("packed.txt")) + self.assertEqual(b"stored raw", vfs.read_file("stored.txt")) + self.assertEqual(b"hello", vfs.read_file("legacy.txt")) + self.assertEqual("zlib", vfs.get_blob_info("packed.txt")["compress_algo"]) + self.assertEqual("none", vfs.get_blob_info("stored.txt")["compress_algo"]) + self.assertTrue(vfs.verify(check_content=True)["ok"]) + + +class MigrateCliTest(helpers.VFSTestCase): + """迁移后的库要能被 CLI 正常解包。""" + + def test_migrated_database_unpacks_via_cli(self): + payload = b"migrated content " * 20 + src = helpers.make_tree(self.path("src"), {"a.txt": payload, "sub/b.txt": b"b"}) + db = self.path("v2.svfs") + helpers.pack(src, db, compress=True) + + # 把库降级成 v2 形态:内容搬回 files 表,去掉 blobs/snapshots + conn = sqlite3.connect(db) + conn.executescript( + """ + ALTER TABLE files ADD COLUMN content BLOB; + ALTER TABLE files ADD COLUMN freq_blob BLOB; + ALTER TABLE files ADD COLUMN compress_algo TEXT DEFAULT 'none'; + UPDATE files + SET content = (SELECT b.content FROM blobs b WHERE b.hash = files.content_hash), + freq_blob = (SELECT b.freq_blob FROM blobs b WHERE b.hash = files.content_hash), + compress_algo = (SELECT b.compress_algo FROM blobs b WHERE b.hash = files.content_hash); + DROP TABLE blobs; + DROP TABLE snapshots; + DROP TABLE signatures; + """ + ) + conn.commit() + conn.close() + self.assertEqual(2, migration.detect_schema_version(db)) + + out = self.path("out") + failed = helpers.run_cli("sqlite_vfs.cli.unpacker", db, out) + self.assertNotEqual(0, failed.returncode, "v2 库应当被拒绝并提示迁移") + + self.assertTrue(migration.migrate(db)["migrated"]) + result = helpers.run_cli("sqlite_vfs.cli.unpacker", db, out) + self.assertEqual(0, result.returncode, helpers.cli_output(result)) + helpers.assert_tree_equal(self, helpers.read_tree(src), helpers.read_tree(out)) + + +class MigrateInvalidInputTest(helpers.VFSTestCase): + def test_non_vfs_database_is_rejected(self): + path = self.path("plain.sqlite") + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE unrelated (id INTEGER)") + conn.commit() + conn.close() + with self.assertRaises(ValueError): + migration.migrate(path) + + def test_missing_file_raises(self): + with self.assertRaises(FileNotFoundError): + migration.migrate(self.path("nope.svfs")) + + def test_original_survives_a_failure(self): + """迁移失败时原库必须原样保留(中间态写在临时文件里)。""" + path = self.path("plain.sqlite") + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE unrelated (id INTEGER)") + conn.commit() + conn.close() + before = helpers.md5_of(path) + + with self.assertRaises(ValueError): + migration.migrate(path) + self.assertEqual(before, helpers.md5_of(path)) + self.assertFalse(os.path.exists(path + migration.TEMP_SUFFIX)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_publish.py b/tests/test_publish.py new file mode 100644 index 0000000..511ce67 --- /dev/null +++ b/tests/test_publish.py @@ -0,0 +1,561 @@ +"""按内容寻址发布:导出对象 + 清单,客户端按清单取回并校验。 + +这里刻意用**真实的 HTTP 服务器**跑一遍(而不是只读本地文件), +因为这条路线的卖点正是"普通的静态托管 / CDN 就能用", +顺带也能量出"取一个文件到底要几次请求、多少字节"。 +""" + +import functools +import hashlib +import http.server +import json +import os +import shutil +import threading +import time +import unittest + +import helpers + +from sqlite_vfs import publish +from sqlite_vfs.core import SQLiteVFS + +import zlib + + +class CountingHandler(http.server.SimpleHTTPRequestHandler): + """记录请求次数与字节数的静态文件服务。""" + + requests = None # 由测试注入的列表 + + def log_message(self, *args): # 安静点 + pass + + def do_GET(self): + CountingHandler.requests.append(self.path) + super().do_GET() + + def send_head(self): + path = self.translate_path(self.path) + if os.path.isdir(path): + # 不列目录,避免噪音 + self.send_error(404, "no listing") + return None + return super().send_head() + + +class HttpServerFixture(helpers.VFSTestCase): + def serve(self, directory): + CountingHandler.requests = [] + handler = functools.partial(CountingHandler, directory=directory) + httpd = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) + + def serve(): + try: + httpd.serve_forever() + except OSError: + # 关停时 select 作用在已关闭的 socket 上(Windows 上的收尾竞态), + # 与被测代码无关,静音以免掩盖真实错误 + pass + + thread = threading.Thread(target=serve, daemon=True) + thread.start() + self.addCleanup(httpd.shutdown) + self.addCleanup(httpd.server_close) + host, port = httpd.server_address[:2] + return f"http://{host}:{port}" + + +class ExportTest(helpers.VFSTestCase): + def _pack(self, tree=None, compress=True, name="p.svfs"): + src = helpers.make_tree( + self.path("src"), + tree + or { + "index.html": b"", + "a.txt": b"same-content", + "dup.txt": b"same-content", + "sub/b.js": b"console.log('hello');\n" * 40, + "empty_dir": None, + }, + ) + db = self.path(name) + helpers.pack(src, db, compress=compress, name="demo-pack") + return src, db + + def test_objects_are_deduplicated(self): + _src, db = self._pack() + out = self.path("objects") + result = publish.export_objects(db, out) + self.assertEqual(4, result["files"], "4 个文件") + self.assertEqual( + 3, result["objects"], "a.txt 与 dup.txt 内容相同 → 只导出 3 个对象" + ) + self.assertGreater(result["dedup_saved"], 0) + + def test_object_content_is_the_original_bytes(self): + """库里内容是压缩的,导出必须是原始字节——消费方不该需要我们的解压代码。""" + _src, db = self._pack(compress=True) + out = self.path("objects") + publish.export_objects(db, out) + + vfs = SQLiteVFS(db, readonly=True) + try: + self.assertEqual("zlib", vfs.get_blob_info("sub/b.js")["compress_algo"]) + digest = vfs.get_blob_info("sub/b.js")["hash"] + finally: + vfs.close() + + expected = b"console.log('hello');\n" * 40 + blob = os.path.join(out, "blobs", digest[:2], digest) + with open(blob, "rb") as f: + self.assertEqual(expected, f.read()) + self.assertEqual(digest, hashlib.sha256(expected).hexdigest()) + + def test_object_name_is_its_hash(self): + _src, db = self._pack() + out = self.path("objects") + publish.export_objects(db, out) + blob_root = os.path.join(out, "blobs") + found = 0 + for root, _dirs, files in os.walk(blob_root): + for name in files: + with open(os.path.join(root, name), "rb") as f: + self.assertEqual( + name, hashlib.sha256(f.read()).hexdigest(), f"{name} 名字与内容不符" + ) + found += 1 + self.assertEqual(3, found) + + def test_savings_are_reported_in_separate_units(self): + """去重与压缩必须分开报:把"压缩省下的"算进"去重省下的"会误导选型。""" + _src, db = self._pack() + out = self.path("objects") + result = publish.export_objects(db, out) + manifest = publish.load_manifest(os.path.join(out, publish.MANIFEST_NAME)) + + vfs = SQLiteVFS(db, readonly=True) + try: + blobs = vfs.list_blobs() + finally: + vfs.close() + unique = sum(b["size"] for b in blobs) + stored = sum(b["stored_size"] for b in blobs) + + self.assertEqual(unique, result["unique_bytes"]) + self.assertEqual(stored, result["stored_bytes"]) + self.assertEqual( + unique, result["objects_bytes"], "导出的对象正好是去重后的原始字节" + ) + self.assertEqual( + max(0, manifest["stats"]["total_size"] - unique), result["dedup_saved"] + ) + self.assertEqual(max(0, unique - stored), result["compression_saved"]) + # 两个口径加起来才是"相对原始文件总量省下的全部" + self.assertLessEqual( + result["dedup_saved"] + result["compression_saved"], + manifest["stats"]["total_size"], + ) + + def test_manifest_contents(self): + _src, db = self._pack() + out = self.path("objects") + publish.export_objects(db, out) + manifest = publish.load_manifest(os.path.join(out, publish.MANIFEST_NAME)) + + self.assertEqual(publish.MANIFEST_FORMAT, manifest["format"]) + self.assertEqual(publish.MANIFEST_VERSION, manifest["version"]) + self.assertEqual("demo-pack", manifest["name"]) + self.assertEqual(1, manifest["snapshot"]["id"]) + self.assertEqual(64, len(manifest["snapshot"]["digest"])) + + entries = manifest["entries"] + self.assertTrue(entries["empty_dir"]["dir"], "空目录也要出现在清单里") + self.assertEqual("text/javascript", entries["sub/b.js"]["mime"]) + self.assertEqual(len(b"console.log('hello');\n" * 40), entries["sub/b.js"]["size"]) + self.assertEqual(entries["a.txt"]["hash"], entries["dup.txt"]["hash"]) + self.assertEqual( + {"files": 4, "directories": 2}, + {k: manifest["stats"][k] for k in ("files", "directories")}, + ) + + def test_manifest_is_deterministic_for_the_same_snapshot(self): + """清单里不放"生成时间",所以同一快照导出两次得到完全一样的清单(可 diff、可签名)。""" + _src, db = self._pack() + first = self.path("out1") + second = self.path("out2") + publish.export_objects(db, first) + publish.export_objects(db, second) + self.assertEqual( + helpers.read_bytes(os.path.join(first, publish.MANIFEST_NAME)), + helpers.read_bytes(os.path.join(second, publish.MANIFEST_NAME)), + ) + + def test_publish_is_read_only_on_the_pack(self): + _src, db = self._pack() + before = helpers.md5_of(db) + publish.export_objects(db, self.path("objects")) + self.assertEqual(before, helpers.md5_of(db)) + + def test_refuses_non_empty_output_without_overwrite(self): + _src, db = self._pack() + out = self.path("objects") + publish.export_objects(db, out) + with self.assertRaises(FileExistsError): + publish.export_objects(db, out) + publish.export_objects(db, out, overwrite=True) + + def test_shard_zero_puts_objects_flat(self): + _src, db = self._pack() + out = self.path("objects") + publish.export_objects(db, out, shard=0) + names = os.listdir(os.path.join(out, "blobs")) + self.assertEqual(3, len(names)) + self.assertTrue(all(len(n) == 64 for n in names)) + + def test_publish_a_specific_snapshot(self): + src, db = self._pack(tree={"a.txt": b"v1"}) + vfs = SQLiteVFS(db, compress=True) + try: + vfs.create_snapshot("v2", copy_entries=True) + vfs.remove("a.txt") + with helpers.quiet(): + vfs.add_file(helpers.write_file(self.path("a2.txt"), b"v2"), "a.txt") + vfs.recount() + finally: + vfs.close() + + out = self.path("objects") + publish.export_objects(db, out, snapshot_id=1) + manifest = publish.load_manifest(os.path.join(out, publish.MANIFEST_NAME)) + self.assertEqual(1, manifest["snapshot"]["id"]) + digest = manifest["entries"]["a.txt"]["hash"] + with open(os.path.join(out, "blobs", digest[:2], digest), "rb") as f: + self.assertEqual(b"v1", f.read()) + + def test_manifest_of_a_nonexistent_snapshot_is_rejected(self): + _src, db = self._pack() + with self.assertRaises(FileNotFoundError): + publish.export_objects(db, self.path("objects"), snapshot_id=99) + + +class FetchLocalTest(helpers.VFSTestCase): + def _export(self, tree=None): + src = helpers.make_tree( + self.path("src"), + tree + or { + "index.html": b"", + "a.txt": b"same-content", + "dup.txt": b"same-content", + "sub/b.js": b"console.log(1)", + "empty_dir": None, + }, + ) + db = self.path("p.svfs") + helpers.pack(src, db, compress=True) + out = self.path("objects") + publish.export_objects(db, out) + return src, out + + def _fetch(self, objects, target, cache=None, **kwargs): + manifest = publish.load_manifest(os.path.join(objects, publish.MANIFEST_NAME)) + fetcher = publish.ObjectFetcher( + manifest, local_root=objects, cache_dir=cache, **kwargs + ) + return fetcher, fetcher.fetch(target, paths=kwargs.pop("paths", None)) + + def test_roundtrip_restores_the_tree(self): + src, objects = self._export() + target = self.path("restored") + _fetcher, stats = self._fetch(objects, target) + + helpers.assert_tree_equal(self, helpers.read_tree(src), helpers.read_tree(target)) + self.assertEqual(4, stats.files) + self.assertEqual(2, stats.directories, "empty_dir 与 sub 都要建出来") + self.assertEqual(3, stats.objects_downloaded, "一个对象只取一次(两个路径共用)") + + def test_mtime_and_mode_are_preserved(self): + src, objects = self._export() + target = self.path("restored") + self._fetch(objects, target) + for name in ("a.txt", "sub/b.js"): + with self.subTest(name=name): + source = os.path.join(src, *name.split("/")) + restored = os.path.join(target, *name.split("/")) + self.assertAlmostEqual( + os.path.getmtime(source), os.path.getmtime(restored), delta=2 + ) + + def test_partial_fetch_by_file_and_by_prefix(self): + _src, objects = self._export() + manifest = publish.load_manifest(os.path.join(objects, publish.MANIFEST_NAME)) + + fetcher = publish.ObjectFetcher(manifest, local_root=objects) + self.assertEqual({"sub/b.js"}, fetcher.resolve(["sub/b.js"])) + self.assertEqual({"sub/b.js"}, fetcher.resolve(["sub"])) + self.assertEqual({"sub/b.js"}, fetcher.resolve(["sub/"])) + self.assertEqual( + {"a.txt", "sub/b.js"}, + fetcher.resolve(["a.txt", "sub"]), + "没被选中的同级文件不该被带进来", + ) + self.assertEqual( + {"index.html", "a.txt", "dup.txt", "sub/b.js"}, + fetcher.resolve(None), + "不指定 --path 就是全部文件(不含目录)", + ) + with self.assertRaises(FileNotFoundError): + fetcher.resolve(["nope.txt"]) + + def test_partial_fetch_writes_only_selected(self): + _src, objects = self._export() + target = self.path("partial") + manifest = publish.load_manifest(os.path.join(objects, publish.MANIFEST_NAME)) + stats = publish.ObjectFetcher(manifest, local_root=objects).fetch( + target, paths=["sub"] + ) + self.assertEqual(1, stats.files) + self.assertEqual( + {"sub/b.js"}, {k for k, v in helpers.read_tree(target).items() if v is not None} + ) + + def test_cache_is_reused_and_verified(self): + _src, objects = self._export() + cache = self.path("cache") + _fetcher, first = self._fetch(objects, self.path("r1"), cache=cache) + self.assertEqual(3, first.objects_downloaded) + self.assertEqual(0, first.objects_from_cache) + + _fetcher, second = self._fetch(objects, self.path("r2"), cache=cache) + self.assertEqual(0, second.objects_downloaded, "第二次应当全部命中缓存") + self.assertEqual(3, second.objects_from_cache) + self.assertEqual(3, second.verified) + + def test_corrupted_cache_entry_is_healed(self): + _src, objects = self._export() + cache = self.path("cache") + self._fetch(objects, self.path("r1"), cache=cache) + + # 把缓存里的某个对象改坏 + blob_root = os.path.join(cache, "blobs") + victim = None + for root, _dirs, files in os.walk(blob_root): + for name in files: + victim = os.path.join(root, name) + with open(victim, "wb") as f: + f.write(b"corrupted!!") + + _fetcher, stats = self._fetch(objects, self.path("r2"), cache=cache) + self.assertEqual(1, stats.objects_downloaded, "坏掉的缓存应当被重新下载") + self.assertEqual(2, stats.objects_from_cache) + + def test_link_mode_hardlinks_into_the_cache(self): + if os.name == "nt" and not hasattr(os, "link"): + self.skipTest("平台不支持硬链接") + _src, objects = self._export() + cache = self.path("cache") + target = self.path("linked") + manifest = publish.load_manifest(os.path.join(objects, publish.MANIFEST_NAME)) + publish.ObjectFetcher( + manifest, local_root=objects, cache_dir=cache, link=True + ).fetch(target) + linked = os.path.join(target, "a.txt") + digest = hashlib.sha256(b"same-content").hexdigest() + cached = os.path.join(cache, "blobs", digest[:2], digest) + self.assertEqual( + os.stat(cached).st_ino, os.stat(linked).st_ino, "应当共用同一个 inode" + ) + + +class FetchOverHttpTest(HttpServerFixture): + def _export_and_serve(self): + src = helpers.make_tree( + self.path("src"), + { + "index.html": b"", + "big.bin": b"x" * (256 * 1024), + "sub/b.js": b"console.log(1)", + }, + ) + db = self.path("p.svfs") + helpers.pack(src, db, compress=True) + objects = self.path("objects") + publish.export_objects(db, objects) + return src, objects, self.serve(objects), db + + def test_fetch_from_a_real_http_server(self): + src, objects, base, db = self._export_and_serve() + manifest_url = f"{base}/manifest.json" + target = self.path("restored") + + manifest = publish.load_manifest(manifest_url) + fetcher = publish.ObjectFetcher( + manifest, base_url=base, cache_dir=self.path("cache") + ) + started = time.time() + stats = fetcher.fetch(target) + elapsed = time.time() - started + + helpers.assert_tree_equal(self, helpers.read_tree(src), helpers.read_tree(target)) + self.assertEqual(3, stats.files) + self.assertEqual(3, stats.objects_downloaded) + + requests = CountingHandler.requests + manifest_bytes = os.path.getsize(os.path.join(objects, publish.MANIFEST_NAME)) + total_bytes = os.path.getsize(db) + print( + f"\n [HTTP 实测] 请求 {len(requests)} 次" + f"(清单 1 + 对象 {stats.objects_downloaded})" + f",下载 {stats.bytes_downloaded / 1024:.1f} KB" + f"(清单 {manifest_bytes / 1024:.1f} KB)" + f",耗时 {elapsed * 1000:.0f}ms" + ) + self.assertEqual( + 1 + stats.objects_downloaded, + len(requests), + "总请求数应当是 1 次清单 + 每个对象一次", + ) + self.assertLess(manifest_bytes, total_bytes, "清单必须远小于整包") + + def test_missing_object_is_reported_clearly(self): + _src, objects, base, _db = self._export_and_serve() + manifest = publish.load_manifest(f"{base}/manifest.json") + # 删掉一个对象,模拟"CDN 上少传了一个文件" + digest = manifest["entries"]["sub/b.js"]["hash"] + os.remove(os.path.join(objects, "blobs", digest[:2], digest)) + + fetcher = publish.ObjectFetcher(manifest, base_url=base) + with self.assertRaises(Exception): + fetcher.fetch(self.path("restored")) + + def test_tampered_object_over_http_is_rejected(self): + _src, objects, base, _db = self._export_and_serve() + manifest = publish.load_manifest(f"{base}/manifest.json") + digest = manifest["entries"]["sub/b.js"]["hash"] + victim = os.path.join(objects, "blobs", digest[:2], digest) + with open(victim, "wb") as f: + f.write(b"console.log(2)") # 同样长度,但内容不同 + + fetcher = publish.ObjectFetcher(manifest, base_url=base, verify=True) + with self.assertRaises(ValueError) as ctx: + fetcher.fetch(self.path("restored")) + self.assertIn("校验失败", str(ctx.exception)) + + # 关掉校验就会放过(默认必须是开的) + manifest2 = publish.load_manifest(f"{base}/manifest.json") + fetcher2 = publish.ObjectFetcher(manifest2, base_url=base, verify=False) + fetcher2.fetch(self.path("unverified")) + with open(os.path.join(self.path("unverified"), "sub", "b.js"), "rb") as f: + self.assertEqual(b"console.log(2)", f.read()) + + def test_manifest_from_url_mismatch_is_reported(self): + _src, objects, base, _db = self._export_and_serve() + with self.assertRaises(Exception): + publish.load_manifest(f"{base}/nope.json") + + def test_bad_manifest_format_is_rejected(self): + bad = helpers.write_file( + self.path("bad.json"), json.dumps({"format": "something-else"}).encode() + ) + with self.assertRaises(ValueError) as ctx: + publish.load_manifest(bad) + self.assertIn("svfs-objects", str(ctx.exception)) + + old = helpers.write_file( + self.path("old.json"), + json.dumps( + {"format": publish.MANIFEST_FORMAT, "version": 999} + ).encode(), + ) + with self.assertRaises(ValueError): + publish.load_manifest(old) + + +class PublishCliTest(helpers.VFSTestCase): + PUBLISH = "sqlite_vfs.cli.publish" + FETCH = "sqlite_vfs.cli.fetch" + + def _pack(self): + src = helpers.make_tree( + self.path("src"), {"a.txt": b"aaa", "sub/b.txt": b"bbb"} + ) + db = self.path("p.svfs") + helpers.run_cli("sqlite_vfs.cli.packer", src, db, "--compress") + return src, db + + def _fetch(self, manifest, target, *extra): + """CLI 默认用的是用户级缓存目录,测试必须换成临时目录才可复现。""" + return helpers.run_cli( + self.FETCH, + manifest, + target, + "--cache", + self.path("cache"), + *extra, + ) + + def test_publish_and_fetch_via_cli(self): + src, db = self._pack() + objects = self.path("objects") + published = helpers.run_cli(self.PUBLISH, db, objects) + self.assertEqual(0, published.returncode, helpers.cli_output(published)) + self.assertIn("发布完成", published.stdout) + self.assertTrue(os.path.exists(os.path.join(objects, "manifest.json"))) + + target = self.path("restored") + fetched = self._fetch(os.path.join(objects, "manifest.json"), target) + self.assertEqual(0, fetched.returncode, helpers.cli_output(fetched)) + helpers.assert_tree_equal(self, helpers.read_tree(src), helpers.read_tree(target)) + + def test_publish_missing_db_exits_nonzero(self): + result = helpers.run_cli(self.PUBLISH, self.path("nope.svfs"), self.path("o")) + self.assertNotEqual(0, result.returncode) + self.assertNotIn("Traceback", helpers.cli_output(result)) + + def test_publish_to_nonempty_dir_requires_overwrite(self): + _src, db = self._pack() + objects = self.path("objects") + helpers.run_cli(self.PUBLISH, db, objects) + again = helpers.run_cli(self.PUBLISH, db, objects) + self.assertNotEqual(0, again.returncode) + self.assertIn("错误", again.stdout) + forced = helpers.run_cli(self.PUBLISH, db, objects, "--overwrite") + self.assertEqual(0, forced.returncode, helpers.cli_output(forced)) + + def test_manifest_only_mode(self): + _src, db = self._pack() + objects = self.path("objects") + result = helpers.run_cli(self.PUBLISH, db, objects, "--no-objects") + self.assertEqual(0, result.returncode, helpers.cli_output(result)) + self.assertTrue(os.path.exists(os.path.join(objects, "manifest.json"))) + self.assertFalse(os.path.exists(os.path.join(objects, "blobs"))) + + def test_fetch_json_output(self): + _src, db = self._pack() + objects = self.path("objects") + helpers.run_cli(self.PUBLISH, db, objects) + result = self._fetch( + os.path.join(objects, "manifest.json"), self.path("restored"), "--json" + ) + self.assertEqual(0, result.returncode, helpers.cli_output(result)) + stats = json.loads(result.stdout) + self.assertEqual(2, stats["files"]) + self.assertEqual(2, stats["objects_downloaded"]) + + def test_fetch_missing_path_exits_nonzero(self): + _src, db = self._pack() + objects = self.path("objects") + helpers.run_cli(self.PUBLISH, db, objects) + result = self._fetch( + os.path.join(objects, "manifest.json"), + self.path("restored"), + "--path", + "nope.txt", + ) + self.assertNotEqual(0, result.returncode) + self.assertIn("清单里没有", helpers.cli_output(result)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py new file mode 100644 index 0000000..fed7744 --- /dev/null +++ b/tests/test_roundtrip.py @@ -0,0 +1,153 @@ +"""端到端回归测试:打包 → 解包,必须逐字节无损。 + +这是本项目最核心的验收测试:只要它绿了,说明"把文件夹装进一个 .svfs 再拿出来" +这条主链路是通的。 +""" + +import os +import random +import unittest + +import helpers + +from sqlite_vfs.core import SQLiteVFS + + +def build_spec(): + """构造一个覆盖面较广的目录树。 + + 刻意包含:嵌套目录、空目录、空文件、压缩阈值两侧的样本、 + 可压缩与不可压缩内容、中文路径、带空格的路径、 + 以及含 % 和 _ 的文件名(这两个字符在 SQL LIKE 里是通配符)。 + """ + rnd = random.Random(20240913) + return { + "top.txt": b"top level\n", + "empty.txt": b"", + "one_byte.bin": b"\x00", + "below_threshold.bin": rnd.randbytes(999), + "compressible.txt": b"hello world, this is a highly compressible payload. " * 200, + "random.bin": rnd.randbytes(4096), + "empty_dir": None, + "sub/deep/deeper/nested.txt": b"nested\n", + "sub/space name.txt": b"spaces\n", + "sub/中文名.txt": "中文内容,用来验证非 ASCII 路径与内容。\n".encode("utf-8"), + "sub/percent%name_.txt": b"wildcards\n", + "doc/a.txt": b"a\n", + "docs/b.txt": b"b\n", + } + + +class RoundTripTest(helpers.VFSTestCase): + def _pack(self, compress): + src = helpers.make_tree(self.path("src"), build_spec()) + db = helpers.pack(src, self.path("out.svfs"), compress=compress, name="roundtrip") + return src, db + + def _assert_lossless(self, src, db, ignore_timestamps=False): + out = self.path("out") + with helpers.quiet(): + helpers.unpack(db, out, ignore_timestamps=ignore_timestamps) + # 以磁盘上的源目录为基准:build_spec 里没显式写出的中间目录 + # (sub、sub/deep 等)也必须在导出结果里出现 + helpers.assert_tree_equal(self, helpers.read_tree(src), helpers.read_tree(out)) + + def test_roundtrip_without_compression(self): + src, db = self._pack(compress=False) + self._assert_lossless(src, db) + + def test_roundtrip_with_compression(self): + src, db = self._pack(compress=True) + self._assert_lossless(src, db) + + def test_roundtrip_with_ignore_timestamps(self): + src, db = self._pack(compress=True) + self._assert_lossless(src, db, ignore_timestamps=True) + + def test_every_source_file_is_stored(self): + """P0-1:打包后库里必须有和源目录一一对应的文件记录。""" + src, db = self._pack(compress=True) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + + expected = {rel for rel, content in build_spec().items() if content is not None} + stored = set(helpers.stored_files(vfs)) + self.assertEqual( + "无差异", + helpers.diff_trees({p: None for p in expected}, {p: None for p in stored}), + ) + + def test_metadata_counts_match_database(self): + """P1-5:上报的统计数字必须来自真实入库的数据,而不是遍历计数。""" + src, db = self._pack(compress=True) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + + stats = vfs.get_stats() + stored_count = helpers.count_files(vfs) + source_count = len( + [rel for rel, content in build_spec().items() if content is not None] + ) + + self.assertEqual(source_count, stored_count, "入库文件数 ≠ 源文件数") + self.assertEqual( + source_count, + stats["total_files"], + "元数据里的文件数与实际入库数不一致", + ) + # total_size 记录的是原始文件大小,压缩后 content 里存的是压缩数据, + # 所以这里要和源文件大小比,而不是库里 blob 的长度 + source_size = sum( + len(content) for content in build_spec().values() if content is not None + ) + self.assertEqual(source_size, stats["total_size"]) + + def test_compression_reduces_database_size(self): + """用一个足够大的重复文本,让压缩效果超过 SQLite 页粒度(4KB)的噪声。 + + build_spec 里的小文件即使压得动,省下的字节也可能被页对齐吃掉, + 所以这里单独造一份 92KB 的样本。 + """ + payload = b"hello world, this is a highly compressible payload. " * 2000 + src = helpers.make_tree(self.path("big_src"), {"big.txt": payload}) + plain_db = helpers.pack(src, self.path("plain.svfs"), compress=False) + compressed_db = helpers.pack(src, self.path("compressed.svfs"), compress=True) + + plain_size = os.path.getsize(plain_db) + compressed_size = os.path.getsize(compressed_db) + self.assertLess( + compressed_size, + plain_size, + f"压缩后数据库应当更小: plain={plain_size}, compressed={compressed_size}", + ) + + def test_stored_blob_of_compressible_file_is_smaller(self): + """压缩不只是"标记了一下":库里真正存的字节必须变少。""" + src, db = self._pack(compress=True) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + + blob = vfs.get_blob_info("compressible.txt") + self.assertEqual("zlib", blob["compress_algo"]) + self.assertIsNone(blob["freq_blob"], "zlib 不需要频率表") + with open(os.path.join(src, "compressible.txt"), "rb") as f: + original = f.read() + self.assertEqual(len(original), blob["size"], "blob 记录的应当是原始大小") + # 高度重复的文本,zlib 至少能压掉一半(huffman 只能压到 ~56%) + self.assertLess(blob["stored_size"], len(original) // 2) + self.assertEqual(original, vfs.read_file("compressible.txt")) + + def test_packed_database_contains_no_duplicate_paths(self): + """唯一性现在是"每个快照内"的:UNIQUE(snapshot_id, path)。""" + src, db = self._pack(compress=False) + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + snapshot_id = vfs._active_snapshot() + total = vfs.conn.execute( + "SELECT COUNT(*) AS c FROM files WHERE snapshot_id = ?", (snapshot_id,) + ).fetchone()["c"] + self.assertEqual(total, len(helpers.all_paths(vfs))) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_snapshots.py b/tests/test_snapshots.py new file mode 100644 index 0000000..1009d6f --- /dev/null +++ b/tests/test_snapshots.py @@ -0,0 +1,308 @@ +"""快照与时间旅行。 + +一个库里可以存同一棵树的历史版本:``files`` 表的唯一约束从 ``path`` +变成 ``UNIQUE(snapshot_id, path)``,所以同一路径在不同快照里各有一行。 +""" + +import os +import unittest + +import helpers + +from sqlite_vfs.core import SQLiteVFS + + +class SnapshotLifecycleTest(helpers.VFSTestCase): + def _vfs(self, **kwargs): + vfs = SQLiteVFS(self.path("s.svfs"), **kwargs) + self.addCleanup(vfs.close) + return vfs + + def test_first_write_creates_a_default_snapshot(self): + vfs = self._vfs() + self.assertIsNone(vfs.snapshot_id) + src = helpers.write_file(self.path("a.txt"), b"a") + with helpers.quiet(): + vfs.add_file(src, "a.txt") + + self.assertIsNotNone(vfs.snapshot_id) + snapshots = vfs.list_snapshots() + self.assertEqual(1, len(snapshots)) + self.assertEqual(vfs.snapshot_id, snapshots[0]["id"]) + + def test_add_vfs_metadata_names_the_default_snapshot(self): + vfs = self._vfs() + vfs.add_vfs_metadata("my-pack") + self.assertEqual("my-pack", vfs.list_snapshots()[0]["name"]) + + def test_readonly_never_creates_a_snapshot(self): + """只读打开不能因为"读一下"就写库——否则只读挂载会失败。""" + db = self.path("ro.svfs") + vfs = SQLiteVFS(db) + vfs.close() + before = helpers.md5_of(db) + + readonly = SQLiteVFS(db, readonly=True) + self.addCleanup(readonly.close) + self.assertIsNone(readonly._active_snapshot()) + self.assertEqual([], readonly.list_directory("/")) + self.assertIsNone(readonly.get_file_info("anything")) + self.assertEqual(0, readonly.get_stats()["total_files"]) + self.assertEqual(before, helpers.md5_of(db), "只读会话改动了库文件") + + def test_create_snapshot_becomes_active_and_is_listed(self): + vfs = self._vfs() + src = helpers.write_file(self.path("a.txt"), b"a") + with helpers.quiet(): + vfs.add_file(src, "a.txt") + first = vfs.snapshot_id + + second = vfs.create_snapshot("v2") + self.assertNotEqual(first, second) + self.assertEqual(second, vfs.snapshot_id, "新建的快照应当成为当前快照") + self.assertEqual(["v2"], [s["name"] for s in vfs.list_snapshots() if s["id"] == second]) + self.assertEqual(0, helpers.count_files(vfs), "新快照本身是空的") + + def test_use_snapshot_switches_the_view(self): + vfs = self._vfs() + src = helpers.write_file(self.path("a.txt"), b"a") + with helpers.quiet(): + vfs.add_file(src, "a.txt") + first = vfs.snapshot_id + vfs.create_snapshot("v2") + + vfs.use_snapshot(first) + self.assertEqual(first, vfs.snapshot_id) + self.assertTrue(vfs.exists("a.txt")) + + def test_use_missing_snapshot_raises(self): + vfs = self._vfs() + with self.assertRaises(FileNotFoundError): + vfs.use_snapshot(999) + + def test_constructor_releases_the_connection_when_snapshot_is_invalid(self): + """构造期抛异常也要释放连接,否则 Windows 上库文件被锁住删不掉。 + + 注意:POSIX 上删除已打开的文件总是成功,所以这条断言主要是在 Windows 上生效 + (那正是它被发现的地方)。 + """ + db = self.path("locked.svfs") + SQLiteVFS(db).close() # 先建出一个合法的库 + with self.assertRaises(FileNotFoundError): + SQLiteVFS(db, snapshot_id=999) + + os.remove(db) # 句柄没释放的话这里会 PermissionError + self.assertFalse(os.path.exists(db)) + + def test_reopen_defaults_to_the_latest_snapshot(self): + db = self.path("multi.svfs") + vfs = SQLiteVFS(db) + src = helpers.write_file(self.path("a.txt"), b"a") + with helpers.quiet(): + vfs.add_file(src, "a.txt") + vfs.create_snapshot("v2") + vfs.close() + + reopened = SQLiteVFS(db) + self.addCleanup(reopened.close) + self.assertEqual(2, reopened.get_stats()["snapshot_id"]) + self.assertEqual("v2", reopened.get_stats()["snapshot_name"]) + + def test_delete_snapshot_removes_entries_and_signature_rows(self): + vfs = self._vfs() + src = helpers.write_file(self.path("a.txt"), b"a") + with helpers.quiet(): + vfs.add_file(src, "a.txt") + vfs.create_snapshot("v2") + vfs.sign("k") + self.assertEqual(1, len(vfs.list_signatures(2))) + + deleted = vfs.delete_snapshot(2) + self.assertEqual(0, deleted, "v2 是空快照,没有条目被删") + self.assertEqual([1], [s["id"] for s in vfs.list_snapshots()]) + self.assertIsNone(vfs.snapshot_id, "当前快照被删后应当需要重新解析") + + +class TimeTravelTest(helpers.VFSTestCase): + def _pack_v1_then_v2(self): + """构造两个版本的库:v1 有 a.txt=first,v2 里 a.txt=second 且多了 b.txt。""" + db = self.path("hist.svfs") + vfs = SQLiteVFS(db) + self.addCleanup(vfs.close) + vfs.add_vfs_metadata("history") + + first = helpers.write_file(self.path("a1.txt"), b"first") + with helpers.quiet(): + vfs.add_file(first, "a.txt") + vfs.recount() + + vfs.create_snapshot("v2", copy_entries=True) + vfs.remove("a.txt") + second = helpers.write_file(self.path("a2.txt"), b"second") + extra = helpers.write_file(self.path("b.txt"), b"b") + with helpers.quiet(): + vfs.add_file(second, "a.txt") + vfs.add_file(extra, "b.txt") + vfs.recount() + return vfs + + def test_same_path_has_different_content_per_snapshot(self): + vfs = self._pack_v1_then_v2() + self.assertEqual(b"second", vfs.read_file("a.txt")) + + vfs.use_snapshot(1) + self.assertEqual(b"first", vfs.read_file("a.txt")) + + def test_new_files_are_invisible_in_the_older_snapshot(self): + vfs = self._pack_v1_then_v2() + self.assertTrue(vfs.exists("b.txt")) + vfs.use_snapshot(1) + self.assertFalse(vfs.exists("b.txt")) + + def test_copy_entries_forks_from_the_parent(self): + vfs = self._pack_v1_then_v2() + vfs.use_snapshot(1) + self.assertEqual({b"first"}, {vfs.read_file("a.txt")}) + self.assertEqual({"a.txt"}, helpers.all_paths(vfs)) + + def test_blobs_are_shared_between_snapshots(self): + vfs = self._pack_v1_then_v2() + # first / second / b 三份内容 + self.assertEqual(3, helpers.blob_count(vfs)) + + def test_each_snapshot_has_its_own_digest(self): + vfs = self._pack_v1_then_v2() + digests = {s["id"]: s["digest"] for s in vfs.list_snapshots()} + self.assertEqual(2, len(digests)) + self.assertTrue(all(digests.values()), "每个快照都应当有摘要") + self.assertNotEqual(digests[1], digests[2], "内容不同,摘要必须不同") + + def test_counts_are_per_snapshot(self): + vfs = self._pack_v1_then_v2() + self.assertEqual(2, vfs.get_stats()["total_files"]) + vfs.use_snapshot(1) + self.assertEqual(1, vfs.get_stats()["total_files"]) + + def test_export_respects_the_active_snapshot(self): + vfs = self._pack_v1_then_v2() + out_v2 = self.path("out_v2") + with helpers.quiet(): + vfs.export_filesystem(out_v2) + + vfs.use_snapshot(1) + out_v1 = self.path("out_v1") + with helpers.quiet(): + vfs.export_filesystem(out_v1) + + self.assertEqual( + {"a.txt": b"first"}, {k: v for k, v in helpers.read_tree(out_v1).items()} + ) + self.assertEqual( + {"a.txt": b"second", "b.txt": b"b"}, + {k: v for k, v in helpers.read_tree(out_v2).items()}, + ) + + def test_list_directory_is_snapshot_scoped(self): + vfs = self._pack_v1_then_v2() + self.assertEqual( + ["a.txt", "b.txt"], sorted(r["path"] for r in vfs.list_directory("/")) + ) + vfs.use_snapshot(1) + self.assertEqual( + ["a.txt"], sorted(r["path"] for r in vfs.list_directory("/")) + ) + + def test_remove_in_one_snapshot_does_not_affect_the_other(self): + vfs = self._pack_v1_then_v2() + vfs.remove("a.txt") + self.assertFalse(vfs.exists("a.txt")) + vfs.use_snapshot(1) + self.assertEqual(b"first", vfs.read_file("a.txt")) + + def test_mkdir_in_one_snapshot_does_not_leak(self): + vfs = self._pack_v1_then_v2() + vfs.mk_dir("only-here") + self.assertTrue(vfs.is_dir("only-here")) + vfs.use_snapshot(1) + self.assertFalse(vfs.exists("only-here")) + + def test_snapshot_id_can_be_chosen_at_construction(self): + vfs = self._pack_v1_then_v2() + db = vfs.db_path + first = SQLiteVFS(db, snapshot_id=1) + self.addCleanup(first.close) + self.assertEqual(b"first", first.read_file("a.txt")) + self.assertEqual(1, first.get_stats()["snapshot_id"]) + + +class UnpackerSnapshotTest(helpers.VFSTestCase): + def _two_versions(self, db_name="u.svfs"): + """v1 有 one.txt,v2(派生自 v1)删掉 one.txt 并加上 two.txt。""" + db = self.path(db_name) + vfs = SQLiteVFS(db) + vfs.add_vfs_metadata("u") + one = helpers.write_file(self.path("one.txt"), b"one") + with helpers.quiet(): + vfs.add_file(one, "one.txt") + vfs.recount() + vfs.create_snapshot("v2", copy_entries=True) + vfs.remove("one.txt") + two = helpers.write_file(self.path("two.txt"), b"two") + with helpers.quiet(): + vfs.add_file(two, "two.txt") + vfs.recount() + return vfs + + def test_unpacker_list_root_is_snapshot_scoped(self): + from sqlite_vfs.folder_unpacker import FolderUnpacker + + vfs = self._two_versions() + self.addCleanup(vfs.close) + + self.assertEqual( + ["two.txt"], [r["path"] for r in FolderUnpacker(vfs).list_root()] + ) + vfs.use_snapshot(1) + self.assertEqual( + ["one.txt"], [r["path"] for r in FolderUnpacker(vfs).list_root()] + ) + + def test_unpacker_export_follows_the_snapshot(self): + from sqlite_vfs.folder_unpacker import FolderUnpacker + + vfs = self._two_versions("u2.svfs") + self.addCleanup(vfs.close) + + out = self.path("out") + with helpers.quiet(): + FolderUnpacker(vfs).unpack_filesystem(out) + self.assertEqual({"two.txt": b"two"}, helpers.read_tree(out)) + + vfs.use_snapshot(1) + out1 = self.path("out1") + with helpers.quiet(): + FolderUnpacker(vfs).unpack_filesystem(out1) + self.assertEqual({"one.txt": b"one"}, helpers.read_tree(out1)) + + def test_unpacker_check_database_counts_only_the_active_snapshot(self): + from sqlite_vfs.folder_unpacker import FolderUnpacker + + vfs = self._two_versions("u3.svfs") + self.addCleanup(vfs.close) + + # v2 只有一个条目(two.txt),v1 也只有一个(one.txt); + # 两个快照加起来是 2 条,所以"报 1"就证明统计是分快照的 + ok, message = FolderUnpacker(vfs).check_database() + self.assertTrue(ok, message) + self.assertIn("快照 2", message) + self.assertIn("1 个文件/目录", message) + + vfs.use_snapshot(1) + ok, message = FolderUnpacker(vfs).check_database() + self.assertTrue(ok, message) + self.assertIn("快照 1", message) + self.assertIn("1 个文件/目录", message) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index c44507b..ff85f46 100644 --- a/uv.lock +++ b/uv.lock @@ -8,6 +8,144 @@ resolution-markers = [ "python_full_version < '3.8'", ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.5.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/97/41ccb6acac36fdd13592a686a21b311418f786f519e5794b957afbcea938/annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802", size = 13504, upload-time = "2023-05-31T19:09:57.294Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/f0/a2ee543a96cc624c35a9086f39b1ed2aa403c6d355dfe47a11ee5c64a164/annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd", size = 11136, upload-time = "2023-05-31T19:09:56.141Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "3.7.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, + { name = "idna", version = "3.10", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "sniffio", marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927, upload-time = "2023-07-05T16:45:02.294Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896, upload-time = "2023-07-05T16:44:59.805Z" }, +] + +[[package]] +name = "anyio" +version = "4.5.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, + { name = "idna", version = "3.11", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "sniffio", marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293, upload-time = "2024-10-13T22:18:03.307Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766, upload-time = "2024-10-13T22:18:01.524Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, + { name = "idna", version = "3.11", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna", version = "3.11", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "backports-tarfile" version = "1.2.0" @@ -97,43 +235,31 @@ sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/97/c783634659c292 wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932, upload-time = "2024-09-04T20:44:49.491Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585, upload-time = "2024-09-04T20:44:51.671Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268, upload-time = "2024-09-04T20:44:53.51Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592, upload-time = "2024-09-04T20:44:55.085Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512, upload-time = "2024-09-04T20:44:57.135Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, @@ -155,50 +281,36 @@ sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/56/b1ba7935a17738 wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, @@ -386,10 +498,8 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, @@ -397,10 +507,8 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, @@ -408,10 +516,8 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, @@ -447,6 +553,94 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.103.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "anyio", version = "3.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pydantic", version = "2.5.3", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "starlette", version = "0.27.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/25/bde6d83e92c05deb6ffc320dbe29f9f9e3f83803b3a4e2e47e48153e8c44/fastapi-0.103.2.tar.gz", hash = "sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653", size = 11231158, upload-time = "2023-09-28T20:05:52.685Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/d2/3ad038a2365fefbac19d9a046cab7ce45f4c7bfa81d877cbece9707de9ce/fastapi-0.103.2-py3-none-any.whl", hash = "sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e", size = 66338, upload-time = "2023-09-28T20:05:50.621Z" }, +] + +[[package]] +name = "fastapi" +version = "0.124.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "annotated-doc", version = "0.0.4", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "starlette", version = "0.44.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/21/ade3ff6745a82ea8ad88552b4139d27941549e4f19125879f848ac8f3c3d/fastapi-0.124.4.tar.gz", hash = "sha256:0e9422e8d6b797515f33f500309f6e1c98ee4e85563ba0f2debb282df6343763", size = 378460, upload-time = "2025-12-12T15:00:43.891Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/57/aa70121b5008f44031be645a61a7c4abc24e0e888ad3fc8fda916f4d188e/fastapi-0.124.4-py3-none-any.whl", hash = "sha256:6d1e703698443ccb89e50abe4893f3c84d9d6689c0cf1ca4fad6d3c15cf69f15", size = 113281, upload-time = "2025-12-12T15:00:42.44Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.8" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "annotated-doc", version = "0.0.5", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pydantic", version = "2.13.5", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-inspection", version = "0.4.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "annotated-doc", version = "0.0.5", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic", version = "2.13.5", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "1.6.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", version = "0.4.4", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + [[package]] name = "id" version = "1.5.0" @@ -883,6 +1077,427 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] +[[package]] +name = "pydantic" +version = "2.5.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "annotated-types", version = "0.5.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pydantic-core", version = "2.14.6", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/3f/56142232152145ecbee663d70a19a45d078180633321efb3847d2562b490/pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", size = 651797, upload-time = "2023-12-22T10:39:46.736Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/b7/9aea7ee6c01fe3f3c03b8ca3c7797c866df5fecece9d6cb27caa138db2e2/pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4", size = 381926, upload-time = "2023-12-22T10:39:43.758Z" }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "annotated-types", version = "0.7.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pydantic-core", version = "2.27.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681, upload-time = "2025-01-24T01:42:12.693Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696, upload-time = "2025-01-24T01:42:10.371Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "annotated-types", version = "0.7.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "annotated-types", version = "0.8.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic-core", version = "2.46.5", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-inspection", version = "0.4.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-inspection", version = "0.4.4", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/7d/8304d8471cfe4288f95a3065ebda56f9790d087edc356ad5bd83c89e2d79/pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", size = 360305, upload-time = "2023-12-21T19:56:33.986Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/fc/bfb0da2b2d5b44e49c4c0ce99b07bbfd9f1a4dc92fd3e328a5cf1144467e/pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", size = 1864856, upload-time = "2023-12-21T19:52:15.42Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/3a/46913f3134aff44d11edd7bdbba88efe6081f963014e6eaccf83fd8de2d7/pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", size = 1744162, upload-time = "2023-12-21T19:52:17.997Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/2d/919d3642da44bc9d9c60a2e7bbda04633fc3ffbd6768c355ac0d7e2424d7/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", size = 1832506, upload-time = "2023-12-21T19:52:20.422Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/cd/a2db754b0124e64ad7912160d9c9db310cbd52a990841ef121b53453992d/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", size = 1855925, upload-time = "2023-12-21T19:52:22.463Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/5c/63eb74c7a97daf0ee45dc876f0b0d9cdea9c5c9d64e92508a765cb802e14/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", size = 2005859, upload-time = "2023-12-21T19:52:25.127Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/0c/3aeafa496aaf656be3682cbcacbfe3b4a4b366aaddac0ea74fb2c7c276a2/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", size = 2987131, upload-time = "2023-12-21T19:52:27.874Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/28/3c6843e6b203999be2660d3f114be196f2182dcac533dc764ad320c9184d/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", size = 2072940, upload-time = "2023-12-21T19:52:29.905Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/7e/f1c1cf229bd404f5daf972345030f0c205424a326e67ae888c4a5a9066bd/pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", size = 1922278, upload-time = "2023-12-21T19:52:31.836Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/32/a2f381c8ae08a9682d4e7943ba1f5b518e6f2bdd8261c23721691b332966/pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", size = 2012961, upload-time = "2023-12-21T19:52:34.561Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/c5/2accf5bbc145b890454d4eaf8dcd6423d406fc9f64147fd9020618363866/pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", size = 2138726, upload-time = "2023-12-21T19:52:36.463Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/70/9e4f5624c6d62fe4e0c3199c818c141ab37756987e1e6db965b18d9c344b/pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7", size = 1731407, upload-time = "2023-12-21T19:52:38.92Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/26/0645f87ed58c9ec41def2268ea1d96d4a436bbcd151fe126b80cb649e49d/pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87", size = 1891831, upload-time = "2023-12-21T19:52:41.027Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/77/cbfa02b5f46c5ec6be131d97ae93eef883e25d61b4f4d0a058c792b7e3a2/pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", size = 1861976, upload-time = "2023-12-21T19:52:42.943Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/17/3e4908cf8cb5a1d189f9dfa7cb5698d945e9a4db6b9138e3fef3c32c1f68/pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421", size = 1744008, upload-time = "2023-12-21T19:52:45.088Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/53/7844d20be3a334ea46cdcde8a480cf47e31026d4117d7415a0144d7379c9/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", size = 1830087, upload-time = "2023-12-21T19:52:47.868Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/7b/0fd3444362f31c5f42b655c1ed734480433aa9f8bde97daa19cee0bc2844/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", size = 1852928, upload-time = "2023-12-21T19:52:50.681Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/1d/601f861c0d76154217ea6b066e39f04159a761b9c3a7ca56b0dd0267ce3a/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", size = 2003799, upload-time = "2023-12-21T19:52:52.661Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/5e/a30d56bb6b19e84bcde76cba2d6df45779f127ec73fa2e6d91f0ad3d4bc2/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", size = 2985579, upload-time = "2023-12-21T19:52:55.195Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/84/2dc88180fc6f0d13aab2a47a53b89c2dbc239e2a87d0a58e31077e111e82/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", size = 2071737, upload-time = "2023-12-21T19:52:57.548Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/18/7c17d33b2c8dea2189b2547bafcb70a69a3e537eec12429cc0abfedab683/pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", size = 1919929, upload-time = "2023-12-21T19:52:59.911Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/7b/a1cfe9d3fdedf2b33d41960500c17ccba025b483720c79965b73d607687f/pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", size = 2010042, upload-time = "2023-12-21T19:53:02.556Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/09/c39be628d6068952f30b381576a4392af2024505747572cd70b19f6d9bde/pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", size = 2135569, upload-time = "2023-12-21T19:53:04.926Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/07/ea362b25882fb0efbe2818011a572a112416903fbc3205b6c5dab3d9695c/pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2", size = 1730779, upload-time = "2023-12-21T19:53:06.842Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/d2/fcb3bc3d6d2fa35387b57e9925f1ff5469c2da634b85061dadbd8c398545/pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23", size = 1891297, upload-time = "2023-12-21T19:53:09.412Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/68/41661007a1436f5f3dea7b9f536f083bbf843c8ebd6a207c36c98b01bde1/pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6", size = 1849591, upload-time = "2023-12-21T19:53:11.84Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/26/4bd7ac215215322a693c178a022993450ebf7b1e91b26941f72407e1e9a1/pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", size = 1855038, upload-time = "2023-12-21T19:53:14.05Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/f8/07a2563f40b863ba97f3db648697f3f1d7b7edf1bd679f210064cb556e74/pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", size = 1718829, upload-time = "2023-12-21T19:53:16.61Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/98/fb42628ed811643c364e05353d3a015c74859402994420aeba8e3e34a54c/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", size = 1824883, upload-time = "2023-12-21T19:53:18.652Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/ed/6a318d3846ac45e4e8d7c81a4c4f9cad341f4715521cc2cc7baecd6be9c0/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", size = 1834955, upload-time = "2023-12-21T19:53:20.879Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/d0/adf341fb8ed080bf5abb91c42752ffa099d8439e45d3fa40a21f259f724c/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", size = 1994990, upload-time = "2023-12-21T19:53:23.309Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/58/7cac843607f3b2d0af1768fae90ef219413db163a7cfb7557344edfeed2f/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", size = 3001843, upload-time = "2023-12-21T19:53:25.501Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/62/076e6c43735950e911d80c6edf215314a8cf9b8adefe9613b72b09ccb1ee/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", size = 2067417, upload-time = "2023-12-21T19:53:27.652Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/21/7ca5edf46bc6706152d459b560d669cfd72afe0dda24292408f1be8008d6/pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", size = 1922514, upload-time = "2023-12-21T19:53:30.443Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/53/101aac1d63a743284cdae804ceb6f561879c385f355caf20d2d87da6d36d/pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", size = 2004496, upload-time = "2023-12-21T19:53:32.411Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/7a/2e906fc1a5e4ca45e730118f0afb4878a39a1d505d895835d8cc5452446c/pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", size = 2129106, upload-time = "2023-12-21T19:53:34.501Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/3e/28bd56c6aeadaae2eca12e39274b880505dd7fc9fa7b732d11167275c084/pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786", size = 1743090, upload-time = "2023-12-21T19:53:36.711Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/13/afa2b5c336d30a43592f9bc8d5769ccd15b32b4ef243bd792496fe336925/pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40", size = 1866497, upload-time = "2023-12-21T19:53:39.401Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/33/68e91365ac5ef23fc70fbc4e24ab2f212a6ca39cd23b81589af9807946df/pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e", size = 1844384, upload-time = "2023-12-21T19:53:41.316Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/36/5202bd359fa46cc9a1a33bda770097b406f9d5d3771bb991c665d1ed0b24/pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0", size = 1865689, upload-time = "2023-12-21T19:53:43.508Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/31/58342e325be50d24a8201e3973e4c652db921c717f7c152e20beb4ffa6fc/pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c", size = 1733541, upload-time = "2023-12-21T19:53:45.743Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/c0/cd9456b07fc057119132564176a0a91b9bcc918cb826b8252b7d31122fd1/pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed", size = 1833672, upload-time = "2023-12-21T19:53:47.837Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/a6/af8e5d076b6336c1d81716fd43956fe17501cf69b2afb9a17c09be6309bb/pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a", size = 1857284, upload-time = "2023-12-21T19:53:50.302Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/b1/42b9d9fddd5bbe483c1c84f376cbee7778d192086e71e9db506dea5965e2/pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c", size = 2006188, upload-time = "2023-12-21T19:53:52.403Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/44/e590ba7e3b14f2cc5442309a44d05f06c53f5beeab152846ea5804575d57/pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb", size = 2942523, upload-time = "2023-12-21T19:53:54.979Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/29/7c8ffcf7e736c7a6345f83657a34ea7cf710bf39be4452632f35c09b1905/pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06", size = 2060504, upload-time = "2023-12-21T19:53:57.272Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/ab/14f074ca58d994b4a552864d935cbf3949d6beb8b4b76f8bbee218a9087c/pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8", size = 1919242, upload-time = "2023-12-21T19:53:59.169Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/d0/a4f7847083b5fe46bb17e8bc02779a433ac62985dc26ddbf35dcb8f0307f/pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8", size = 2014090, upload-time = "2023-12-21T19:54:02.707Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/54/ddbc2dbc3bbe75956d19a67e0dd4d85baddcbca1377fd8954f0446286500/pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e", size = 2140516, upload-time = "2023-12-21T19:54:05.411Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/41/1d11547d817e949659947386360a3c77afbb93cab86ffefc0a38f8b581f2/pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6", size = 1731790, upload-time = "2023-12-21T19:54:07.561Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/9f/bca85affee28fff3d8fd848643f6c7bbcbd0567c07f2fb2e5c8e5690f6e8/pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391", size = 1893193, upload-time = "2023-12-21T19:54:09.599Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/2a/8cff567680c0d5e03ef4da218656a61286add825b4733476e6ba13ffeee9/pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", size = 1864405, upload-time = "2023-12-21T19:54:12.146Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/39/cf14a183949bf162ab13a327b2f3a0f757e610f9c378a850e195d71bcfa0/pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", size = 1731310, upload-time = "2023-12-21T19:54:14.199Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/4b/da4ed701ee2ff392916f19149f8fb6d705282d96971cbf256142d0c11594/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", size = 1831776, upload-time = "2023-12-21T19:54:16.553Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/10/dc849eb0c1890c99958d3ae2cfacb502e4d0ab0360c63c7a20231ea04b32/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", size = 1855995, upload-time = "2023-12-21T19:54:19.059Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/ea/435b1ad6890eec709e49dbcc5c0a72ca62ff8c6e62cfc45b7386e5e4cecc/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", size = 2005517, upload-time = "2023-12-21T19:54:21.248Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/3d/f4739255d8676debf398116e8ded523cf9bc9289a14734b3dc10645da67d/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", size = 2941205, upload-time = "2023-12-21T19:54:23.311Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/3d/1a5936fc5558521e8aae22dfb7f0ae6b649040b5fcef7f25be1371d02752/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", size = 2058974, upload-time = "2023-12-21T19:54:26.203Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/5c/289261738045fa6b97e75d8c2ee110fab5c2d1025f7d345816f0f56f1c1e/pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", size = 1922529, upload-time = "2023-12-21T19:54:28.48Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/d1/a291cef89adaa3d82b89055a010bd60560a7bda798e2e729d3dfeb875236/pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", size = 2013197, upload-time = "2023-12-21T19:54:31.422Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/8a/d2c7668e15d3be9157e8328712db22568770640fdcc3a13f4ff0cdd87ee9/pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", size = 2138823, upload-time = "2023-12-21T19:54:33.59Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/cc/207e04b69e0c891f33b83f95fd37d9376ca4ffaf29887c1279617e955229/pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe", size = 1731248, upload-time = "2023-12-21T19:54:36.503Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/43/413069afaae660a6f54e19af18e05783ef1316f9c3e0791632579b65d8d7/pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8", size = 1891828, upload-time = "2023-12-21T19:54:39.276Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/8c/d40937f7f7ccfe9776d1e32b36cebe606da9f11624927bd26722c43ea9cb/pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", size = 1864545, upload-time = "2023-12-21T19:54:42.17Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/cd/252101e88458f4e7c4d2c44400050f92a0b13960ed3c489b513c97aaa7a6/pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", size = 1731389, upload-time = "2023-12-21T19:54:44.353Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/1c/ab01fa05c9fc885a73357116c494feafe1207035f13848e4a772fc9d6154/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", size = 1831538, upload-time = "2023-12-21T19:54:46.398Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/cf/d2e97b2bfd0bff7c4e9086fab03956003e906557c9c52941c15fed75152d/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", size = 1855754, upload-time = "2023-12-21T19:54:48.673Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/57/9a77cc69f05f725a2b492a18209a43ba4e8b9ee179d3c27a8b6b3ab2f921/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", size = 2005450, upload-time = "2023-12-21T19:54:51.464Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/ca/e8fe62da4eb4b538c380900372021c560c3514514677d6d328ac5b95da7c/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", size = 2988683, upload-time = "2023-12-21T19:54:53.962Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/0f/45626f8bf7f7973320531bb384ac302eb9b05a70885b9db2bf1db4cf447b/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", size = 2072750, upload-time = "2023-12-21T19:54:56.089Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/95/d0bc7df3de0eaad08de467c50d1dc423839864f32e78da1cf57af3bbb2cc/pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", size = 1922674, upload-time = "2023-12-21T19:54:58.347Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/09/8078e77e73dda7df0d5cca8541d1fb731a52bc00188806676c3635c344a9/pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", size = 2013299, upload-time = "2023-12-21T19:55:00.875Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/ae/ec8eaa6d9a1305100321d7b9c3c87e015ae61da02a877cfd16b0366b18ff/pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", size = 2138619, upload-time = "2023-12-21T19:55:05.131Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/23/d210c33379ef0525c2cf870a1922b89c11afc3a5b5b2e0485323a383c03a/pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975", size = 1731133, upload-time = "2023-12-21T19:55:08.945Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/88/a996678cc0a6ed714ccef36c155caacae9b072689c72f1a6f5e62ea62f5b/pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9", size = 1891877, upload-time = "2023-12-21T19:55:11.41Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/1e/04ede6259a552777a859d2d5828aedd540ca0db967641d61be864a49671a/pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", size = 1862248, upload-time = "2023-12-21T19:55:13.855Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/84/c53d351f926630753b8dcf37ec2edf8b55a5a1724b3edc5104e06d3e54f1/pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", size = 1734357, upload-time = "2023-12-21T19:55:16.575Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/bb/58bd737b1f4a3b567410fd7a55f2e0ed4ba3209bb1a7a35856714a322a04/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", size = 1821824, upload-time = "2023-12-21T19:55:18.643Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/7f/20ddc4eb15708cc6832c0cc2e398d0fa642aaf28d6ebcbcfb2d284ec6824/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", size = 1956646, upload-time = "2023-12-21T19:55:20.924Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/33/9f761908fde3a6bb10ac865459a6931e53a2cde622782d243365e70981b7/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", size = 1907843, upload-time = "2023-12-21T19:55:24.015Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/e4/da29895abb136eea169944eb81f866d783255c4a6fd581c667c15743b171/pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", size = 2002448, upload-time = "2023-12-21T19:55:26.496Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/21/32afbed9bfedf916dff87846e10ecd8711ba63c88cd6c9bcfc3297ef22ca/pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", size = 2131708, upload-time = "2023-12-21T19:55:28.924Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/b1/bb98ca320ddc91734839ef12af4afe8ae9710a734a05850225ff1830e7e8/pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66", size = 1985502, upload-time = "2023-12-21T19:55:31.264Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/a2/814a1657ab6cd52585c3d8e6a6323063633b912e639f924bc993eca4bb31/pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2", size = 1863057, upload-time = "2023-12-21T19:55:33.786Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/e9/3fb82cf52207408b10c4d4fa0fbe38e0752c13d073d78278d85636d543f0/pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24", size = 1822874, upload-time = "2023-12-21T19:55:36.867Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/4c/e620e8f31376d840e8e606ed5c856390170ac01a8d69e1057bdbec64565d/pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd", size = 1957139, upload-time = "2023-12-21T19:55:39.38Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/04/1f380c8dec71d265f7f83a0a66eeb76ce38c073dd94b58a2f1f53e7e76a6/pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8", size = 1909022, upload-time = "2023-12-21T19:55:41.93Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/d6/d85082bcb96ba4c5398fa4d4f609afed1e90aefb32bf925bd2463df53a0f/pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f", size = 2002841, upload-time = "2023-12-21T19:55:44.966Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/61/4863614d5953b5760ca251f8f937db66982bbef8fbdd8038ab199b840beb/pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a", size = 2132438, upload-time = "2023-12-21T19:55:47.847Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/0a/c56318f1668de782f31b6e9798217e2e5a99d4cce7a8eddffb60bebe3c09/pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", size = 1862337, upload-time = "2023-12-21T19:55:50.819Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/64/de5432d19c42adbb26c4513866e2639c37c9081687c670bf8dc16cedfb6f/pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", size = 1734547, upload-time = "2023-12-21T19:55:53.512Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/47/9f996e867123189f0b12364b00057887b61193d3d004a4391450e980512f/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", size = 1821630, upload-time = "2023-12-21T19:55:56.439Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/9e/f42db0e2931cd67bf990d22215ec50444e31aa6e80e63b8531ab1a5f3ffb/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", size = 1956611, upload-time = "2023-12-21T19:55:58.763Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/91/b5d718de2fc191a1937470e79b53535cf0c3a87b2f21ee927710f4dd4570/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", size = 1908319, upload-time = "2023-12-21T19:56:01.193Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/6d/789f2495c66c99a98b7a09a96145d5f3408941f839de7751995d9a5a8428/pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", size = 2002542, upload-time = "2023-12-21T19:56:04.204Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/4a/3be721510f2fea9ce56b25812e6d6ecea9833c06fa8ae479cd41beb404f5/pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", size = 2131778, upload-time = "2023-12-21T19:56:07.136Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/03/2393b702e4b13c0270c096837a9d28d133e4032d8bd382906190cee296d1/pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d", size = 1985802, upload-time = "2023-12-21T19:56:09.555Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/f6/1e7193769d24b32b19139fb875693d7a351af17f10354e7583a0f7b61a49/pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", size = 1862312, upload-time = "2023-12-21T19:56:12.75Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/ff/812893fd262a98f0291f6afd87a530eb87c75ddc92034b938b8d15aa5ff4/pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", size = 1734314, upload-time = "2023-12-21T19:56:15.351Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/7e/4af14122c7ea67ad5582fddae56f7827044f6b43cca6c7e7421686cca3de/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", size = 1821778, upload-time = "2023-12-21T19:56:17.794Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/3d/91a26a7004a57f374d85d837b4b06dde818045ddba34bc19909e04e2a14d/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", size = 1956642, upload-time = "2023-12-21T19:56:20.471Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/76/ee3c136138fbda5f58c3c49371503b42f3a9c678ef284a0b39be17253d78/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", size = 1907789, upload-time = "2023-12-21T19:56:23.676Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/7a/ceb3c9228ad9ff009ee70fd09ffb9160a45a8adaac5c9a90bc9496a1020e/pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", size = 2002380, upload-time = "2023-12-21T19:56:26.159Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/b8/5baba04b116546302bc0a07ba0989326a167aeec29fd6f5cadc7deb758b1/pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", size = 2131726, upload-time = "2023-12-21T19:56:28.883Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/54/0e231a3405827ad8cdc84ae043aa95bfa2cd2d712fc089cb5162dcbbb2e6/pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e", size = 1985447, upload-time = "2023-12-21T19:56:31.571Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443, upload-time = "2024-12-18T11:31:54.917Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938, upload-time = "2024-12-18T11:27:14.406Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684, upload-time = "2024-12-18T11:27:16.489Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169, upload-time = "2024-12-18T11:27:22.16Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227, upload-time = "2024-12-18T11:27:25.097Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695, upload-time = "2024-12-18T11:27:28.656Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662, upload-time = "2024-12-18T11:27:30.798Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370, upload-time = "2024-12-18T11:27:33.692Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813, upload-time = "2024-12-18T11:27:37.111Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287, upload-time = "2024-12-18T11:27:40.566Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414, upload-time = "2024-12-18T11:27:43.757Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301, upload-time = "2024-12-18T11:27:47.36Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685, upload-time = "2024-12-18T11:27:50.508Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876, upload-time = "2024-12-18T11:27:53.54Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421, upload-time = "2024-12-18T11:27:55.409Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998, upload-time = "2024-12-18T11:27:57.252Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167, upload-time = "2024-12-18T11:27:59.146Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071, upload-time = "2024-12-18T11:28:02.625Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244, upload-time = "2024-12-18T11:28:04.442Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470, upload-time = "2024-12-18T11:28:07.679Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291, upload-time = "2024-12-18T11:28:10.297Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613, upload-time = "2024-12-18T11:28:13.362Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355, upload-time = "2024-12-18T11:28:16.587Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661, upload-time = "2024-12-18T11:28:18.407Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261, upload-time = "2024-12-18T11:28:21.471Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361, upload-time = "2024-12-18T11:28:23.53Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484, upload-time = "2024-12-18T11:28:25.391Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102, upload-time = "2024-12-18T11:28:28.593Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709, upload-time = "2024-12-18T11:29:03.193Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273, upload-time = "2024-12-18T11:29:05.306Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027, upload-time = "2024-12-18T11:29:07.294Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888, upload-time = "2024-12-18T11:29:09.249Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738, upload-time = "2024-12-18T11:29:11.23Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138, upload-time = "2024-12-18T11:29:16.396Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025, upload-time = "2024-12-18T11:29:20.25Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633, upload-time = "2024-12-18T11:29:23.877Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404, upload-time = "2024-12-18T11:29:25.872Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130, upload-time = "2024-12-18T11:29:29.252Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946, upload-time = "2024-12-18T11:29:31.338Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387, upload-time = "2024-12-18T11:29:33.481Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453, upload-time = "2024-12-18T11:29:35.533Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186, upload-time = "2024-12-18T11:29:37.649Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/53/13e9917fc69c0a4aea06fd63ed6a8d6cda9cf140ca9584d49c1650b0ef5e/pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506", size = 1899595, upload-time = "2024-12-18T11:29:40.887Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/20/26c549249769ed84877f862f7bb93f89a6ee08b4bee1ed8781616b7fbb5e/pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320", size = 1775010, upload-time = "2024-12-18T11:29:44.823Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/eb/8234e05452d92d2b102ffa1b56d801c3567e628fdc63f02080fdfc68fd5e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145", size = 1830727, upload-time = "2024-12-18T11:29:46.904Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/df/59f915c8b929d5f61e5a46accf748a87110ba145156f9326d1a7d28912b2/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1", size = 1868393, upload-time = "2024-12-18T11:29:49.098Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/52/81cf4071dca654d485c277c581db368b0c95b2b883f4d7b736ab54f72ddf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228", size = 2040300, upload-time = "2024-12-18T11:29:51.43Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/00/05197ce1614f5c08d7a06e1d39d5d8e704dc81971b2719af134b844e2eaf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046", size = 2738785, upload-time = "2024-12-18T11:29:55.001Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/a3/5f19bc495793546825ab160e530330c2afcee2281c02b5ffafd0b32ac05e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5", size = 1996493, upload-time = "2024-12-18T11:29:57.13Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/e8/e0102c2ec153dc3eed88aea03990e1b06cfbca532916b8a48173245afe60/pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a", size = 1998544, upload-time = "2024-12-18T11:30:00.681Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/a3/4be70845b555bd80aaee9f9812a7cf3df81550bce6dadb3cfee9c5d8421d/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d", size = 2007449, upload-time = "2024-12-18T11:30:02.985Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/9f/b779ed2480ba355c054e6d7ea77792467631d674b13d8257085a4bc7dcda/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9", size = 2129460, upload-time = "2024-12-18T11:30:06.55Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f0/a6ab0681f6e95260c7fbf552874af7302f2ea37b459f9b7f00698f875492/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da", size = 2159609, upload-time = "2024-12-18T11:30:09.428Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/2b/e1059506795104349712fbca647b18b3f4a7fd541c099e6259717441e1e0/pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b", size = 1819886, upload-time = "2024-12-18T11:30:11.777Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/6d/df49c17f024dfc58db0bacc7b03610058018dd2ea2eaf748ccbada4c3d06/pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad", size = 1980773, upload-time = "2024-12-18T11:30:14.828Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475, upload-time = "2024-12-18T11:30:18.316Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279, upload-time = "2024-12-18T11:30:20.547Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112, upload-time = "2024-12-18T11:30:23.255Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780, upload-time = "2024-12-18T11:30:25.742Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943, upload-time = "2024-12-18T11:30:28.036Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492, upload-time = "2024-12-18T11:30:30.412Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714, upload-time = "2024-12-18T11:30:34.358Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163, upload-time = "2024-12-18T11:30:37.979Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217, upload-time = "2024-12-18T11:30:40.367Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899, upload-time = "2024-12-18T11:30:42.737Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726, upload-time = "2024-12-18T11:30:45.279Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219, upload-time = "2024-12-18T11:30:47.718Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382, upload-time = "2024-12-18T11:30:51.871Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159, upload-time = "2024-12-18T11:30:54.382Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331, upload-time = "2024-12-18T11:30:58.178Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467, upload-time = "2024-12-18T11:31:00.6Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797, upload-time = "2024-12-18T11:31:07.243Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839, upload-time = "2024-12-18T11:31:09.775Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861, upload-time = "2024-12-18T11:31:13.469Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582, upload-time = "2024-12-18T11:31:17.423Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985, upload-time = "2024-12-18T11:31:19.901Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715, upload-time = "2024-12-18T11:31:22.821Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733, upload-time = "2024-12-18T11:31:26.876Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375, upload-time = "2024-12-18T11:31:29.276Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307, upload-time = "2024-12-18T11:31:33.123Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971, upload-time = "2024-12-18T11:31:35.755Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616, upload-time = "2024-12-18T11:31:38.534Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943, upload-time = "2024-12-18T11:31:41.853Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654, upload-time = "2024-12-18T11:31:44.756Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292, upload-time = "2024-12-18T11:31:48.613Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961, upload-time = "2024-12-18T11:31:52.446Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/cc/4c88abc035cc0d8b2646a715d8c4145fad7d95817eb5f18297066b21e20e/pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed", size = 2078970, upload-time = "2026-08-28T10:00:18.938Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/59/fa3ef009cc1b2ca3753fd6869ee461b0b5b67c420cf659e32a12be027a6a/pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0", size = 1917185, upload-time = "2026-08-28T10:00:20.891Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/5e/b3d8901f9775ad928077c3155c36f56fc1c813285e3986ed736a5fbf538e/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655", size = 1955266, upload-time = "2026-08-28T10:00:23.135Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/9e/5522b09d12e8720013f2e4ac174999f40a05501bd15ad2bfa197bc136198/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a", size = 2023466, upload-time = "2026-08-28T10:00:25.489Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/2a/a5267bf2c6c7ded3f282b315e5f0cf2c58008c15b917a652fc32f92d6775/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d", size = 2198448, upload-time = "2026-08-28T10:00:27.676Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/a5/f72c192aba23924065946728e4fba96f73939b90e5aa4f7d41e728aea8d4/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8", size = 2240121, upload-time = "2026-08-28T10:00:30.168Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/9e/0c0cc24149429c030bef1a5c1776150e7e61fcbbfc068c6d1f9de90eb259/pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf", size = 2067262, upload-time = "2026-08-28T10:00:32.285Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/00/a2b8690a11d909d9ec9c4eb4d084b4d2e1b227e9b2e74f5926cd39096245/pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464", size = 2095116, upload-time = "2026-08-28T10:00:35.556Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/7e/d3088a2717b7bb316d8d0e64a4b0caf994769e88c56df79df547d75c1dc0/pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64", size = 2134727, upload-time = "2026-08-28T10:00:37.829Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/a4/55a9e0ef61cfd1cbf4289059eb68a3eab765fca8ead6f9991d7de027d42e/pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168", size = 2147932, upload-time = "2026-08-28T10:00:40.123Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/25/d2fbc9d59f91f6c50c0d2ec032041c5e3295d68325ade06ec93fa82da43c/pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e", size = 2301528, upload-time = "2026-08-28T10:00:42.339Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/76/eccc0528d1421e298b42f85650cf021f0f7c42f502c7e58808db4a672bdb/pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13", size = 2322431, upload-time = "2026-08-28T10:00:44.399Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/0c/ffab5a9a0fb82825c44f00dea8ec9d540d2e1e4ab2f1c4f0c32bb8b37fd9/pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7", size = 1958681, upload-time = "2026-08-28T10:00:46.786Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/89/8bb47660fed8c16adf1aae301ba149442e8fd220c126bbea2d24b987abb8/pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0", size = 2046649, upload-time = "2026-08-28T10:00:48.99Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, +] + [[package]] name = "pygments" version = "2.17.2" @@ -1128,11 +1743,32 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "sqlite-vfs" version = "0.1.0" source = { editable = "." } +[package.optional-dependencies] +fastapi = [ + { name = "fastapi", version = "0.103.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "fastapi", version = "0.124.4", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "fastapi", version = "0.128.8", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "fastapi", version = "0.141.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.27.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "starlette", version = "0.44.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "starlette", version = "1.6.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, +] + [package.dev-dependencies] dev = [ { name = "twine", version = "4.0.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, @@ -1141,10 +1777,79 @@ dev = [ ] [package.metadata] +requires-dist = [ + { name = "fastapi", marker = "extra == 'fastapi'", specifier = ">=0.100" }, + { name = "starlette", marker = "extra == 'fastapi'", specifier = ">=0.27" }, +] +provides-extras = ["fastapi"] [package.metadata.requires-dev] dev = [{ name = "twine", specifier = ">=4.0.2" }] +[[package]] +name = "starlette" +version = "0.27.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "anyio", version = "3.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/68/559bed5484e746f1ab2ebbe22312f2c25ec62e4b534916d41a8c21147bf8/starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", size = 51394, upload-time = "2023-05-16T10:59:56.286Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/f8/e2cca22387965584a409795913b774235752be4176d276714e15e1a58884/starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91", size = 66978, upload-time = "2023-05-16T10:59:53.927Z" }, +] + +[[package]] +name = "starlette" +version = "0.44.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/b4/910f693584958b687b8f9c628f8217cfef19a42b64d2de7840814937365c/starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715", size = 2575579, upload-time = "2024-12-28T07:32:56.003Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/c5/7ae467eeddb57260c8ce17a3a09f9f5edba35820fc022d7c55b7decd5d3a/starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea", size = 73412, upload-time = "2024-12-28T07:32:53.871Z" }, +] + +[[package]] +name = "starlette" +version = "0.49.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "anyio", version = "4.14.2", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + [[package]] name = "twine" version = "4.0.2" @@ -1254,6 +1959,36 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + [[package]] name = "urllib3" version = "2.0.7"