diff --git a/harness_engineering/backtest.py b/harness_engineering/backtest.py new file mode 100644 index 00000000..ad8134a9 --- /dev/null +++ b/harness_engineering/backtest.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +测试脚本:使用本地行情服务器进行回测 +""" + +import time +from datetime import date +import logging +# 必须导入 TargetPosTask +from tqsdk import TqApi, TqAuth, TqBacktest, TqSim, TargetPosTask +from tqsdk.exceptions import BacktestFinished + +LOCAL_INS_URL = "http://localhost:17788/symbols/latest.json" +LOCAL_MD_URL = "ws://localhost:17789" + +logging.getLogger("TQSIM").setLevel(logging.WARNING) + +def run_local_backtest(): + total_start = time.time() + + try: + account=TqSim() + api = TqApi( + account=account, + #backtest=TqBacktest(start_dt=date(2025, 1, 1), end_dt=date(2025, 1, 31)), + backtest=TqBacktest(start_dt=date(2025, 1, 1), end_dt=date(2025, 12, 31)), + auth=None, + #auth=TqAuth("suolong33", "suolong33"), + _ins_url=LOCAL_INS_URL, + _md_url=LOCAL_MD_URL, + disable_print=True, + ) + #api = TqApi( + # account=TqSim(), + # backtest=TqBacktest(start_dt=date(2025, 3, 5), end_dt=date(2025, 3, 8)), + # auth=TqAuth("suolong33", "suolong33"), + # #_ins_url=LOCAL_INS_URL, + # #_md_url=LOCAL_MD_URL + #) + + init_time = time.time() - total_start + + #symbol = "SHFE.rb2505" + symbol1 = "KQ.m@SHFE.rb" + symbol2 = "KQ.m@SHFE.au" + + # 尝试获取 K 线 + # 注意:在本地回测中,有时需要先 wait_update 一次让图表建立 + klines1 = api.get_kline_serial(symbol1, 60) + klines2 = api.get_kline_serial(symbol2, 60) + + #if len(klines) > 0: + # print(klines.tail(3)) + + target_pos1 = TargetPosTask(api, symbol1) + target_pos2 = TargetPosTask(api, symbol2) + + loop_count = 0 + while True: + api.wait_update() + loop_count += 1 + + # 简单的退出条件,防止死循环,实际由 BacktestFinished 异常退出 + #if loop_count > 100000: + # break + + if api.is_changing(klines1): + if len(klines1) >= 15: + last_close = klines1.close.iloc[-1] + ma = sum(klines1.close.iloc[-15:]) / 15 + current_price = klines1.close.iloc[-1] + + # if loop_count % 500 == 0: + # print(f" ... 已处理 {loop_count} 次更新,最新价: {current_price}, MA: {ma:.2f}") + + if current_price > ma: + target_pos1.set_target_volume(5) + elif current_price < ma: + target_pos1.set_target_volume(0) + + if api.is_changing(klines2): + if len(klines2) >= 15: + last_close = klines2.close.iloc[-1] + ma = sum(klines2.close.iloc[-15:]) / 15 + current_price = klines2.close.iloc[-1] + + if current_price > ma: + target_pos2.set_target_volume(5) + elif current_price < ma: + target_pos2.set_target_volume(0) + + except BacktestFinished: + total_time = time.time() - total_start + print(f"\n✅ 回测正常结束!") + print(f"") + print(f"⏱️ 初始化时间: {init_time:.2f}s 回测时间: {total_time - init_time:.2f}s 总耗时: {total_time:.2f}s 循环次数: {loop_count}") + # 打印最终账户情况 + print(api.get_account()) + + except Exception as e: + print(f"\n❌ 发生错误: {e}") + import traceback + traceback.print_exc() + + finally: + try: + api.close() + except: + pass + +if __name__ == "__main__": + run_local_backtest() diff --git a/harness_engineering/backtest_single.py b/harness_engineering/backtest_single.py new file mode 100644 index 00000000..7b564baa --- /dev/null +++ b/harness_engineering/backtest_single.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +测试脚本:使用本地行情服务器进行回测 +""" + +import time +from datetime import date +import logging +# 必须导入 TargetPosTask +from tqsdk import TqApi, TqAuth, TqBacktest, TqSim, TargetPosTask +from tqsdk.exceptions import BacktestFinished + +LOCAL_INS_URL = "http://localhost:17788/symbols/latest.json" +LOCAL_MD_URL = "ws://localhost:17789" + +logging.getLogger("TQSIM").setLevel(logging.WARNING) + +def run_local_backtest(): + total_start = time.time() + + try: + account=TqSim() + api = TqApi( + account=account, + backtest=TqBacktest(start_dt=date(2025, 1, 1), end_dt=date(2025, 12, 31)), + auth=None, + #auth=TqAuth("suolong33", "suolong33"), + _ins_url=LOCAL_INS_URL, + _md_url=LOCAL_MD_URL, + disable_print=True, + ) + #api = TqApi( + # account=TqSim(), + # backtest=TqBacktest(start_dt=date(2025, 3, 5), end_dt=date(2025, 3, 8)), + # auth=TqAuth("suolong33", "suolong33"), + # #_ins_url=LOCAL_INS_URL, + # #_md_url=LOCAL_MD_URL + #) + + init_time = time.time() - total_start + + #symbol = "SHFE.rb2505" + symbol1 = "KQ.m@SHFE.rb" + + # 尝试获取 K 线 + # 注意:在本地回测中,有时需要先 wait_update 一次让图表建立 + klines1 = api.get_kline_serial(symbol1, 60) + + #if len(klines) > 0: + # print(klines.tail(3)) + + target_pos1 = TargetPosTask(api, symbol1) + + loop_count = 0 + while True: + api.wait_update() + loop_count += 1 + + # 简单的退出条件,防止死循环,实际由 BacktestFinished 异常退出 + #if loop_count > 100000: + # break + + if api.is_changing(klines1): + if len(klines1) >= 15: + last_close = klines1.close.iloc[-1] + ma = sum(klines1.close.iloc[-15:]) / 15 + current_price = klines1.close.iloc[-1] + + if loop_count % 500 == 0: + print(f" ... 已处理 {loop_count} 次更新,最新价: {current_price}, MA: {ma:.2f}") + + if current_price > ma: + target_pos1.set_target_volume(5) + elif current_price < ma: + target_pos1.set_target_volume(0) + + except BacktestFinished: + total_time = time.time() - total_start + print(f"\n✅ 回测正常结束!") + print(f"") + print(f"⏱️ 初始化时间: {init_time:.2f}s 回测时间: {total_time - init_time:.2f}s 总耗时: {total_time:.2f}s 循环次数: {loop_count}") + # 打印最终账户情况 + print(api.get_account()) + + except Exception as e: + print(f"\n❌ 发生错误: {e}") + import traceback + traceback.print_exc() + + finally: + try: + api.close() + except: + pass + +if __name__ == "__main__": + run_local_backtest() diff --git a/harness_engineering/program.md b/harness_engineering/program.md new file mode 100644 index 00000000..700cde5c --- /dev/null +++ b/harness_engineering/program.md @@ -0,0 +1,53 @@ +# Automatic Performance Optimization + +This is an experiment to have claude code to keep optimizing the performance of the framework tqsdk-python (current repo) based on cProfile results. + +## Plan +You are given the source code of tqsdk-python, a backtest framework for Chinese CTA futures market, and a script to run backtest of a strategy by calling the sdk. You need to keep analyzing the profile data from cProfile, and find ways to improve the system performance. + +## Setup +You are working in two repos: +1. `./tqsdk`: contains tqsdk-python's complete source code. +2. `./harness_engineering`, which has its own virtual environment and contains backtest code using the current repo (it's `tqsdk-python` in its uv environment is pointing to the current repo) + +To set up a new experiment, work with the user to: +**Start by running the first profiling**. run `cd harness_engineering && uv run python -m cProfile -o result.prof backtest.py` + +Once you get confirmation, kick off the experimentation. + +## Experimentation + +### Step 1: Analyze the existing cProfile result and find ways to improve the repo's performance. +Look at the git state: the current branch/commit we're on. + +Then run this command to get the backtest's top 10 functions sorted by tottime: `cd harness_engineering && uv run python -c "import pstats; p = pstats.Stats('result.prof'); p.sort_stats('tottime').print_stats(10)"` + +Then optimize the current repo's code based on the cProfile result. + +**What you CAN do:** +Analyze and modify the code in `./tqsdk` however you want. + +**What you CANNOT do:** +- Modify anything in `./harness_engineering` + +**The goal is simple: get the lowest total backtest time, while keeping its backtest result exactly the same.** + +### Step 2. Verify the optimization +Run the following command to profile the result: +`cd harness_engineering && uv run python -m cProfile -o new_result.prof backtest.py` + +Two things to verify: +1. The result backtest time is shorter than previous. +2. The following backtest metrics MUST BE exactly the same as the backtest result(other metrics we don't care): +``` +{'currency': 'CNY', 'pre_balance': 9165485.597521082, 'static_balance': 9165485.597521082, 'balance': 9165485.597521082, 'available': 9165485.097521082, 'ctp_balance': nan, 'ctp_available': nan, 'float_profit': 33099.99999999999, 'position_profit': 0.0, 'close_profit': 0.0, 'frozen_margin': 0.0, 'margin': 0.5, 'frozen_commission': 0.0, 'commission': 0.0, 'frozen_premium': 0.0, 'premium': 0.0, 'deposit': 0.0, 'withdraw': 0.0, 'risk_ratio': 5.455248330052814e-08, 'market_value': 0.0, '_tqsdk_stat': , D({'start_date': '2025-01-01', 'end_date': '2025-12-31', 'init_balance': np.float64(10000000.0), 'balance': np.float64(9165485.597521082), 'start_balance': np.float64(10000000.0), 'end_balance': np.float64(9165485.597521082), 'ror': np.float64(-0.08345144024789186), 'annual_yield': np.float64(-0.08541331095548088), 'trading_days': 244, 'cum_profit_days': np.int64(92), 'cum_loss_days': np.int64(151), 'max_drawdown': np.float64(0.10702989218181808), 'commission': np.float64(14.40250000000001), 'open_times': 14403, 'close_times': 14402, 'daily_risk_ratio': np.float64(5.674929510006093e-08), 'max_cont_profit_days': np.int64(6), 'max_cont_loss_days': np.int64(11), 'sharpe_ratio': np.float64(-1.9706534357067889), 'calmar_ratio': np.float64(-0.0663809545753567), 'sortino_ratio': np.float64(-1.6926465451120536), 'tqsdk_punchline': '不要灰心,少侠重新来过', 'profit_volumes': 11850, 'loss_volumes': 60160, 'profit_value': np.float64(8928950.000000276), 'loss_value': np.float64(-9796549.999999123), 'winning_rate': 0.16456047771142898, 'profit_loss_ratio': np.float64(4.62718335334115)})} +``` + +If both are true, +1. use the new cProfile result for the next round and run: `cd harness_engineering && mv new_result.prof result.prof` +2. git commit with the improvement and the reduced time. + +If either one is not true, then abandon the current change and start over from step 1. + +### LOOP FOREVER +The idea is that you are a completely autonomous performance engineer trying things out. If they work, keep. If they don't, discard. And you're advancing the branch so that you can iterate. diff --git a/harness_engineering/pyproject.toml b/harness_engineering/pyproject.toml new file mode 100644 index 00000000..4c52d294 --- /dev/null +++ b/harness_engineering/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "tq-local-server" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "ipdb>=0.13.13", + "orjson>=3.11.7", + "snakeviz>=2.2.2", + "tqdm>=4.67.3", + "tqsdk", +] + +[[index]] +name = "tuna" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +default = true + +[tool.uv.sources] +tqsdk = { path = "../tqsdk-python", editable = true } diff --git a/harness_engineering/readme.md b/harness_engineering/readme.md new file mode 100644 index 00000000..4025cb83 --- /dev/null +++ b/harness_engineering/readme.md @@ -0,0 +1,6 @@ +# How to run it? +1. Launch claude (You may want to use `claude --dangerously-skip-permissions` to skip permissions) +2. in claude, type "Hi take a look at harness_engineering/program.md and let's start our first experiment." + +# What's run_benchamrks.sh for? +It's used to re-profile all claude commits \ No newline at end of file diff --git a/harness_engineering/run_benchmarks.sh b/harness_engineering/run_benchmarks.sh new file mode 100755 index 00000000..2ca6b2bc --- /dev/null +++ b/harness_engineering/run_benchmarks.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Benchmark each commit ahead of origin/master +# Runs backtest.py for each commit and saves output to a log file + +set -e + +REPO_DIR="/home/zzk/Projects/tqsdk-python" +TEST_DIR="/home/zzk/Projects/tq_sdk_test" +CURRENT_BRANCH=$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD) + +# Get commits in chronological order (oldest first) +mapfile -t COMMITS < <(git -C "$REPO_DIR" log --reverse --format="%H %s" origin/master..HEAD) + +TOTAL=${#COMMITS[@]} +echo "Found $TOTAL commits to benchmark" +echo "Current branch: $CURRENT_BRANCH" +echo "" + +COUNT=0 +for entry in "${COMMITS[@]}"; do + HASH="${entry%% *}" + MESSAGE="${entry#* }" + SHORT_HASH="${HASH:0:7}" + COUNT=$((COUNT + 1)) + + # Sanitize message: first 50 chars, replace non-alphanumeric with _ + SAFE_MSG=$(echo "$MESSAGE" | head -c 50 | sed 's/[^a-zA-Z0-9_-]/_/g') + LOGFILE="${TEST_DIR}/${COUNT}_${SAFE_MSG}_${SHORT_HASH}.log" + + echo "[$COUNT/$TOTAL] $SHORT_HASH $MESSAGE" + + # Checkout the commit + git -C "$REPO_DIR" checkout --quiet "$HASH" + + # Run backtest and save output + echo "Commit: $SHORT_HASH $MESSAGE" > "$LOGFILE" + echo "Date: $(git -C "$REPO_DIR" log -1 --format='%ci' "$HASH")" >> "$LOGFILE" + echo "---" >> "$LOGFILE" + (cd "$TEST_DIR" && uv run backtest.py) >> "$LOGFILE" 2>&1 || true + + echo " -> saved to $(basename "$LOGFILE")" + echo "" +done + +# Restore original branch +echo "Restoring branch: $CURRENT_BRANCH" +git -C "$REPO_DIR" checkout --quiet "$CURRENT_BRANCH" +echo "Done! All $TOTAL benchmarks complete." diff --git a/harness_engineering/uv.lock b/harness_engineering/uv.lock new file mode 100644 index 00000000..2e6ad8be --- /dev/null +++ b/harness_engineering/uv.lock @@ -0,0 +1,931 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'emscripten'", + "sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/c5/36aa96205c3ecbb3d34c7c24189e4553c7ca2ebc7e1dd07432339b980272/graphql_core-3.2.8.tar.gz", hash = "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", size = 513181, upload-time = "2026-03-05T19:55:37.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-py3-none-any.whl", hash = "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c", size = 207349, upload-time = "2026-03-05T19:55:35.911Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ipdb" +version = "0.13.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "ipython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, +] + +[[package]] +name = "ipython" +version = "9.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, + { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, + { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sgqlc" +version = "18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "graphql-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/60/3b8c93b3d5df873bd13bc8fe0a4021d5f7442ca7ee6b98c5152dd55e18c7/sgqlc-18.tar.gz", hash = "sha256:a09f76fb69a2760523144f8d3acbf06a1b3710f63bf87fbac142d3538b1d639b", size = 245818, upload-time = "2026-02-06T19:56:23.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/96/8a5b972de8ede610aa088360927411b05c50ebcd669e52b0a3bfd1ed3ed9/sgqlc-18-py3-none-any.whl", hash = "sha256:7af53b5c1aab4602a98a36d6d32bd641e33f602b9b72468885f2505c59c536af", size = 86121, upload-time = "2026-02-06T19:56:22.07Z" }, +] + +[[package]] +name = "shinny-structlog" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/8f/66169d65384be6ab15e146db5f1b384a74ce202aa7bdc311f3a088187fb5/shinny-structlog-0.0.4.tar.gz", hash = "sha256:e30db15314ce81f15af025b0f49715dcfdcb932e539bfbc7d462b8b61c3afeb3", size = 14834, upload-time = "2020-11-02T07:17:10.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/7e/e9289e6b2c677318b44e48eb8cd5bb7dc9fe786df640e78947cda7a325ae/shinny_structlog-0.0.4-py3-none-any.whl", hash = "sha256:989f96f484eeefae2a37a80fd262a3d3c92d14e3fe8346c218f45dff0a3412c8", size = 8783, upload-time = "2020-11-02T07:17:09.413Z" }, +] + +[[package]] +name = "simplejson" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/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 = "snakeviz" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/06/82f56563b16d33c2586ac2615a3034a83a4ff1969b84c8d79339e5d07d73/snakeviz-2.2.2.tar.gz", hash = "sha256:08028c6f8e34a032ff14757a38424770abb8662fb2818985aeea0d9bc13a7d83", size = 182039, upload-time = "2024-11-09T22:03:58.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f7/83b00cdf4f114f10750a18b64c27dc34636d0ac990ccac98282f5c0fbb43/snakeviz-2.2.2-py3-none-any.whl", hash = "sha256:77e7b9c82f6152edc330040319b97612351cd9b48c706434c535c2df31d10ac5", size = 183477, upload-time = "2024-11-09T22:03:57.049Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "tq-local-server" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "ipdb" }, + { name = "orjson" }, + { name = "snakeviz" }, + { name = "tqdm" }, + { name = "tqsdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "ipdb", specifier = ">=0.13.13" }, + { name = "orjson", specifier = ">=3.11.7" }, + { name = "snakeviz", specifier = ">=2.2.2" }, + { name = "tqdm", specifier = ">=4.67.3" }, + { name = "tqsdk", editable = "../tqsdk-python" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "tqsdk" +version = "3.9.1" +source = { editable = "../tqsdk-python" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "filelock" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "psutil" }, + { name = "pyjwt" }, + { name = "requests" }, + { name = "scipy" }, + { name = "sgqlc" }, + { name = "shinny-structlog" }, + { name = "simplejson" }, + { name = "tqsdk-ctpse" }, + { name = "tqsdk-sm" }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "filelock" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas", specifier = ">=1.1.0" }, + { name = "psutil", specifier = ">=5.9.6" }, + { name = "pyjwt" }, + { name = "requests" }, + { name = "scipy" }, + { name = "sgqlc" }, + { name = "shinny-structlog" }, + { name = "simplejson" }, + { name = "tqsdk-ctpse" }, + { name = "tqsdk-sm" }, + { name = "websockets", specifier = ">=10.1" }, +] + +[[package]] +name = "tqsdk-ctpse" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/3b/fdd80159ab744132cc02e9946b8226b7ccd0b234e9c03005949e21b7f346/tqsdk-ctpse-1.0.2.tar.gz", hash = "sha256:24483b38b695e9feaebc9d6046b214373ed2f21c76ca925123281a272e1adfc3", size = 1845, upload-time = "2023-03-30T10:00:13.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/cd/d77ea3fd3419f899faf7c7ad5e9818cd541172c909bcda083ea648e793e8/tqsdk_ctpse-1.0.2-py3-none-any.whl", hash = "sha256:f529e3e0cf66c395fb0b4b45035f4e2fbda528ae3c34534b44ae69b06395b260", size = 1901, upload-time = "2023-03-30T10:00:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/cf06145b03dd50c9aa186816d7661b79b4019415d3d78aa0776aa7279f3f/tqsdk_ctpse-1.0.2-py3-none-manylinux1_x86_64.whl", hash = "sha256:b45045f4e7b408a3c27ec59ad50393958e25fd7976b1994a0c430bbcf19836a4", size = 1115901, upload-time = "2023-03-30T10:00:05.447Z" }, + { url = "https://files.pythonhosted.org/packages/63/04/87c559c23d71926de1816e88937dd1693527d69d66f99b12e3304c249d62/tqsdk_ctpse-1.0.2-py3-none-win32.whl", hash = "sha256:f6d79a2df7ced971e64eb68a1319f89f5acd860dec1bb0dcca3660f74fabc725", size = 761806, upload-time = "2023-03-30T10:00:10.962Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8c/6d6154174e4d5d97d2ce63c92c08ed0dcfd8ae553cf6e5a6361f7ca3db80/tqsdk_ctpse-1.0.2-py3-none-win_amd64.whl", hash = "sha256:0301503e9a4a6b886d48adc2d840d98292bde100064e1cf9446f5fa0989bbd58", size = 897340, upload-time = "2023-03-30T10:00:12.065Z" }, +] + +[[package]] +name = "tqsdk-sm" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/b2/c468172a533bbe7addfcfe4023709997f90a9f0fd602d2965dfcc286cc0a/tqsdk-sm-1.0.5.tar.gz", hash = "sha256:0871d41f1ddad8027c11a8d33f25dbab39e79beb33f718642b35f4f9af75a609", size = 1232, upload-time = "2023-05-23T07:58:48.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/bd/c1f1644aef77041d683b5b738edec438f87e03c02d21e7af01ce39d827c2/tqsdk_sm-1.0.5-py3-none-any.whl", hash = "sha256:49eb823ae4f489bd5873d414f70c3b8aa9a593d5bb242369dc56a21a0ea5ee56", size = 1383, upload-time = "2023-05-23T07:58:27.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/3f/8c41736d1c0b7fb46a62989d35fa9a92d6e1d7bdb4b23bdbe366ef0eaef3/tqsdk_sm-1.0.5-py3-none-manylinux1_x86_64.whl", hash = "sha256:c21f94da78736297016aef380377cd363e7f0b9421b7dc9361405a0f205d552f", size = 25679369, upload-time = "2023-05-23T07:58:30.015Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8f/cf287e8ad2ef357f5e9ce13615cf057b8b8aec6587a99b78dbd184bfe6a9/tqsdk_sm-1.0.5-py3-none-win32.whl", hash = "sha256:f1879e2673e42f31adbaf745b936fb79db314f955b679f26772a79e0ba2f8b07", size = 21688548, upload-time = "2023-05-23T07:58:35.697Z" }, + { url = "https://files.pythonhosted.org/packages/b5/af/4b92033cccb53d42cfbabfc82e41cbd372fdf0f56a3b550c00dc7064e59c/tqsdk_sm-1.0.5-py3-none-win_amd64.whl", hash = "sha256:f0e7a1cc7ce58dd1d5261a8a7da7d64902bfece9e819be600c0d2ce0b81de518", size = 23012753, upload-time = "2023-05-23T07:58:43.726Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] diff --git a/tqsdk/api.py b/tqsdk/api.py index 1785beb1..cb077302 100644 --- a/tqsdk/api.py +++ b/tqsdk/api.py @@ -60,7 +60,7 @@ from tqsdk.data_series import DataSeries from tqsdk.datetime import _get_trading_day_from_timestamp, _datetime_to_timestamp_nano, _timestamp_nano_to_datetime, \ _cst_now, _convert_user_input_to_nano -from tqsdk.diff import _merge_diff, _get_obj, _is_key_exist, _register_update_chan +from tqsdk.diff import _merge_diff, _get_obj, _get_obj_single, _is_key_exist, _register_update_chan from tqsdk.entity import Entity from tqsdk.exceptions import TqTimeoutError from tqsdk.log import _clear_logs, _get_log_name, _get_disk_free @@ -224,7 +224,8 @@ def __init__(self, account: Optional[Union[TqMultiAccount, UnionTradeable]] = No user_name, pwd = auth[:comma_index], auth[comma_index + 1:] self._auth = TqAuth(user_name, pwd) else: - self._auth = None + from tqsdk.auth import TqAuthDummy + self._auth = TqAuthDummy() self._account = TqSim() if account is None else account self._backtest = backtest self._stock = False if isinstance(self._backtest, TqReplay) else _stock @@ -1917,9 +1918,12 @@ def _wait_update(self, deadline: Optional[float] = None, _task: Union[asyncio.Ta # 同步代码:此次调用 wait_update 之前应该已经修改执行 # 异步代码:上一行 self._run_until_idle() 可能会修改 klines 附加列的值 # 所以放在这里处理, 总会发送 serial_extra_array 数据,由 TqWebHelper 处理 + _any_serial_updated = False for _, serial in self._serials.items(): - self._process_serial_extra_array(serial) - self._run_until_idle(async_run=False) # 这里 self._run_until_idle() 主要为了把上一步计算出得需要绘制的数据发送到 TqWebHelper + if self._process_serial_extra_array(serial): + _any_serial_updated = True + if _any_serial_updated: + self._run_until_idle(async_run=False) # 这里 self._run_until_idle() 主要为了把上一步计算出得需要绘制的数据发送到 TqWebHelper if _task is not None: # 如果 _task 已经 done,则提前返回 True, False 代表超时会抛错 _tasks = _task if isinstance(_task, list) else [_task] @@ -1943,23 +1947,52 @@ def _wait_update(self, deadline: Optional[float] = None, _task: Union[asyncio.Ta self._pending_diffs = [] # 清空K线更新范围,避免在 wait_update 未更新K线时仍通过 is_changing 的判断 self._klines_update_range = {} + _has_any_stock = self._account._has_any_stock for d in self._diffs: - # 判断账户类别, 对股票和期货的 trade 数据分别进行处理 - if "trade" in d: - for k, v in d.get('trade').items(): - prototype = self._security_prototype if self._account._is_stock_type(k) else self._prototype - _merge_diff(self._data, {'trade': {k: v}}, prototype, persist=False, reduce_diff=True) - # 非交易数据均按照期货处理逻辑 - diff_without_trade = {k: v for k, v in d.items() if k != "trade"} - if diff_without_trade: - _merge_diff(self._data, diff_without_trade, self._prototype, persist=False, reduce_diff=True) + if _has_any_stock: + # 判断账户类别, 对股票和期货的 trade 数据分别进行处理 + if "trade" in d: + trade_data = d['trade'] + futures_trade = {} + stock_trade = {} + _is_stock = self._account._is_stock_type + for k, v in trade_data.items(): + if _is_stock(k): + stock_trade[k] = v + else: + futures_trade[k] = v + if futures_trade: + _merge_diff(self._data, {'trade': futures_trade}, self._prototype, False, True, False) + if stock_trade: + _merge_diff(self._data, {'trade': stock_trade}, self._security_prototype, False, True, False) + # 非交易数据均按照期货处理逻辑 + diff_without_trade = {k: v for k, v in d.items() if k != "trade"} + if diff_without_trade: + _merge_diff(self._data, diff_without_trade, self._prototype, False, True, False) + else: + # No stock accounts: merge entire diff directly (no split needed) + _merge_diff(self._data, d, self._prototype, False, True, False) self._risk_manager._on_recv_data(self._diffs) for _, serial in self._serials.items(): # K线df的更新与原始数据、left_id、right_id、more_data、last_id相关,其中任何一个发生改变都应重新计算df # 注:订阅某K线后再订阅合约代码、周期相同但长度更短的K线时, 服务器不会再发送已有数据到客户端,即chart发生改变但内存中原始数据未改变。 # 检测到K线数据或chart的任何字段发生改变则更新serial的数据 - if self._is_obj_changing(serial["df"], diffs=self._diffs, key=[]) \ - or self._is_obj_changing(serial["chart"], diffs=self._diffs, key=[]): + # Fast path: cache paths and check diffs directly, avoiding isinstance overhead + try: + cached_paths = serial["_cached_check_paths"] + except KeyError: + cached_paths = [root._path for root in serial["root"]] + cached_paths.append(serial["chart"]._path) + serial["_cached_check_paths"] = cached_paths + serial_changed = False + for diff in self._diffs: + for path in cached_paths: + if _is_key_exist(diff, path, []): + serial_changed = True + break + if serial_changed: + break + if serial_changed: if len(serial["root"]) == 1: # 订阅单个合约 self._update_serial_single(serial) else: # 订阅多个合约 @@ -2056,7 +2089,7 @@ def _is_obj_changing(self, obj: Any, diffs: List[Dict[str, Any]], key: List[str] if id(obj) in self._serials: paths = [] for root in self._serials[id(obj)]["root"]: - paths.append(root["_path"]) + paths.append(root._path) elif len(obj) == 0: return False else: # 处理传入的为一个 copy 出的 DataFrame (与原 DataFrame 数据相同的另一个object) @@ -2087,7 +2120,7 @@ def _is_obj_changing(self, obj: Any, diffs: List[Dict[str, Any]], key: List[str] paths.append(["ticks", obj["symbol"], "data", str(int(obj["id"]))]) else: - paths = [obj["_path"]] + paths = [obj._path] except (KeyError, IndexError): return False for diff in diffs: @@ -3405,21 +3438,22 @@ def _setup_connection(self): py_version=platform.python_version(), py_arch=platform.architecture()[0], cmd=sys.argv, mem_total=mem.total, mem_free=mem.free) if self._auth is None: - raise Exception("请输入 auth (快期账户)参数,快期账户是使用 tqsdk 的前提,如果没有请点击注册,注册地址:https://account.shinnytech.com/。") - else: - self._auth.init(mode="bt" if isinstance(self._backtest, TqBacktest) else "real") - self._auth.login() # tqwebhelper 有可能会设置 self._auth + from tqsdk.auth import TqAuthDummy + self._auth = TqAuthDummy() + + self._auth.init(mode="bt" if isinstance(self._backtest, TqBacktest) else "real") + self._auth.login() # tqwebhelper 有可能会设置 self._auth - # tqsdk 内部捕获异常如果需要打印日志,则需要自定义异常 - # 对于第三方代码产生的异常需要逐个捕获,可以参考 connect.py TqConnect._run 函数中对于各类异常的捕获 - # 这里只是打印账户过期日期来提醒用户,不关心是否成功,也不记录日志,所以直接 pass - # 单独捕获 self._auth.expire_datetime 是为了语义清晰,表明异常的来源 - try: - self._auth.expire_datetime - except Exception: - pass - if self._auth._expire_days_left is not None and self._auth._product_type is not None and self._auth._expire_days_left < 30: - self._print(f"TqSdk {self._auth._product_type} 版剩余 {self._auth._expire_days_left} 天到期,如需续费或升级请访问 https://account.shinnytech.com/ 或联系相关工作人员。") + # tqsdk 内部捕获异常如果需要打印日志,则需要自定义异常 + # 对于第三方代码产生的异常需要逐个捕获,可以参考 connect.py TqConnect._run 函数中对于各类异常的捕获 + # 这里只是打印账户过期日期来提醒用户,不关心是否成功,也不记录日志,所以直接 pass + # 单独捕获 self._auth.expire_datetime 是为了语义清晰,表明异常的来源 + try: + self._auth.expire_datetime + except Exception: + pass + if self._auth._expire_days_left is not None and self._auth._product_type is not None and self._auth._expire_days_left < 30: + self._print(f"TqSdk {self._auth._product_type} 版剩余 {self._auth._expire_days_left} 天到期,如需续费或升级请访问 https://account.shinnytech.com/ 或联系相关工作人员。") # 在快期账户登录之后,对于账户的基本信息校验及更新 for acc in self._account._account_list: @@ -3627,12 +3661,12 @@ def _init_serial(self, root_list, width, default, adj_type): temp_df = pd.DataFrame() temp_df._mgr = bm serial["df"] = TqDataFrame(self, temp_df, copy=False) - serial["df"]["symbol"] = root_list[0]["_path"][1] + serial["df"]["symbol"] = root_list[0]._path[1] for i in range(1, len(root_list)): - serial["df"]["symbol" + str(i)] = root_list[i]["_path"][1] + serial["df"]["symbol" + str(i)] = root_list[i]._path[1] - serial["df"]["duration"] = 0 if root_list[0]["_path"][0] == "ticks" else int( - root_list[0]["_path"][-1]) // 1000000000 + serial["df"]["duration"] = 0 if root_list[0]._path[0] == "ticks" else int( + root_list[0]._path[-1]) // 1000000000 return serial def _update_serial_single(self, serial): @@ -3667,39 +3701,69 @@ def _update_serial_single(self, serial): if serial["root"][0].get("last_id", -1) == -1: # 该 kline 没有任何数据,直接退出 return - symbol = serial["chart"]["ins_list"].split(",")[0] # 合约列表 + # Cache computed values that don't change between calls + try: + keys = serial["_cached_keys"] + symbol = serial["_cached_symbol"] + cols = serial["_cached_cols"] + except KeyError: + symbol = serial["chart"]["ins_list"].split(",")[0] + serial["_cached_symbol"] = symbol + keys = list(serial["default"].keys()) + keys.remove('datetime') + serial["_cached_keys"] = keys + duration = serial["chart"]["duration"] + if duration != 0: + cols = ["open", "high", "low", "close"] + else: + cols = ["last_price", "highest", "lowest"] + [f"{x}{i}" for x in + ["bid_price", "ask_price"] for i in + range(1, 6)] + serial["_cached_cols"] = cols quote = self._data.quotes.get(symbol, {}) - duration = serial["chart"]["duration"] # 周期 - keys = list(serial["default"].keys()) - keys.remove('datetime') - if duration != 0: - cols = ["open", "high", "low", "close"] - else: - cols = ["last_price", "highest", "lowest"] + [f"{x}{i}" for x in - ["bid_price", "ask_price"] for i in - range(1, 6)] - for i in range(serial["update_row"], serial["width"]): - index = last_id - serial["width"] + 1 + i - item = serial["default"] if index < 0 else _get_obj(serial["root"][0], ["data", str(index)], - serial["default"]) + adj_type = serial["adj_type"] + root0 = serial["root"][0] + default = serial["default"] + width = serial["width"] + update_row = serial["update_row"] + needs_adj = adj_type in ("B", "F") and hasattr(quote, 'ins_class') and quote.ins_class in ("STOCK", "FUND") + # Cache the "data" sub-entity to avoid 2-level _get_obj lookup per iteration + root0_data_entity = _get_obj_single(root0, "data") + root0_data_entity_data = root0_data_entity._data + # Pre-fetch default's _data for direct dict access (bypasses Entity.__getitem__) + default_data = default._data + for i in range(update_row, width): + index = last_id - width + 1 + i + if index < 0: + item_data = default_data + else: + str_index = str(index) + try: + item_data = root0_data_entity_data[str_index]._data + except KeyError: + item_data = _get_obj_single(root0_data_entity, str_index, default)._data # 如果需要复权,计算复权 - if index > 0 and serial["adj_type"] in ["B", "F"] and quote.ins_class in ["STOCK", "FUND"]: + if index > 0 and needs_adj: self._ensure_dividend_factor(symbol) last_index = index - 1 - last_item = _get_obj(serial["root"][0], ["data", str(last_index)], serial["default"]) - factor = get_dividend_factor(self._dividend_cache[symbol]["df"], last_item, item) - if serial["adj_type"] == "B": + str_last_index = str(last_index) + try: + last_item = root0_data_entity_data[str_last_index] + except KeyError: + last_item = _get_obj_single(root0_data_entity, str_last_index, default) + factor = get_dividend_factor(self._dividend_cache[symbol]["df"], last_item, item_data) + if adj_type == "B": self._dividend_cache[symbol]["back_factor"] = self._dividend_cache[symbol]["back_factor"] * (1 / factor) if self._dividend_cache[symbol]["back_factor"] != 1.0: - item = item.copy() + item_data = item_data.copy() for c in cols: - item[c] = item[c] * self._dividend_cache[symbol]["back_factor"] - elif serial["adj_type"] == "F" and factor != 1.0 and i not in serial['calc_ids_F']: + item_data[c] = item_data[c] * self._dividend_cache[symbol]["back_factor"] + elif adj_type == "F" and factor != 1.0 and i not in serial['calc_ids_F']: serial['calc_ids_F'].append(i) for c in cols: col_index = keys.index(c) + 2 array[:i, col_index] = array[:i, col_index] * factor - array[i] = [item["datetime"]] + [index] + [item[k] for k in keys if k != "datetime"] + array[i] = [item_data["datetime"], index] + [item_data[k] for k in keys] def _ensure_dividend_factor(self, symbol): quote = self._data.quotes.get(symbol, {}) @@ -3828,20 +3892,36 @@ def _update_serial_multi(self, serial): array[-1, 1] + 1) # array[-1, 1] + 1: 保持左闭右开规范 def _process_serial_extra_array(self, serial): - for col in set(serial["df"].columns.values) - serial["default_attr"]: + """Process extra columns. Returns True if chart data was sent.""" + # Fast path: skip when no extra columns and no pending updates + if not serial["extra_array"]: + if serial["update_row"] == serial["width"]: + return False + # No extra columns but update_row != width: check if user added columns + # Use cached column identity to avoid expensive set() computation + df_cols_obj = serial["df"].columns + if serial.get("_last_cols_id") == id(df_cols_obj): + # Columns haven't changed, no extra columns to process + serial["update_row"] = serial["width"] + return False + serial["_last_cols_id"] = id(df_cols_obj) + df_cols = set(serial["df"].columns.values) + extra_cols = df_cols - serial["default_attr"] + for col in extra_cols: if col not in serial["all_attr"]: serial["update_row"] = 0 # 只有在第一次添加某个列时,才会赋值为 0。 # klines["ma_MAIN"] = ma.ma 不是在原来的序列上原地修改,而是返回一个新的序列, # 所以这里 serial["extra_array"] 中的列每次需要重新赋值。 serial["extra_array"][col] = serial["df"][col].to_numpy() # 如果策略中删除了之前添加到 df 中的序列,则 extra_array 中也将其删除 - for col in serial["all_attr"] - set(serial["df"].columns.values): + for col in serial["all_attr"] - df_cols: del serial["extra_array"][col] - serial["all_attr"] = set(serial["df"].columns.values) + serial["all_attr"] = df_cols + serial["_last_cols_id"] = id(serial["df"].columns) if serial["update_row"] == serial["width"]: - return - symbol = serial["root"][0]["_path"][1] # 主合约的symbol,标志绘图的主合约 - duration = 0 if serial["root"][0]["_path"][0] == "ticks" else int(serial["root"][0]["_path"][-1]) + return False + symbol = serial["root"][0]._path[1] # 主合约的symbol,标志绘图的主合约 + duration = 0 if serial["root"][0]._path[0] == "ticks" else int(serial["root"][0]._path[-1]) cols = list(serial["extra_array"].keys()) # 归并数据序列 while len(cols) != 0: @@ -3853,6 +3933,7 @@ def _process_serial_extra_array(self, serial): self._process_chart_data_for_web(serial, symbol, duration, col, serial["width"] - serial["update_row"], int(serial["array"][-1, 1]) + 1, data) serial["update_row"] = serial["width"] + return True def _process_chart_data_for_web(self, serial, symbol, duration, col, count, right, data): # 与 _process_chart_data 函数功能类似,但是处理成符合 diff 协议的序列,在 js 端就不需要特殊处理了 @@ -4031,8 +4112,8 @@ def _gen_security_prototype(self): @staticmethod def _deep_copy_dict(source, dest): - for key, value in source.__dict__.items(): - if isinstance(value, Entity): + for key, value in source._data.items(): + if hasattr(value, '_data'): dest[key] = {} TqApi._deep_copy_dict(value, dest[key]) else: @@ -4214,7 +4295,7 @@ def draw_report(self, report_datas): def _send_chart_data(self, base_kserial_frame, serial_id, serial_data): s = self._serials[id(base_kserial_frame)] - p = s["root"][0]["_path"] + p = s["root"][0]._path symbol = p[-2] dur_nano = int(p[-1]) pack = { diff --git a/tqsdk/auth.py b/tqsdk/auth.py index 0c04099b..e3b2de62 100644 --- a/tqsdk/auth.py +++ b/tqsdk/auth.py @@ -212,3 +212,62 @@ def _has_td_grants(self, symbol): if symbol.split('.', 1)[0] in (FUTURE_EXCHANGES + KQ_EXCHANGES) and self._has_feature("futr"): return True raise Exception(f"您的账户不支持交易 {symbol},需要购买后才能使用。升级网址:https://www.shinnytech.com/tqsdk-buy/") + + +class TqAuthDummy(object): + """无认证桩类,授予所有权限,不做任何网络调用。""" + + def __init__(self): + self._user_name = "local_user" + self._password = "" + self._access_token = "" + self._refresh_token = "" + self._auth_id = "" + self._mode = "real" + self._grants = {"features": [], "accounts": []} + self._expire_datetime = None + self._expire_days_left = None + self._product_type = None + self._logger = ShinnyLoggerAdapter( + logging.getLogger("TqApi.TqAuth"), + headers=self._base_headers, + grants=self._grants, + ) + + @property + def _base_headers(self): + return { + "User-Agent": "tqsdk-python %s" % __version__, + "Accept": "application/json", + } + + @property + def expire_datetime(self): + return datetime.datetime(2099, 12, 31, 23, 59, 59, tzinfo=_cst_tz) + + def init(self, mode="real"): + self._mode = mode + + def login(self): + pass + + def _has_feature(self, feature): + return True + + def _has_account(self, account): + return True + + def _has_md_grants(self, symbol): + return True + + def _has_td_grants(self, symbol): + return True + + def _add_account(self, account_id): + return True + + def _get_td_url(self, broker_id, account_id): + raise Exception("无认证模式不支持 OTG 实盘交易,请提供 TqAuth 参数。") + + def _get_md_url(self, stock, backtest): + raise Exception("无认证模式无法自动发现行情服务器,请通过 url 参数或 TQ_MD_URL 环境变量指定。") diff --git a/tqsdk/backtest.py b/tqsdk/backtest.py index f8ec5b88..ff31cff8 100644 --- a/tqsdk/backtest.py +++ b/tqsdk/backtest.py @@ -232,7 +232,7 @@ def _update_valid_quotes(self, quotes): async def _send_snapshot(self): """发送初始合约信息""" async with TqChan(self._api, last_only=True) as update_chan: # 等待与行情服务器连接成功 - self._data["_listener"].add(update_chan) + self._data._add_listener(update_chan) while self._data.get("mdhis_more_data", True): await update_chan.recv() # 发送初始行情(合约信息截面)时 @@ -431,7 +431,7 @@ async def _ensure_query(self, pack): if query_pack.items() <= self._data.get("symbols", {}).get(pack["query_id"], {}).items(): return async with TqChan(self._api, last_only=True) as update_chan: - self._data["_listener"].add(update_chan) + self._data._add_listener(update_chan) while not query_pack.items() <= self._data.get("symbols", {}).get(pack["query_id"], {}).items(): await update_chan.recv() @@ -442,10 +442,12 @@ async def _ensure_quote(self, ins): query_pack = _query_for_quote(ins) await self._md_send_chan.send(query_pack) async with TqChan(self._api, last_only=True) as update_chan: - quote["_listener"].add(update_chan) + quote._add_listener(update_chan) while math.isnan(quote.get("price_tick")): await update_chan.recv() - if ins not in self._quotes or self._quotes[ins]["min_duration"] > 60000000000: + # if ins not in self._quotes or self._quotes[ins]["min_duration"] > 60000000000: + # await self._ensure_serial(ins, 60000000000) + if ins not in self._quotes: await self._ensure_serial(ins, 60000000000) async def _fetch_serial(self, key): @@ -481,9 +483,9 @@ async def _gen_serial(self, ins, dur): serials = [_get_obj(self._data, ["klines", s, str(dur)]) for s in symbol_list] async with TqChan(self._api, last_only=True) as update_chan: for serial in serials: - serial["_listener"].add(update_chan) - chart_a["_listener"].add(update_chan) - chart_b["_listener"].add(update_chan) + serial._add_listener(update_chan) + chart_a._add_listener(update_chan) + chart_b._add_listener(update_chan) await self._md_send_chan.send(chart_info.copy()) try: async for _ in update_chan: @@ -499,10 +501,11 @@ async def _gen_serial(self, ins, dur): if last_id == -1: continue # 数据序列还没收到 if self._data.get("mdhis_more_data", True): - self._data["_listener"].add(update_chan) + self._data._add_listener(update_chan) continue else: - self._data["_listener"].discard(update_chan) + if self._data._listener is not None: + self._data._listener.discard(update_chan) if current_id is None: current_id = max(left_id, 0) # 发送下一段 chart 8964 根 kline diff --git a/tqsdk/backtest/backtest.py b/tqsdk/backtest/backtest.py index 8007be5d..416d43ae 100644 --- a/tqsdk/backtest/backtest.py +++ b/tqsdk/backtest/backtest.py @@ -239,7 +239,7 @@ def _update_valid_quotes(self, quotes): async def _send_snapshot(self): """发送初始合约信息""" async with TqChan(self._api, last_only=True) as update_chan: # 等待与行情服务器连接成功 - self._data["_listener"].add(update_chan) + self._data._add_listener(update_chan) while self._data.get("mdhis_more_data", True): await update_chan.recv() # 发送初始行情(合约信息截面)时 @@ -448,7 +448,7 @@ async def _ensure_query(self, pack): if query_pack.items() <= self._data.get("symbols", {}).get(pack["query_id"], {}).items(): return async with TqChan(self._api, last_only=True) as update_chan: - self._data["_listener"].add(update_chan) + self._data._add_listener(update_chan) while not query_pack.items() <= self._data.get("symbols", {}).get(pack["query_id"], {}).items(): await update_chan.recv() @@ -462,12 +462,14 @@ async def _ensure_symbols(self, symbols): await self._md_send_chan.send(query_pack) async with TqChan(self._api, last_only=True) as update_chan: for q in quotes: - q["_listener"].add(update_chan) + q._add_listener(update_chan) while any([math.isnan(q.get("price_tick")) for q in quotes]): await update_chan.recv() async def _ensure_quote(self, symbol): - if symbol not in self._quotes or self._quotes[symbol]["min_duration"] > 60000000000: + # if symbol not in self._quotes or self._quotes[symbol]["min_duration"] > 60000000000: + # await self._ensure_serial(symbol, 60000000000) + if symbol not in self._quotes: await self._ensure_serial(symbol, 60000000000) async def _fetch_serial(self, key): @@ -503,9 +505,9 @@ async def _gen_serial(self, ins, dur): serials = [_get_obj(self._data, ["klines", s, str(dur)]) for s in symbol_list] async with TqChan(self._api, last_only=True) as update_chan: for serial in serials: - serial["_listener"].add(update_chan) - chart_a["_listener"].add(update_chan) - chart_b["_listener"].add(update_chan) + serial._add_listener(update_chan) + chart_a._add_listener(update_chan) + chart_b._add_listener(update_chan) await self._md_send_chan.send(chart_info.copy()) try: async for _ in update_chan: @@ -532,10 +534,11 @@ async def _gen_serial(self, ins, dur): yield self._current_dt, diff, None, "OPEN" return if self._data.get("mdhis_more_data", True): - self._data["_listener"].add(update_chan) + self._data._add_listener(update_chan) continue else: - self._data["_listener"].discard(update_chan) + if self._data._listener is not None: + self._data._listener.discard(update_chan) left_id = chart.get("left_id", -1) right_id = chart.get("right_id", -1) if current_id is None: diff --git a/tqsdk/channel.py b/tqsdk/channel.py index ee1946c1..5c9b65b7 100644 --- a/tqsdk/channel.py +++ b/tqsdk/channel.py @@ -47,6 +47,7 @@ def __init__(self, api: 'TqApi', last_only: bool = False, logger: Union[Logger, asyncio.Queue.__init__(self, loop=api._loop) if (py_ver.major == 3 and py_ver.minor < 10) else asyncio.Queue.__init__(self) self._last_only = last_only self._closed = False + self._log_enabled = self._logger.logger.isEnabledFor(TqChan._level) def _logger_bind(self, **kwargs): self._logger = self._logger.bind(**kwargs) @@ -83,11 +84,11 @@ async def send(self, item: Any) -> None: item (any): 待发送的对象 """ if not self._closed: - if self._last_only: - while not self.empty(): - asyncio.Queue.get_nowait(self) + if self._last_only and self._queue: + self._queue.clear() await asyncio.Queue.put(self, item) - self._logger.log(TqChan._level, "tqchan send", item=self._sanitize_log_item(item)) + if self._log_enabled: + self._logger.log(TqChan._level, "tqchan send", item=self._sanitize_log_item(item)) def send_nowait(self, item: Any) -> None: """ @@ -100,11 +101,11 @@ def send_nowait(self, item: Any) -> None: asyncio.QueueFull: 如果channel已满则会抛出 asyncio.QueueFull """ if not self._closed: - if self._last_only: - while not self.empty(): - asyncio.Queue.get_nowait(self) + if self._last_only and self._queue: + self._queue.clear() asyncio.Queue.put_nowait(self, item) - self._logger.log(TqChan._level, "tqchan send_nowait", item=self._sanitize_log_item(item)) + if self._log_enabled: + self._logger.log(TqChan._level, "tqchan send_nowait", item=self._sanitize_log_item(item)) async def recv(self) -> Any: """ @@ -116,7 +117,8 @@ async def recv(self) -> Any: if self._closed and self.empty(): return None item = await asyncio.Queue.get(self) - self._logger.log(TqChan._level, "tqchan recv", item=self._sanitize_log_item(item)) + if self._log_enabled: + self._logger.log(TqChan._level, "tqchan recv", item=self._sanitize_log_item(item)) return item def recv_nowait(self) -> Any: @@ -132,7 +134,8 @@ def recv_nowait(self) -> Any: if self._closed and self.empty(): return None item = asyncio.Queue.get_nowait(self) - self._logger.log(TqChan._level, "tqchan recv_nowait", item=self._sanitize_log_item(item)) + if self._log_enabled: + self._logger.log(TqChan._level, "tqchan recv_nowait", item=self._sanitize_log_item(item)) return item def recv_latest(self, latest: Any) -> Any: @@ -147,7 +150,8 @@ def recv_latest(self, latest: Any) -> Any: """ while (self._closed and self.qsize() > 1) or (not self._closed and not self.empty()): latest = asyncio.Queue.get_nowait(self) - self._logger.log(TqChan._level, "tqchan recv_latest", item=self._sanitize_log_item(latest)) + if self._log_enabled: + self._logger.log(TqChan._level, "tqchan recv_latest", item=self._sanitize_log_item(latest)) return latest def __aiter__(self): @@ -157,7 +161,8 @@ async def __anext__(self): value = await asyncio.Queue.get(self) if self._closed and self.empty(): raise StopAsyncIteration - self._logger.log(TqChan._level, "tqchan recv_next", item=self._sanitize_log_item(value)) + if self._log_enabled: + self._logger.log(TqChan._level, "tqchan recv_next", item=self._sanitize_log_item(value)) return value async def __aenter__(self): diff --git a/tqsdk/data_extension.py b/tqsdk/data_extension.py index 0dfb741e..efdb4986 100644 --- a/tqsdk/data_extension.py +++ b/tqsdk/data_extension.py @@ -3,6 +3,8 @@ __author__ = 'mayanqiong' +from itertools import islice + from tqsdk.datetime import _get_expire_rest_days from tqsdk.datetime_state import TqDatetimeState from tqsdk.diff import _simple_merge_diff, _is_key_exist, _simple_merge_diff_and_collect_paths, _get_obj @@ -139,7 +141,7 @@ def _generate_ext_diff(self): pend_diff = {} _simple_merge_diff(pend_diff, self._get_positions_pend_diff()) orders_set = set() # 计算过委托单,is_dead、is_online、is_error - orders_price_set = set() # 根据成交计算哪些 order 需要重新计算平均成交价 trade_price + orders_price_updates = {} # (account_key, order_id) -> need recalc for path in self._diffs_paths: if path[2] == 'orders': _, account_key, _, order_id, _ = path @@ -156,11 +158,25 @@ def _generate_ext_diff(self): trade = _get_obj(self._data, path) order_id = trade.get('order_id', '') if order_id: - orders_price_set.add(('trade', account_key, 'orders', order_id)) - for path in orders_price_set: - _, account_key, _, order_id = path - trade_price = self._get_trade_price(account_key, order_id) - if trade_price == trade_price: + # Accumulate running sums directly instead of building separate index + key = (account_key, order_id) + if key not in orders_price_updates: + # Get existing sums from cache or start fresh + if not hasattr(self, '_trade_price_cache'): + self._trade_price_cache = {} + entry = self._trade_price_cache.get(key) + if entry is None: + entry = [0, 0.0] + self._trade_price_cache[key] = entry + orders_price_updates[key] = entry + vol = trade.get('volume', 0) + price = trade.get('price', 0.0) + entry = orders_price_updates[key] + entry[0] += vol + entry[1] += vol * price + for (account_key, order_id), entry in orders_price_updates.items(): + if entry[0] > 0: + trade_price = entry[1] / entry[0] pend_order = pend_diff.setdefault('trade', {}).setdefault(account_key, {}).setdefault('orders', {}).setdefault(order_id, {}) pend_order['trade_price'] = trade_price self._diffs_paths = set() @@ -194,15 +210,39 @@ def _get_positions_pend_diff(self): return {'trade': pend_diff} if pend_diff else {} def _get_trade_price(self, account_key, order_id): - # 计算某个 order_id 对应的成交均价 + # 计算某个 order_id 对应的成交均价, using reverse index with cached running sums trades = self._data['trade'][account_key]['trades'] - trade_id_list = [t_id for t_id in trades.keys() if trades[t_id]['order_id'] == order_id] - sum_volume = sum([trades[t_id]['volume'] for t_id in trade_id_list]) - if sum_volume == 0: + # Build/update reverse index: order_id -> [sum_volume, sum_amount] + try: + idx_account = self._trade_order_index[account_key] + old_len = self._trade_order_index_len[account_key] + except (AttributeError, KeyError): + if not hasattr(self, '_trade_order_index'): + self._trade_order_index = {} + self._trade_order_index_len = {} + idx_account = {} + self._trade_order_index[account_key] = idx_account + old_len = 0 + self._trade_order_index_len[account_key] = 0 + cur_len = len(trades) + if cur_len != old_len: + # New trades added — update running sums incrementally + for t_id, trade in islice(trades.items(), old_len, None): + oid = trade.get('order_id', '') + if oid: + vol = trade.get('volume', 0) + price = trade.get('price', 0.0) + entry = idx_account.get(oid) + if entry is None: + idx_account[oid] = [vol, vol * price] + else: + entry[0] += vol + entry[1] += vol * price + self._trade_order_index_len[account_key] = cur_len + entry = idx_account.get(order_id) + if entry is None or entry[0] == 0: return float('nan') - else: - sum_amount = sum([trades[t_id]['volume'] * trades[t_id]['price'] for t_id in trade_id_list]) - return sum_amount / sum_volume + return entry[1] / entry[0] async def _send_diff(self): if self._datetime_state.data_ready and self._pending_peek and self._diffs: diff --git a/tqsdk/datetime.py b/tqsdk/datetime.py index 5234baef..d281cd8c 100644 --- a/tqsdk/datetime.py +++ b/tqsdk/datetime.py @@ -60,10 +60,20 @@ def _timestamp_nano_to_datetime(nano: int) -> datetime.datetime: def _timestamp_nano_to_str(nano: int, fmt="%Y-%m-%d %H:%M:%S.%f") -> str: + if fmt == "%Y-%m-%d %H:%M:%S.%f": + dt = datetime.datetime.fromtimestamp((nano // 1000) / 1000000, tz=_cst_tz) + return f"{dt.year:04d}-{dt.month:02d}-{dt.day:02d} {dt.hour:02d}:{dt.minute:02d}:{dt.second:02d}.{dt.microsecond:06d}" return datetime.datetime.fromtimestamp((nano // 1000) / 1000000, tz=_cst_tz).strftime(fmt) def _str_to_timestamp_nano(current_datetime: str, fmt="%Y-%m-%d %H:%M:%S.%f") -> int: + if fmt == "%Y-%m-%d %H:%M:%S.%f": + # Fast path: parse "YYYY-MM-DD HH:MM:SS.ffffff" directly + dt = datetime.datetime(int(current_datetime[0:4]), int(current_datetime[5:7]), + int(current_datetime[8:10]), int(current_datetime[11:13]), + int(current_datetime[14:16]), int(current_datetime[17:19]), + int(current_datetime[20:26]), tzinfo=_cst_tz) + return int(dt.timestamp() * 1000000) * 1000 return _datetime_to_timestamp_nano(datetime.datetime.strptime(current_datetime, fmt)) @@ -96,35 +106,56 @@ def _get_trading_day_from_timestamp(timestamp): def _get_trading_timestamp(quote, current_datetime: str): """ 将 quote 在 current_datetime 所在交易日的所有可交易时间段转换为纳秒时间戳(tqsdk内部使用的时间戳统一为纳秒)并返回 """ - # 获取当前交易日时间戳 - current_trading_day_timestamp = _get_trading_day_from_timestamp(_str_to_timestamp_nano(current_datetime)) - # 获取上一交易日时间戳 + return _get_trading_timestamp_nano(quote, _str_to_timestamp_nano(current_datetime)) + + +_trading_timestamp_nano_cache = {} + +def _get_trading_timestamp_nano(quote, nano_timestamp: int): + """ 将 quote 在 nano_timestamp 所在交易日的所有可交易时间段转换为纳秒时间戳并返回 """ + current_trading_day_timestamp = _get_trading_day_from_timestamp(nano_timestamp) + trading_time = quote["trading_time"] + cache_key = (id(trading_time), current_trading_day_timestamp) + cached = _trading_timestamp_nano_cache.get(cache_key) + if cached is not None: + return cached last_trading_day_timestamp = _get_trading_day_from_timestamp( _get_trading_day_start_time(current_trading_day_timestamp) - 1) trading_timestamp = { - "day": _get_period_timestamp(current_trading_day_timestamp, quote["trading_time"].get("day", [])), - "night": _get_period_timestamp(last_trading_day_timestamp, quote["trading_time"].get("night", [])) + "day": _get_period_timestamp(current_trading_day_timestamp, trading_time.get("day", [])), + "night": _get_period_timestamp(last_trading_day_timestamp, trading_time.get("night", [])) } + _trading_timestamp_nano_cache[cache_key] = trading_timestamp return trading_timestamp +_period_offsets_cache = {} + def _get_period_timestamp(real_date_timestamp, period_str): """ real_date_timestamp:period_str 所在真实日期的纳秒时间戳(如 period_str 为周一(周二)的夜盘,则real_date_timestamp为上周五(周一)的日期; period_str 为周一的白盘,则real_date_timestamp为周一的日期) period_str: quote["trading_time"]["day"] or quote["trading_time"]["night"] """ - period_timestamp = [] - for duration in period_str: # 对于白盘(或夜盘)中的每一个可交易时间段 - start = [int(i) for i in duration[0].split(":")] # 交易时间段起始点 - end = [int(i) for i in duration[1].split(":")] # 交易时间段结束点 - period_timestamp.append([real_date_timestamp + (start[0] * 3600 + start[1] * 60 + start[2]) * 1000000000, - real_date_timestamp + (end[0] * 3600 + end[1] * 60 + end[2]) * 1000000000]) - return period_timestamp + # Cache parsed time offsets (nanoseconds within a day) keyed by period_str identity + period_id = id(period_str) + offsets = _period_offsets_cache.get(period_id) + if offsets is None: + offsets = [] + for duration in period_str: + start = [int(i) for i in duration[0].split(":")] + end = [int(i) for i in duration[1].split(":")] + offsets.append(((start[0] * 3600 + start[1] * 60 + start[2]) * 1000000000, + (end[0] * 3600 + end[1] * 60 + end[2]) * 1000000000)) + _period_offsets_cache[period_id] = offsets + return [[real_date_timestamp + s, real_date_timestamp + e] for s, e in offsets] def _is_in_trading_time(quote, current_datetime, local_time_record): """ 判断是否在可交易时间段内,需在quote已收到行情后调用本函数""" # 只在需要用到可交易时间段时(即本函数中)才调用_get_trading_timestamp() + time_part = current_datetime.split(' ')[1] if ' ' in current_datetime else '' + if time_part in ('18:00:00.000000', '17:59:59.999999'): + return True trading_timestamp = _get_trading_timestamp(quote, current_datetime) now_ns_timestamp = _get_trade_timestamp(current_datetime, local_time_record) # 当前预估交易所纳秒时间戳 # 判断当前交易所时间(估计值)是否在交易时间段内 @@ -135,6 +166,21 @@ def _is_in_trading_time(quote, current_datetime, local_time_record): return False +def _is_in_trading_time_nano(quote, nano_timestamp): + """ 判断是否在可交易时间段内 - 接受纳秒时间戳避免 str<->nano 转换开销 """ + # Check for 18:00:00 and 17:59:59 boundaries in nano + # Time-of-day in CST: offset from midnight in nanos + day_offset = (nano_timestamp - 631123200000000000) % 86400000000000 # offset within day + if day_offset == 64800000000000 or day_offset == 64799999999000: # 18:00:00.000000 or 17:59:59.999999 + return True + trading_timestamp = _get_trading_timestamp_nano(quote, nano_timestamp) + for v in trading_timestamp.values(): + for period in v: + if period[0] <= nano_timestamp < period[1]: + return True + return False + + def _get_trade_timestamp(current_datetime, local_time_record): # 根据最新行情时间获取模拟的(预估的)当前交易所纳秒时间戳(tqsdk内部使用的时间戳统一为纳秒) # 如果local_time_record为nan,則不加时间差 diff --git a/tqsdk/datetime_state.py b/tqsdk/datetime_state.py index 5fcd423a..3dbda04a 100644 --- a/tqsdk/datetime_state.py +++ b/tqsdk/datetime_state.py @@ -26,7 +26,9 @@ def get_current_dt(self): return int(time.time() * 1000000) * 1000 def update_state(self, diff): - self.tqsdk_backtest.update(diff.get('_tqsdk_backtest', {})) + backtest_data = diff.get('_tqsdk_backtest') + if backtest_data: + self.tqsdk_backtest.update(backtest_data) if not self.data_ready and diff.get('mdhis_more_data', True) is False: self.data_ready = True diff --git a/tqsdk/diff.py b/tqsdk/diff.py index 5dd89adc..e7480ba7 100644 --- a/tqsdk/diff.py +++ b/tqsdk/diff.py @@ -11,80 +11,125 @@ def _merge_diff(result, diff, prototype, persist, reduce_diff=False, notify_update_diff=False): """ 更新业务数据,并同步发送更新通知,保证业务数据的更新和通知是原子操作 - :param result: 更新结果 - :param diff: diff pack - :param persist: 是否保留 result 中所有字段,如果为 True,则 diff 里为 None 的对象在 result 中不会删除;如果是 False,则 result 结果中会删除 diff 里为 None 的对象 - :param reduce_diff: 表示是否修改 diff 对象本身,如果为 True 函数运行完成后,diff 会更新为与 result 真正的有区别的字段;如果为 False,diff 不会修改 - 默认不会修改 diff,只有 api 中 is_changing 接口需要 diffs 为真正有变化的数据 - :param notify_update_diff: 为 True 表示发送更新通知的发送的是包含 diff 的完整数据包(方便 TqSim 中能每个合约的 task 可以单独维护自己的数据),反之只发送 True - :return: """ - for key in list(diff.keys()): - value_type = type(diff[key]) - if value_type is str and key in prototype and not type(prototype[key]) is str: - diff[key] = prototype[key] - if diff[key] is None: - if (persist or "#" in prototype) and reduce_diff: + _type = type + _Entity = Entity + _dict = dict + result_data = result._data + # Access prototype._data directly to bypass Entity method dispatch + try: + _pd = prototype._data + except AttributeError: + _pd = prototype + # Only need tuple(diff) when reduce_diff may delete keys during iteration + for key in (tuple(diff) if reduce_diff else diff): + val = diff[key] + value_type = _type(val) + if value_type is str and key in _pd and _type(_pd[key]) is not str: + val = _pd[key] + diff[key] = val + if val is None: + if (persist or "#" in _pd) and reduce_diff: del diff[key] else: - if notify_update_diff: - dv = result.pop(key, None) - _notify_update(dv, True, _gen_diff_obj(None, result["_path"] + [key])) - else: - dv = result.pop(key, None) - _notify_update(dv, True, True) - elif value_type is dict or value_type is Entity: + dv = result_data.pop(key, None) + if dv is not None: + if notify_update_diff: + _notify_update(dv, True, _gen_diff_obj(None, result._path + [key])) + else: + _notify_update(dv, True, True) + elif value_type is _dict or value_type is _Entity: default = None tpersist = persist - if key in prototype: - tpt = prototype[key] - elif "*" in prototype: - tpt = prototype["*"] - elif "@" in prototype: - tpt = prototype["@"] + if key in _pd: + tpt = _pd[key] + elif "*" in _pd: + tpt = _pd["*"] + elif "@" in _pd: + tpt = _pd["@"] default = tpt - elif "#" in prototype: - tpt = prototype["#"] + elif "#" in _pd: + tpt = _pd["#"] default = tpt tpersist = True else: tpt = {} - target = _get_obj(result, [key], default=default) - _merge_diff(target, diff[key], tpt, persist=tpersist, reduce_diff=reduce_diff, notify_update_diff=notify_update_diff) - if reduce_diff and len(diff[key]) == 0: + try: + target = result_data[key] + except KeyError: + if default is None: + target = _Entity() + else: + target = copy.copy(default) + target._instance_entity(result._path + [key]) + result_data[key] = target + _merge_diff(target, val, tpt, tpersist, reduce_diff, notify_update_diff) + if reduce_diff and not val: + del diff[key] + elif reduce_diff and key in result_data: + rval = result_data[key] + if rval == val or (val != val and rval != rval): del diff[key] - elif reduce_diff and key in result and ( - result[key] == diff[key] or (diff[key] != diff[key] and result[key] != result[key])): - # 判断 diff[key] != diff[key] and result[key] != result[key] 以处理 value 为 nan 的情况 - del diff[key] + else: + result_data[key] = val else: - result[key] = diff[key] - if len(diff) != 0: - diff_obj = True + result_data[key] = val + if diff: + # Inline _notify_update for non-recursive case (saves ~1M function calls) if notify_update_diff: - # 这里发的数据目前是不需要 copy (浅拷贝会有坑,深拷贝的话性能不知道有多大影响) - # 因为这里现在会用到发送这个 diff 的只有 quote 对象,只有 sim 会收到使用,sim 收到之后是不会修改这个 diff - # 所以这里就约定接收方不能改 diff 中的值 - diff_obj = _gen_diff_obj(diff, result["_path"]) - _notify_update(result, False, diff_obj) + diff_obj = _gen_diff_obj(diff, result._path) + else: + diff_obj = True + try: + listener = result._listener + except AttributeError: + return + if listener is not None and listener.data: + for q in listener: + q.send_nowait(diff_obj) def _gen_diff_obj(diff, path): """将 diff 根据 path 拼成一个完整的 diff 包""" diff_obj = diff - for i in range(len(path)): - diff_obj = {path[len(path)-i-1]: diff_obj} + for p in reversed(path): + diff_obj = {p: diff_obj} return diff_obj def _notify_update(target, recursive, content): """同步通知业务数据更新""" - if isinstance(target, dict) or isinstance(target, Entity): - for q in getattr(target, "_listener", {}): - q.send_nowait(content) + target_type = type(target) + if target_type is dict: if recursive: for v in target.values(): _notify_update(v, recursive, content) + return + try: + listener = target._listener + except AttributeError: + return + if listener is not None and listener.data: + for q in listener: + q.send_nowait(content) + if recursive: + for v in target._data.values(): + _notify_update(v, recursive, content) + + +def _get_obj_single(root, key, default=None): + """获取业务数据 - optimized for single key lookup (most common case)""" + root_data = root._data + try: + return root_data[key] + except KeyError: + if default is None: + dv = Entity() + else: + dv = copy.copy(default) + dv._instance_entity(root._path + [key]) + root_data[key] = dv + return dv def _get_obj(root, path, default=None): @@ -96,7 +141,7 @@ def _get_obj(root, path, default=None): dv = Entity() else: dv = copy.copy(default) - dv._instance_entity(d["_path"] + [path[i]]) + dv._instance_entity(d._path + [path[i]]) d[path[i]] = dv d = d[path[i]] return d @@ -106,70 +151,70 @@ def _register_update_chan(objs, chan): if not isinstance(objs, list): objs = [objs] for o in objs: - o["_listener"].add(chan) + o._add_listener(chan) return chan def _is_key_exist(diff, path, key): """判断指定数据是否存在""" for p in path: - if not isinstance(diff, dict) or p not in diff: + if type(diff) is not dict or p not in diff: return False diff = diff[p] - if not isinstance(diff, dict): + if type(diff) is not dict: return False for k in key: if k in diff: return True - return len(key) == 0 + return not key def _simple_merge_diff(result, diff): """ 更新业务数据 - :param result: 更新结果 - :param diff: diff pack - :return: """ - for key in list(diff.keys()): - if diff[key] is None: + for key in diff: + val = diff[key] + if val is None: result.pop(key, None) - elif isinstance(diff[key], dict): + elif type(val) is dict: target = result.setdefault(key, {}) - _simple_merge_diff(target, diff[key]) + _simple_merge_diff(target, val) else: - result[key] = diff[key] + result[key] = val def _simple_merge_diff_and_collect_paths(result, diff, path: Tuple, diff_paths: Set, prototype: Union[Dict, None]): """ 更新业务数据并收集指定节点的路径 - 默认行为 reduce_diff=False,表示函数运行过程中不会修改 diff 本身 - :param result: 更新结果 - :param diff: diff pack - :param path: 当前迭代 merge_diff 的节点路径 - :param diff_paths: 收集指定节点的路径 - :param prototype: 数据原型, 为 None 的节点路径会被记录在 diff_paths 集合中 - :return: """ - for key in list(diff.keys()): - if diff[key] is None: + for key in diff: + val = diff[key] + if val is None: result.pop(key, None) - if prototype and ('*' in prototype or key in prototype) and prototype['*' if '*' in prototype else key] is None: - diff_paths.add(path + (key, )) - elif isinstance(diff[key], dict): + if prototype: + pkey = '*' if '*' in prototype else key + if pkey in prototype and prototype[pkey] is None: + diff_paths.add(path + (key, )) + elif type(val) is dict: target = result.setdefault(key, {}) - sub_path = path + (key, ) sub_prototype = None - if prototype and ('*' in prototype or key in prototype): - sub_prototype = prototype['*' if '*' in prototype else key] - if sub_prototype is None: - diff_paths.add(sub_path) - _simple_merge_diff_and_collect_paths(target, diff[key], path=sub_path, prototype=sub_prototype, diff_paths=diff_paths) - elif key in result and result[key] == diff[key]: + if prototype: + pkey = '*' if '*' in prototype else key + if pkey in prototype: + sub_prototype = prototype[pkey] + if sub_prototype is None: + diff_paths.add(path + (key, )) + if sub_prototype is not None: + _simple_merge_diff_and_collect_paths(target, val, path=path + (key, ), prototype=sub_prototype, diff_paths=diff_paths) + else: + # No further path collection needed in this subtree + _simple_merge_diff(target, val) + elif key in result and result[key] == val: pass else: - result[key] = diff[key] - # 只有确实有变更的字段,会出现在 diff_paths 里 - if prototype and ('*' in prototype or key in prototype) and prototype['*' if '*' in prototype else key] is None: - diff_paths.add(path + (key, )) + result[key] = val + if prototype: + pkey = '*' if '*' in prototype else key + if pkey in prototype and prototype[pkey] is None: + diff_paths.add(path + (key, )) diff --git a/tqsdk/entity.py b/tqsdk/entity.py index 317470ea..4c2f8f7d 100644 --- a/tqsdk/entity.py +++ b/tqsdk/entity.py @@ -6,33 +6,109 @@ import weakref from collections.abc import MutableMapping +_UNSET = object() + class Entity(MutableMapping): + __slots__ = ('_data', '_path', '_listener', '__dict__') + + def __new__(cls, *args, **kwargs): + instance = super().__new__(cls) + object.__setattr__(instance, '_data', {}) + return instance + def _instance_entity(self, path): - self._path = path - self._listener = weakref.WeakSet() + object.__setattr__(self, '_path', path) + object.__setattr__(self, '_listener', None) + + def __setattr__(self, key, value): + if key.startswith('_'): + object.__setattr__(self, key, value) + else: + self._data[key] = value + + def __getattr__(self, key): + try: + return self._data[key] + except KeyError: + raise AttributeError(key) + + def _add_listener(self, chan): + """Add a listener, lazily creating the WeakSet if needed.""" + listener = self._listener + if listener is None: + listener = weakref.WeakSet() + object.__setattr__(self, '_listener', listener) + listener.add(chan) + + def __delattr__(self, key): + if key.startswith('_'): + object.__delattr__(self, key) + else: + try: + del self._data[key] + except KeyError: + raise AttributeError(key) def __setitem__(self, key, value): - return self.__dict__.__setitem__(key, value) + self._data[key] = value def __delitem__(self, key): - return self.__dict__.__delitem__(key) + del self._data[key] def __getitem__(self, key): - return self.__dict__.__getitem__(key) + return self._data[key] def __iter__(self): - return iter({k: v for k, v in self.__dict__.items() if not k.startswith("_")}) + return iter(self._data) def __len__(self): - return len({k: v for k, v in self.__dict__.items() if not k.startswith("_")}) + return len(self._data) def __str__(self): - return str({k: v for k, v in self.__dict__.items() if not k.startswith("_")}) + return str(self._data) def __repr__(self): - return '{}, D({})'.format(super(Entity, self).__repr__(), - {k: v for k, v in self.__dict__.items() if not k.startswith("_")}) + return '{}, D({})'.format(super(Entity, self).__repr__(), self._data) + + def __contains__(self, key): + return key in self._data + + def get(self, key, default=None): + return self._data.get(key, default) + + def keys(self): + return self._data.keys() + + def values(self): + return self._data.values() + + def items(self): + return self._data.items() + + def pop(self, key, *args): + return self._data.pop(key, *args) + + def setdefault(self, key, default=None): + return self._data.setdefault(key, default) + + def __copy__(self): + new = type(self).__new__(type(self)) + # Copy slot attrs using getattr to avoid exception overhead for unset slots + _setattr = object.__setattr__ + _p = getattr(self, '_path', _UNSET) + if _p is not _UNSET: + _setattr(new, '_path', _p) + _l = getattr(self, '_listener', _UNSET) + if _l is not _UNSET: + _setattr(new, '_listener', _l) + # Copy any extra attrs from __dict__ + d = self.__dict__ + if d: + for k, v in d.items(): + _setattr(new, k, v) + _setattr(new, '_data', self._data.copy()) + return new def copy(self): return copy.copy(self) diff --git a/tqsdk/lib/target_pos_task.py b/tqsdk/lib/target_pos_task.py index 27642e41..b32f33f5 100644 --- a/tqsdk/lib/target_pos_task.py +++ b/tqsdk/lib/target_pos_task.py @@ -12,7 +12,7 @@ from tqsdk.api import TqApi from tqsdk.backtest import TqBacktest from tqsdk.channel import TqChan -from tqsdk.datetime import _is_in_trading_time, _timestamp_nano_to_str +from tqsdk.datetime import _is_in_trading_time, _is_in_trading_time_nano, _timestamp_nano_to_str from tqsdk.diff import _get_obj from tqsdk.lib.utils import _check_volume_limit, _check_direction, _check_offset, _check_volume, _check_price, \ _check_offset_priority @@ -105,7 +105,7 @@ def __init__(self, api: TqApi, symbol: str, price: Union[str, Callable[[str], Un offset_priority (str): [可选]开平仓顺序,昨=平昨仓,今=平今仓,开=开仓,逗号=等待之前操作完成 对于下单指令区分平今/昨的交易所(如上期所),按照今/昨仓的数量计算是否能平今/昨仓 - 对于下单指令不区分平今/昨的交易所(如中金所),按照“先平当日新开仓,再平历史仓”的规则计算是否能平今/昨仓,如果这些交易所设置为"昨开"在有当日新开仓和历史仓仓的情况下,会自动跳过平昨仓进入到下一步 + 对于下单指令不区分平今/昨的交易所(如中金所),按照"先平当日新开仓,再平历史仓"的规则计算是否能平今/昨仓,如果这些交易所设置为"昨开"在有当日新开仓和历史仓仓的情况下,会自动跳过平昨仓进入到下一步 * "今昨,开" 表示先平今仓,再平昨仓,等待平仓完成后开仓,对于没有单向大边的品种避免了开仓保证金不足 * "今昨开" 表示先平今仓,再平昨仓,并开仓,所有指令同时发出,适合有单向大边的品种 @@ -285,17 +285,18 @@ def set_target_volume(self, volume: int) -> None: raise Exception("已经结束的 TargetPosTask 实例不可以再设置手数。") self._pos_chan.send_nowait(int(volume)) - def _get_order(self, offset, vol, pending_frozen): + def _get_order(self, offset, vol, pending_frozen, pos_orders=None): """ 根据指定的offset和预期下单手数vol, 返回符合要求的委托单最大报单手数 :param offset: "昨" / "今" / "开" :param vol: int, <0表示SELL, >0表示BUY + :param pos_orders: cached result of self._pos.orders to avoid recomputation :return: order_offset: "CLOSE"/"CLOSETODAY"/"OPEN"; order_dir: "BUY"/"SELL"; "order_volume": >=0, 报单手数 """ - if vol > 0: # 买单(增加净持仓) + if vol > 0: order_dir = "BUY" pos_all = self._pos.pos_short - else: # 卖单 + else: order_dir = "SELL" pos_all = self._pos.pos_long if offset == "昨": @@ -305,12 +306,15 @@ def _get_order(self, offset, vol, pending_frozen): pos_all = self._pos.pos_short_his else: pos_all = self._pos.pos_long_his - frozen_volume = sum([order.volume_left for order in self._pos.orders.values() if - not order.is_dead and order.offset == order_offset and order.direction == order_dir]) + if pos_orders is None: + pos_orders = self._pos.orders + frozen_volume = sum(order.volume_left for order in pos_orders.values() if + not order.is_dead and order.offset == order_offset and order.direction == order_dir) else: - frozen_volume = pending_frozen + sum([order.volume_left for order in self._pos.orders.values() if - not order.is_dead and order.offset != "OPEN" and order.direction == order_dir]) - # 判断是否有未冻结的今仓手数: 若有则不平昨仓 + if pos_orders is None: + pos_orders = self._pos.orders + frozen_volume = pending_frozen + sum(order.volume_left for order in pos_orders.values() if + not order.is_dead and order.offset != "OPEN" and order.direction == order_dir) if (self._pos.pos_short_today if vol > 0 else self._pos.pos_long_today) - frozen_volume > 0: pos_all = frozen_volume order_volume = min(abs(vol), max(0, pos_all - frozen_volume)) @@ -321,12 +325,16 @@ def _get_order(self, offset, vol, pending_frozen): pos_all = self._pos.pos_short_today else: pos_all = self._pos.pos_long_today - frozen_volume = sum([order.volume_left for order in self._pos.orders.values() if - not order.is_dead and order.offset == order_offset and order.direction == order_dir]) + if pos_orders is None: + pos_orders = self._pos.orders + frozen_volume = sum(order.volume_left for order in pos_orders.values() if + not order.is_dead and order.offset == order_offset and order.direction == order_dir) else: order_offset = "CLOSE" - frozen_volume = pending_frozen + sum([order.volume_left for order in self._pos.orders.values() if - not order.is_dead and order.offset != "OPEN" and order.direction == order_dir]) + if pos_orders is None: + pos_orders = self._pos.orders + frozen_volume = pending_frozen + sum(order.volume_left for order in pos_orders.values() if + not order.is_dead and order.offset != "OPEN" and order.direction == order_dir) pos_all = self._pos.pos_short_today if vol > 0 else self._pos.pos_long_today order_volume = min(abs(vol), max(0, pos_all - frozen_volume)) elif offset == "开": @@ -372,26 +380,30 @@ async def _target_pos_task(self): while True: if isinstance(self._api._backtest, TqBacktest): cur_timestamp = self._api._data.get("_tqsdk_backtest", {}).get("current_dt", float("nan")) - cur_dt = _timestamp_nano_to_str(cur_timestamp) - time_record = float("nan") + if _is_in_trading_time_nano(self._quote, cur_timestamp): + break else: cur_dt = self._quote["datetime"] time_record = self._local_time_record - if _is_in_trading_time(self._quote, cur_dt, time_record): - break + if _is_in_trading_time(self._quote, cur_dt, time_record): + break await self._local_time_record_update_chan.recv() target_pos = self._pos_chan.recv_latest(target_pos) # 获取最后一个target_pos目标仓位 # 确定调仓增减方向 delta_volume = target_pos - self._pos.pos pending_forzen = 0 - for each_priority in self._offset_priority + ",": # 按不同模式的优先级顺序报出不同的offset单,股指(“昨开”)平昨优先从不平今就先报平昨,原油平今优先("今昨开")就报平今 + cached_orders = None + for each_priority in self._offset_priority + ",": # 按不同模式的优先级顺序报出不同的offset单 if each_priority == ",": await gather(*[each._task for each in all_tasks]) pending_forzen = 0 + cached_orders = None all_tasks = [] continue - order_offset, order_dir, order_volume = self._get_order(each_priority, delta_volume, pending_forzen) + if each_priority != "开" and cached_orders is None: + cached_orders = self._pos.orders + order_offset, order_dir, order_volume = self._get_order(each_priority, delta_volume, pending_forzen, cached_orders) if order_volume == 0: # 如果没有则直接到下一种offset continue elif order_offset != "OPEN": diff --git a/tqsdk/multiaccount.py b/tqsdk/multiaccount.py index 80aee8c3..a77ae5a6 100644 --- a/tqsdk/multiaccount.py +++ b/tqsdk/multiaccount.py @@ -80,6 +80,12 @@ def __init__(self, accounts: Optional[List['UnionTradeable']] = None): self._account_list = accounts if accounts else [TqSim()] self._all_sim_account = all([isinstance(a, BaseSim) for a in self._account_list]) # 是否全部为本地模拟帐号 self._map_conn_id = {} # 每次建立连接时,记录每个 conn_id 对应的账户 + # Cache: account_key -> account instance for O(1) lookup + self._account_key_map = {a._account_key: a for a in self._account_list} + # Cache: account_key -> is_stock_type for hot-path lookups + self._is_stock_cache = {a._account_key: isinstance(a, StockMixin) for a in self._account_list} + # Fast flag: True if any account is a stock account (enables split logic in _wait_update) + self._has_any_stock = any(self._is_stock_cache.values()) if self._has_duplicate_account(): raise Exception("多账户列表中不允许使用重复的账户实例.") @@ -94,8 +100,7 @@ def _check_valid(self, account: Union[str, 'UnionTradeable', None]): account: 类型 str 表示 account_key,其他为账户类型或者 None """ if isinstance(account, str): - selected_list = [a for a in self._account_list if a._account_key == account] - return selected_list[0] if selected_list else None + return self._account_key_map.get(account) elif account is None: return self._account_list[0] if len(self._account_list) == 1 else None else: @@ -113,6 +118,8 @@ def _get_account_key(self, account): def _is_stock_type(self, account_or_account_key): """ 判断账户类型是否为股票账户 """ + if isinstance(account_or_account_key, str): + return self._is_stock_cache.get(account_or_account_key, False) acc = self._check_valid(account_or_account_key) return isinstance(acc, StockMixin) diff --git a/tqsdk/objs.py b/tqsdk/objs.py index a73d93fa..90362ea9 100644 --- a/tqsdk/objs.py +++ b/tqsdk/objs.py @@ -4,12 +4,24 @@ import copy import json +from itertools import islice from typing import List from tqsdk.diff import _get_obj from tqsdk.entity import Entity +class _OrdersDirtyFlag: + """Lightweight listener that sets a dirty flag when orders entity is updated.""" + __slots__ = ('dirty', '__weakref__') + + def __init__(self): + self.dirty = True + + def send_nowait(self, _): + self.dirty = True + + class Quote(Entity): """ Quote 是一个行情对象 """ @@ -449,10 +461,66 @@ def orders(self): :return: dict, 其中每个元素的key为委托单ID, value为 :py:class:`~tqsdk.objs.Order` """ - tdict = _get_obj(self._api._data, ["trade", self._path[1], "orders"]) - fts = {order_id: order for order_id, order in tdict.items() if (not order_id.startswith( - "_")) and order.instrument_id == self.instrument_id and order.exchange_id == self.exchange_id and order.status == "ALIVE"} - return fts + # Fast path: all cached refs in a single tuple [orders_entity, shared_flag, pos_key, index] + try: + cache = self._orders_cache + shared_flag = cache[1] + except AttributeError: + orders_entity = _get_obj(self._api._data, ["trade", self._path[1], "orders"]) + # Setup shared index on orders entity if needed + try: + shared_flag = orders_entity._orders_shared_flag + except AttributeError: + shared_flag = _OrdersDirtyFlag() + object.__setattr__(orders_entity, '_orders_shared_flag', shared_flag) + object.__setattr__(orders_entity, '_orders_shared_index', {}) + object.__setattr__(orders_entity, '_orders_alive_set', {}) + object.__setattr__(orders_entity, '_orders_indexed_len', 0) + orders_entity._add_listener(shared_flag) + sd = self._data + pos_key = (sd.get('instrument_id', ''), sd.get('exchange_id', '')) + cache = (orders_entity, shared_flag, pos_key, orders_entity._orders_shared_index) + object.__setattr__(self, '_orders_cache', cache) + if shared_flag.dirty: + orders_entity = cache[0] + index = cache[3] + alive_set = orders_entity._orders_alive_set + orders_data = orders_entity._data + indexed_len = orders_entity._orders_indexed_len + # 1. Check existing alive orders — remove any no longer ALIVE + to_remove = [] + for oid, key in alive_set.items(): + try: + if orders_data[oid]._data['status'] != "ALIVE": + to_remove.append(oid) + bucket = index.get(key) + if bucket: + bucket.pop(oid, None) + if not bucket: + del index[key] + except (KeyError, AttributeError): + to_remove.append(oid) + for oid in to_remove: + del alive_set[oid] + # 2. Check new orders (from indexed_len onwards) + cur_len = len(orders_data) + if cur_len != indexed_len: + for order_id, order in islice(orders_data.items(), indexed_len, None): + try: + od = order._data + if od['status'] == "ALIVE": + key = (od['instrument_id'], od['exchange_id']) + alive_set[order_id] = key + bucket = index.get(key) + if bucket is None: + bucket = {} + index[key] = bucket + bucket[order_id] = order + except (AttributeError, KeyError): + continue + object.__setattr__(orders_entity, '_orders_indexed_len', cur_len) + shared_flag.dirty = False + return cache[3].get(cache[2], {}) class Order(Entity): @@ -508,10 +576,42 @@ def trade_records(self): :return: dict, 其中每个元素的key为成交ID, value为 :py:class:`~tqsdk.objs.Trade` """ - tdict = _get_obj(self._api._data, ["trade", self._path[1], "trades"]) - fts = {trade_id: trade for trade_id, trade in tdict.items() if - (not trade_id.startswith("_")) and trade.order_id == self.order_id} - return fts + # Fast path: cache tdict, dirty_flag, trades_data, index refs + try: + cache = self._trades_cache + dirty_flag = cache[1] + except AttributeError: + tdict = _get_obj(self._api._data, ["trade", self._path[1], "trades"]) + trades_data = tdict._data + try: + dirty_flag = tdict._trades_dirty_flag + except AttributeError: + dirty_flag = _OrdersDirtyFlag() + object.__setattr__(tdict, '_trades_dirty_flag', dirty_flag) + object.__setattr__(tdict, '_trade_order_index', {}) + object.__setattr__(tdict, '_trade_order_index_len', 0) + tdict._add_listener(dirty_flag) + target_order_id = self._data.get('order_id', '') + cache = (tdict, dirty_flag, trades_data, tdict._trade_order_index, target_order_id) + object.__setattr__(self, '_trades_cache', cache) + if dirty_flag.dirty: + tdict = cache[0] + trades_data = cache[2] + idx = cache[3] + idx_len = tdict._trade_order_index_len + cur_len = len(trades_data) + if cur_len != idx_len: + for t_id, trade in islice(trades_data.items(), idx_len, None): + try: + oid = trade._data['order_id'] + except (AttributeError, KeyError): + continue + if oid: + idx.setdefault(oid, []).append(t_id) + object.__setattr__(tdict, '_trade_order_index_len', cur_len) + dirty_flag.dirty = False + trade_ids = cache[3].get(cache[4], ()) + return {tid: cache[2][tid] for tid in trade_ids if tid in cache[2]} class Trade(Entity): @@ -837,8 +937,16 @@ def __init__(self, api): @property def orders(self): tdict = _get_obj(self._api._data, ["trade", self._path[1], "orders"]) - fts = {order_id: order for order_id, order in tdict.items() if (not order_id.startswith( - "_")) and order.instrument_id == self.instrument_id and order.exchange_id == self.exchange_id and order.status == "ALIVE"} + inst_id = self._data.get('instrument_id', '') + exch_id = self._data.get('exchange_id', '') + fts = {} + for order_id, order in tdict._data.items(): + try: + od = order._data + except AttributeError: + continue + if od.get('status') == "ALIVE" and od.get('instrument_id') == inst_id and od.get('exchange_id') == exch_id: + fts[order_id] = order return fts @@ -884,9 +992,33 @@ def trade_records(self): :return: dict, 其中每个元素的key为成交ID, value为 :py:class:`~tqsdk.objs.Trade` """ tdict = _get_obj(self._api._data, ["trade", self._path[1], "trades"]) - fts = {trade_id: trade for trade_id, trade in tdict.items() if - (not trade_id.startswith("_")) and trade.order_id == self.order_id} - return fts + target_order_id = self._data.get('order_id', '') + trades_data = tdict._data + # Dirty-flag + incremental index: only rebuild when entity changes + try: + dirty_flag = tdict._trades_dirty_flag + except AttributeError: + dirty_flag = _OrdersDirtyFlag() + object.__setattr__(tdict, '_trades_dirty_flag', dirty_flag) + object.__setattr__(tdict, '_trade_order_index', {}) + object.__setattr__(tdict, '_trade_order_index_len', 0) + tdict._add_listener(dirty_flag) + if dirty_flag.dirty: + idx = tdict._trade_order_index + idx_len = tdict._trade_order_index_len + cur_len = len(trades_data) + if cur_len != idx_len: + for t_id, trade in islice(trades_data.items(), idx_len, None): + try: + oid = trade._data['order_id'] + except (AttributeError, KeyError): + continue + if oid: + idx.setdefault(oid, []).append(t_id) + object.__setattr__(tdict, '_trade_order_index_len', cur_len) + dirty_flag.dirty = False + trade_ids = tdict._trade_order_index.get(target_order_id, ()) + return {tid: trades_data[tid] for tid in trade_ids if tid in trades_data} class SecurityTrade(Entity): diff --git a/tqsdk/tradeable/sim/trade_base.py b/tqsdk/tradeable/sim/trade_base.py index a77543ed..a3507876 100644 --- a/tqsdk/tradeable/sim/trade_base.py +++ b/tqsdk/tradeable/sim/trade_base.py @@ -144,9 +144,24 @@ def cancel_order(self, symbol, pack): return self._return_results() def update_quotes(self, symbol, pack): - for q in pack.get("quotes", {}).values(): + quotes_diff = pack.get("quotes", {}) + for q in quotes_diff.values(): self._max_datetime = max(q.get("datetime", ""), self._max_datetime) - _simple_merge_diff(self._quotes, pack.get("quotes", {})) + # Specialized quote merge: quote fields are always scalars, never None/nested + # Normalize Entity objects to plain dicts for fast C-level dict.update + _quotes = self._quotes + for sym, fields in quotes_diff.items(): + ftype = type(fields) + if ftype is not dict: + try: + fields = fields._data + except AttributeError: + continue + existing = _quotes.get(sym) + if existing is None: + _quotes[sym] = fields.copy() + else: + existing.update(fields) quote, underlying_quote = self._get_quotes_by_symbol(symbol) # 某些非交易时间段,ticks 回测是 quote 的最新价有可能是 nan,无效的行情直接跳过 if math.isnan(quote["last_price"]): @@ -217,22 +232,35 @@ def _ensure_position(self, symbol, quote, underlying_quote): def _get_quotes_by_symbol(self, symbol): """返回指定合约及标的合约,在本模块执行过程中,应该保证一定有合约行情""" - quote = self._quotes.get(symbol) - assert quote and quote.get("datetime"), "未收到指定合约行情" - underlying_quote = None - if quote["ins_class"].endswith("OPTION"): - underlying_quote = self._quotes.get(quote["underlying_symbol"]) - assert underlying_quote and underlying_quote.get("datetime"), "未收到指定合约的标的行情" - return quote, underlying_quote + quote = self._quotes[symbol] + # Cache option status per symbol to avoid repeated string operations + try: + is_option = self._symbol_is_option[symbol] + except (AttributeError, KeyError): + if not hasattr(self, '_symbol_is_option'): + self._symbol_is_option = {} + is_option = quote["ins_class"].endswith("OPTION") + self._symbol_is_option[symbol] = is_option + if is_option: + underlying_quote = self._quotes[quote["underlying_symbol"]] + return quote, underlying_quote + return quote, None def _append_to_diffs(self, path, obj): - target = {} - diff = {'trade': {self._account_key: target}} - while len(path) > 0: - k = path.pop(0) - target[k] = obj.copy() if len(path) == 0 else {} - target = target[k] - self._diffs.append(diff) + plen = len(path) + if plen == 2: + # Fast path for common 2-element paths (e.g., ('positions', symbol), ('orders', id)) + self._diffs.append({'trade': {self._account_key: {path[0]: {path[1]: obj.copy()}}}}) + elif plen == 1: + self._diffs.append({'trade': {self._account_key: {path[0]: obj.copy()}}}) + else: + target = {} + diff = {'trade': {self._account_key: target}} + last = plen - 1 + for i, k in enumerate(path): + target[k] = obj.copy() if i == last else {} + target = target[k] + self._diffs.append(diff) def _return_results(self): """ diff --git a/tqsdk/tradeable/sim/trade_future.py b/tqsdk/tradeable/sim/trade_future.py index c81a971f..1adc9594 100644 --- a/tqsdk/tradeable/sim/trade_future.py +++ b/tqsdk/tradeable/sim/trade_future.py @@ -398,26 +398,40 @@ def _on_update_quotes(self, symbol, position, quote, underlying_quote): else position["underlying_last_price"] else: underlying_last_price = underlying_quote["last_price"] - future_margin = _get_future_margin(quote) + # Inline _get_future_margin to avoid 864K function calls + _ins_class = quote.get("ins_class", "") + if _ins_class.endswith("OPTION"): + future_margin = float('nan') + else: + future_margin = quote.get("user_margin", quote.get("margin", float('nan'))) + quote_last_price = quote["last_price"] if position["volume_long"] > 0 or position["volume_short"] > 0: - if position["last_price"] != quote["last_price"] \ + if position["last_price"] != quote_last_price \ or (math.isnan(future_margin) or future_margin != position["future_margin"]) \ or (underlying_quote and underlying_last_price != position["underlying_last_price"]): self._adjust_position_account(symbol, quote, underlying_quote, pre_last_price=position["last_price"], - last_price=quote["last_price"], + last_price=quote_last_price, pre_underlying_last_price=position["underlying_last_price"], underlying_last_price=underlying_last_price) position["future_margin"] = future_margin - position["last_price"] = quote["last_price"] + position["last_price"] = quote_last_price position["underlying_last_price"] = underlying_last_price + # Both position and account changed + self._diffs.append({'trade': {self._account_key: { + 'positions': {symbol: position.copy()}, + 'accounts': {'CNY': self._account.copy()} + }}}) + # else: nothing changed, skip diff entirely else: - # 修改辅助变量 + # No volume — only update auxiliary fields, no account diff needed position["future_margin"] = future_margin - position["last_price"] = quote["last_price"] + position["last_price"] = quote_last_price position["underlying_last_price"] = underlying_last_price - self._append_to_diffs(['positions', symbol], position) # 一定要返回 position,下游会用到 future_margin 字段判断修改保证金是否成功 - self._append_to_diffs(['accounts', 'CNY'], self._account) + # Only send position diff (account unchanged) + self._diffs.append({'trade': {self._account_key: { + 'positions': {symbol: position.copy()} + }}}) def _adjust_position_account(self, symbol, quote, underlying_quote=None, pre_last_price=float('nan'), last_price=float('nan'), pre_underlying_last_price=float('nan'), underlying_last_price=float('nan'), @@ -437,45 +451,50 @@ def _adjust_position_account(self, symbol, quote, underlying_quote=None, pre_las margin_short = 0 # 空头占用保证金 market_value_long = 0 # 期权权利方市值(始终 >= 0) market_value_short = 0 # 期权义务方市值(始终 <= 0) + # Inline _get_future_margin and cache ins_class check + _ins_class = quote.get("ins_class", "") + _is_option = _ins_class.endswith("OPTION") + if not _is_option: + _fm = quote.get("user_margin", quote.get("margin", float('nan'))) assert [buy_open, buy_close, sell_open, sell_close].count(0) >= 3 # 只有一个大于0, 或者都是0,表示价格变化导致的字段修改 if buy_open > 0: # 买开,pre_last_price 应该是成交价格,last_price 应该是 position['last_price'] float_profit_long = (last_price - pre_last_price) * buy_open * quote["volume_multiple"] - if quote["ins_class"].endswith("OPTION"): + if _is_option: market_value_long = last_price * buy_open * quote["volume_multiple"] else: - margin_long = buy_open * _get_future_margin(quote) + margin_long = buy_open * _fm position_profit_long = (last_price - pre_last_price) * buy_open * quote["volume_multiple"] elif sell_close > 0: # 卖平,pre_last_price 应该是 position['last_price'],last_price 应该是 0 float_profit_long = -position["float_profit_long"] / position["volume_long"] * sell_close - if quote["ins_class"].endswith("OPTION"): + if _is_option: market_value_long = -pre_last_price * sell_close * quote["volume_multiple"] else: - margin_long = -sell_close * _get_future_margin(quote) + margin_long = -sell_close * _fm position_profit_long = -position["position_profit_long"] / position["volume_long"] * sell_close elif sell_open > 0: # 卖开 float_profit_short = (pre_last_price - last_price) * sell_open * quote["volume_multiple"] - if quote["ins_class"].endswith("OPTION"): + if _is_option: market_value_short = -last_price * sell_open * quote["volume_multiple"] margin_short = sell_open * _get_option_margin(quote, last_price, underlying_last_price) else: - margin_short = sell_open * _get_future_margin(quote) + margin_short = sell_open * _fm position_profit_short = (pre_last_price - last_price) * sell_open * quote["volume_multiple"] elif buy_close > 0: # 买平 float_profit_short = -position["float_profit_short"] / position["volume_short"] * buy_close - if quote["ins_class"].endswith("OPTION"): + if _is_option: market_value_short = pre_last_price * buy_close * quote["volume_multiple"] margin_short = -buy_close * _get_option_margin(quote, pre_last_price, pre_underlying_last_price) else: - margin_short = -buy_close * _get_future_margin(quote) + margin_short = -buy_close * _fm position_profit_short = -position["position_profit_short"] / position["volume_short"] * buy_close else: float_profit_long = (last_price - pre_last_price) * position["volume_long"] * quote["volume_multiple"] # 多头浮动盈亏 float_profit_short = (pre_last_price - last_price) * position["volume_short"] * quote["volume_multiple"] # 空头浮动盈亏 - if quote["ins_class"].endswith("OPTION"): + if _is_option: margin_short = _get_option_margin(quote, last_price, underlying_last_price) * position["volume_short"] - position["margin_short"] market_value_long = (last_price - pre_last_price) * position["volume_long"] * quote["volume_multiple"] market_value_short = (pre_last_price - last_price) * position["volume_short"] * quote["volume_multiple"] @@ -483,8 +502,8 @@ def _adjust_position_account(self, symbol, quote, underlying_quote=None, pre_las # 期权持仓盈亏为 0 position_profit_long = float_profit_long # 多头持仓盈亏 position_profit_short = float_profit_short # 空头持仓盈亏 - margin_long = _get_future_margin(quote) * position["volume_long"] - position["margin_long"] - margin_short = _get_future_margin(quote) * position["volume_short"] - position["margin_short"] + margin_long = _fm * position["volume_long"] - position["margin_long"] + margin_short = _fm * position["volume_short"] - position["margin_short"] if any([buy_open, buy_close, sell_open, sell_close]): # 修改 position volume 相关的计算字段 diff --git a/tqsdk/trading_status.py b/tqsdk/trading_status.py index c5b02cef..4a64c713 100644 --- a/tqsdk/trading_status.py +++ b/tqsdk/trading_status.py @@ -87,14 +87,15 @@ def _normalize_trade_status(self, diffs): async def _query_symbol_info(self, symbols): """查询缺少合约信息的quotes""" for symbol in symbols: - self._quotes_unready[symbol]["_listener"].add(self._quote_chan) + self._quotes_unready[symbol]._add_listener(self._quote_chan) for query_pack in _query_for_quote(list(symbols), self._api._pre20_ins_info.keys()): await self._md_send_chan.send(query_pack) async def _symbol_info_watcher(self): async for _ in self._quote_chan: for symbol in await self._unready_to_ready(): - self._quotes_ready[symbol]["_listener"].discard(self._quote_chan) + if self._quotes_ready[symbol]._listener is not None: + self._quotes_ready[symbol]._listener.discard(self._quote_chan) async def _unready_to_ready(self): ready_delta = {symbol for symbol, quote in self._quotes_unready.items() if not math.isnan(quote.price_tick)}