From dde7ab71a96aed9d535f1d3f398086bb9c278a69 Mon Sep 17 00:00:00 2001 From: discountry Date: Sat, 27 Sep 2025 16:12:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=8E=AF=E5=A2=83=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=A4=BA=E4=BE=8B=EF=BC=8C=E6=B7=BB=E5=8A=A0=20GRVT?= =?UTF-8?q?=20=E7=9B=B8=E5=85=B3=E7=9A=84=20API=20=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E5=92=8C=E9=80=89=E9=A1=B9=EF=BC=8C=E5=A2=9E=E5=BC=BA=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E7=9A=84=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E6=89=80=E9=80=82=E9=85=8D=E5=99=A8=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E7=94=A8=E6=88=B7=E8=83=BD=E5=A4=9F=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=92=8C=E4=BD=BF=E7=94=A8=20GRVT=20?= =?UTF-8?q?=E4=BA=A4=E6=98=93=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 18 +- README.md | 56 + bun.lock | 76 +- .../grvt-pysdk-main/.github/workflows/ci.yml | 57 + .../.github/workflows/codeql.yml | 39 + .../grvt-pysdk-main/.github/workflows/pr.yml | 61 + docs/grvt/grvt-pysdk-main/.gitignore | 287 ++ .../grvt-pysdk-main/.pre-commit-config.yaml | 79 + docs/grvt/grvt-pysdk-main/.python-version | 1 + docs/grvt/grvt-pysdk-main/.renovaterc.json | 11 + docs/grvt/grvt-pysdk-main/CHANGELOG.md | 78 + docs/grvt/grvt-pysdk-main/LICENSE | 201 ++ docs/grvt/grvt-pysdk-main/Makefile | 59 + docs/grvt/grvt-pysdk-main/README.md | 300 ++ docs/grvt/grvt-pysdk-main/README_PYPI.md | 382 +++ docs/grvt/grvt-pysdk-main/build_readme.py | 9 + docs/grvt/grvt-pysdk-main/pyproject.toml | 106 + .../grvt-pysdk-main/src/pysdk/__init__.py | 0 .../grvt-pysdk-main/src/pysdk/grvt_ccxt.py | 812 +++++ .../src/pysdk/grvt_ccxt_base.py | 586 ++++ .../src/pysdk/grvt_ccxt_env.py | 195 ++ .../src/pysdk/grvt_ccxt_logging_selector.py | 32 + .../src/pysdk/grvt_ccxt_pro.py | 841 +++++ .../src/pysdk/grvt_ccxt_test_utils.py | 75 + .../src/pysdk/grvt_ccxt_types.py | 81 + .../src/pysdk/grvt_ccxt_utils.py | 544 ++++ .../grvt-pysdk-main/src/pysdk/grvt_ccxt_ws.py | 785 +++++ .../src/pysdk/grvt_fixed_types.py | 25 + .../src/pysdk/grvt_raw_async.py | 365 +++ .../src/pysdk/grvt_raw_base.py | 267 ++ .../grvt-pysdk-main/src/pysdk/grvt_raw_env.py | 77 + .../src/pysdk/grvt_raw_signing.py | 248 ++ .../src/pysdk/grvt_raw_sync.py | 351 +++ .../src/pysdk/grvt_raw_types.py | 2697 +++++++++++++++++ docs/grvt/grvt-pysdk-main/tests/__init__.py | 0 .../grvt-pysdk-main/tests/pysdk/__init__.py | 0 .../tests/pysdk/test_grvt_ccxt.py | 316 ++ .../tests/pysdk/test_grvt_ccxt_pro.py | 300 ++ .../tests/pysdk/test_grvt_ccxt_vault.py | 52 + .../tests/pysdk/test_grvt_ccxt_vault_pro.py | 60 + .../tests/pysdk/test_grvt_ccxt_ws.py | 297 ++ .../tests/pysdk/test_grvt_raw_async.py | 137 + .../tests/pysdk/test_grvt_raw_signing.py | 400 +++ ...est_grvt_raw_signing_intermediate_steps.py | 837 +++++ .../tests/pysdk/test_grvt_raw_sync.py | 141 + .../tests/pysdk/test_raw_utils.py | 158 + .../tests/pysdk/test_transfer.py | 4 + .../tests/pysdk/test_withdrawal.py | 4 + docs/grvt/grvt-pysdk-main/uv.lock | 1961 ++++++++++++ docs/grvt/sdk-readme.md | 247 +- package.json | 2 +- src/exchanges/create-adapter.ts | 29 + src/exchanges/grvt/adapter.ts | 245 ++ src/exchanges/grvt/client.ts | 28 - src/exchanges/grvt/gateway.ts | 1442 +++++++-- src/exchanges/types.ts | 47 +- src/ui/MakerApp.tsx | 34 +- src/ui/OffsetMakerApp.tsx | 35 +- src/ui/TrendApp.tsx | 34 +- tests/exchange-factory.test.ts | 42 + 60 files changed, 16126 insertions(+), 527 deletions(-) create mode 100644 docs/grvt/grvt-pysdk-main/.github/workflows/ci.yml create mode 100644 docs/grvt/grvt-pysdk-main/.github/workflows/codeql.yml create mode 100644 docs/grvt/grvt-pysdk-main/.github/workflows/pr.yml create mode 100644 docs/grvt/grvt-pysdk-main/.gitignore create mode 100644 docs/grvt/grvt-pysdk-main/.pre-commit-config.yaml create mode 100644 docs/grvt/grvt-pysdk-main/.python-version create mode 100644 docs/grvt/grvt-pysdk-main/.renovaterc.json create mode 100644 docs/grvt/grvt-pysdk-main/CHANGELOG.md create mode 100644 docs/grvt/grvt-pysdk-main/LICENSE create mode 100644 docs/grvt/grvt-pysdk-main/Makefile create mode 100644 docs/grvt/grvt-pysdk-main/README.md create mode 100644 docs/grvt/grvt-pysdk-main/README_PYPI.md create mode 100644 docs/grvt/grvt-pysdk-main/build_readme.py create mode 100644 docs/grvt/grvt-pysdk-main/pyproject.toml create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/__init__.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_base.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_env.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_logging_selector.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_pro.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_test_utils.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_types.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_utils.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_ws.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_fixed_types.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_async.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_base.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_env.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_signing.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_sync.py create mode 100644 docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_types.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/__init__.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/__init__.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_pro.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault_pro.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_ws.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_async.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing_intermediate_steps.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_sync.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_raw_utils.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_transfer.py create mode 100644 docs/grvt/grvt-pysdk-main/tests/pysdk/test_withdrawal.py create mode 100644 docs/grvt/grvt-pysdk-main/uv.lock create mode 100644 src/exchanges/create-adapter.ts create mode 100644 src/exchanges/grvt/adapter.ts delete mode 100644 src/exchanges/grvt/client.ts create mode 100644 tests/exchange-factory.test.ts diff --git a/.env.example b/.env.example index ef09008..62491de 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# Exchange selection +EXCHANGE=aster # Pick aster (default) or grvt + # Aster API credentials ASTER_API_KEY= ASTER_API_SECRET= @@ -30,4 +33,17 @@ MAKER_BID_OFFSET=0 # Bid quote offset from top bid (USDT) MAKER_ASK_OFFSET=0 # Ask quote offset from top ask (USDT) MAKER_REFRESH_INTERVAL_MS=1500 # Maker refresh cadence (ms) MAKER_MAX_CLOSE_SLIPPAGE_PCT=0.05 # Maker close slippage guard (fallbacks to MAX_CLOSE_SLIPPAGE_PCT) -MAKER_PRICE_TICK=0.1 # Maker price tick size (defaults to PRICE_TICK) \ No newline at end of file +MAKER_PRICE_TICK=0.1 # Maker price tick size (defaults to PRICE_TICK) + +# GRVT authentication (set when EXCHANGE=grvt) +GRVT_API_KEY= +GRVT_API_SECRET= +GRVT_SUB_ACCOUNT_ID= +GRVT_INSTRUMENT=BTC_USDT_Perp +GRVT_SYMBOL=BTCUSDT +GRVT_ENV=prod + +# Optional advanced overrides +# GRVT_COOKIE="gravity=..." # Pre-provisioned session cookie (auto-refresh uses API key when absent) +# GRVT_ACCOUNT_ID= # Populated automatically after login +# GRVT_SIGNER_PATH=./grvt-signer.cjs # Custom signature provider module diff --git a/README.md b/README.md index 65469fa..92c7b3d 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,62 @@ MAKER_MAX_CLOSE_SLIPPAGE_PCT=0.05 # allowed deviation vs mark when closing MAKER_PRICE_TICK=0.1 # maker tick size; defaults to PRICE_TICK ``` +To switch the entire CLI to GRVT instead of Aster, set `EXCHANGE=grvt` and provide the programmable API credentials: + +```bash +EXCHANGE=grvt +GRVT_API_KEY=your_api_key +GRVT_API_SECRET=0xabc123... # private key used for EIP-712 order signatures +GRVT_SUB_ACCOUNT_ID=your_sub_account # sub account that actually trades +GRVT_INSTRUMENT=BTC_USDT_Perp # instrument as defined by GRVT +GRVT_SYMBOL=BTCUSDT # optional display symbol; defaults to instrument sans underscores +GRVT_ENV=prod # optional environment switch (prod/testnet/staging/dev) +# Optional overrides +# GRVT_SIGNER_PATH=./grvt-signer.cjs # replace the built-in signer if you run an external service +# GRVT_COOKIE="gravity=..." # pre-provisioned session cookie (auto-fetched via API key when absent) +# GRVT_ACCOUNT_ID=... # populated automatically after login +``` + +The adapter logs in with `GRVT_API_KEY`, refreshes cookies automatically, and signs orders locally using `GRVT_API_SECRET` following GRVT's EIP‑712 schema. If you prefer to delegate signing to another process, set `GRVT_SIGNER_PATH`; the module should export a function (default export also works) that receives a context object (unsigned order, nonce/expiration, instrument metadata, chain id, etc.) and returns `{ signer, r, s, v, expiration, nonce }`. If you leave `GRVT_SIGNER_PATH` unset, the built-in signer uses `GRVT_API_SECRET` directly. + +### 切换到 GRVT 交易所 + +将环境变量 `EXCHANGE` 设为 `grvt` 后,所有策略会改用 GRVT 适配器: + +```bash +EXCHANGE=grvt +GRVT_API_KEY=你的APIKey +GRVT_API_SECRET=0xabc123... # 用于订单签名的私钥 +GRVT_SUB_ACCOUNT_ID=your_sub_account_id # 具体交易子账号 +GRVT_INSTRUMENT=BTC_USDT_Perp # 交易品种,需与策略一致 +GRVT_SYMBOL=BTCUSDT # 可选,内部用于展示,默认由 instrument 推导 +GRVT_ENV=prod # 可选:prod / testnet / staging / dev,默认 prod +# 可选覆盖项 +# GRVT_SIGNER_PATH=./grvt-signer.cjs # 如果你希望由外部服务签名 +# GRVT_COOKIE="gravity=..." # 预先获取到的 Cookie(若未提供,将使用 API Key 自动登录) +# GRVT_ACCOUNT_ID=... # 登录后自动填充 +``` + +适配器会基于 `GRVT_API_SECRET` 自动完成 EIP‑712 签名并提交订单。如果你需要自定义签名流程,可通过 `GRVT_SIGNER_PATH` 指定模块(CommonJS 或 ESM 均可)。模块需导出一个函数,接收签名上下文(未签名订单、nonce/expiration、合约元信息、链 ID 等)并返回包含 `signer/r/s/v/expiration/nonce` 字段的对象,例如: + +```js +// grvt-signer.cjs +module.exports = async function signOrder(context) { + // 调用你自己的签名服务,或者使用本地私钥完成签名 + const signature = await mySigner(context); + return { + signer: signature.signer, + r: signature.r, + s: signature.s, + v: signature.v, + expiration: signature.expiration, + nonce: signature.nonce, + }; +}; +``` + +如未自定义 `GRVT_SIGNER_PATH`,适配器会直接使用 `GRVT_API_SECRET` 完成签名。 + ## Running the CLI ```bash bun run index.ts # or: bun run dev / bun run start diff --git a/bun.lock b/bun.lock index 1f8e600..541a9bf 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,7 @@ "": { "name": "ritmex-bot", "dependencies": { - "@grvt/sdk": "^0.0.1-beta.17", + "@grvt/client": "^1.6.4", "axios": "^1.12.2", "ccxt": "^4.5.5", "dotenv": "^17.2.2", @@ -22,8 +22,6 @@ }, }, "packages": { - "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.10.1", "", {}, "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw=="], - "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-qI/5TaaaCZE4yeSZ83lu0+xi1r88JSxUjnH4OP/iZF7+KKZ75u3ee5isd0LxX+6N8U0npL61YrpbthILHB6BnA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="], @@ -78,32 +76,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="], - "@ethereumjs/common": ["@ethereumjs/common@3.2.0", "", { "dependencies": { "@ethereumjs/util": "^8.1.0", "crc-32": "^1.2.0" } }, "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA=="], - - "@ethereumjs/rlp": ["@ethereumjs/rlp@4.0.1", "", { "bin": { "rlp": "bin/rlp" } }, "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw=="], - - "@ethereumjs/tx": ["@ethereumjs/tx@4.2.0", "", { "dependencies": { "@ethereumjs/common": "^3.2.0", "@ethereumjs/rlp": "^4.0.1", "@ethereumjs/util": "^8.1.0", "ethereum-cryptography": "^2.0.0" } }, "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw=="], - - "@ethereumjs/util": ["@ethereumjs/util@8.1.0", "", { "dependencies": { "@ethereumjs/rlp": "^4.0.1", "ethereum-cryptography": "^2.0.0", "micro-ftch": "^0.3.1" } }, "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA=="], - "@grvt/client": ["@grvt/client@1.6.4", "", { "dependencies": { "axios": "^1.12.2" } }, "sha512-yZfEvsC/BtkcdMjoB7Eh6gshBraz/9810qCllvNXDx6OmdxR+NfxPO+ucNlPbyjEt1HfSVzHBWB1yGnVl1usNA=="], - "@grvt/sdk": ["@grvt/sdk@0.0.1-beta.17", "", { "dependencies": { "@grvt/client": "^1.4.21", "@metamask/eth-sig-util": "^8.2.0", "@types/ws": "^8.18.1", "ethers": "^6.13.5", "set-cookie-parser": "^2.7.1", "ws": "^8.18.1" } }, "sha512-ADWGg7mM79xOu354DaYNZY3hyAEGEFC076vEK1dXFL6ykITyluKOPj2eetP3mkLeyLUyYvA7ct1pz6JmLC77Hw=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@metamask/abi-utils": ["@metamask/abi-utils@3.0.0", "", { "dependencies": { "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.0.1" } }, "sha512-a/l0DiSIr7+CBYVpHygUa3ztSlYLFCQMsklLna+t6qmNY9+eIO5TedNxhyIyvaJ+4cN7TLy0NQFbp9FV3X2ktg=="], - - "@metamask/eth-sig-util": ["@metamask/eth-sig-util@8.2.0", "", { "dependencies": { "@ethereumjs/rlp": "^4.0.1", "@ethereumjs/util": "^8.1.0", "@metamask/abi-utils": "^3.0.0", "@metamask/utils": "^11.0.1", "@scure/base": "~1.1.3", "ethereum-cryptography": "^2.1.2", "tweetnacl": "^1.0.3" } }, "sha512-LZDglIh4gYGw9Myp+2aIwKrj6lIJpMC4e0m7wKJU+BxLLBFcrTgKrjdjstXGVWvuYG3kutlh9J+uNBRPJqffWQ=="], - - "@metamask/superstruct": ["@metamask/superstruct@3.2.1", "", {}, "sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g=="], - - "@metamask/utils": ["@metamask/utils@11.8.1", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "@types/lodash": "^4.17.20", "debug": "^4.3.4", "lodash": "^4.17.21", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-DIbsNUyqWLFgqJlZxi1OOCMYvI23GqFCvNJAtzv8/WXWzJfnJnvp1M24j7VvUe3URBi3S86UgQ7+7aWU9p/cnQ=="], - - "@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], - - "@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ=="], @@ -148,12 +124,6 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="], - "@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], - - "@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="], - - "@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="], - "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], @@ -164,16 +134,12 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], @@ -188,8 +154,6 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - "aes-js": ["aes-js@4.0.0-beta.5", "", {}, "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q=="], - "ansi-escapes": ["ansi-escapes@7.1.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -230,8 +194,6 @@ "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -266,10 +228,6 @@ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], - - "ethers": ["ethers@6.15.0", "", { "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", "ws": "8.17.1" } }, "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ=="], - "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -306,16 +264,12 @@ "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "micro-ftch": ["micro-ftch@0.3.1", "", {}, "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -338,8 +292,6 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "pony-cause": ["pony-cause@2.1.11", "", {}, "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], @@ -354,10 +306,6 @@ "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -390,18 +338,12 @@ "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], - "tslib": ["tslib@2.7.0", "", {}, "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="], - - "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], - "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -418,24 +360,8 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "@scure/bip32/@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="], - - "@scure/bip32/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - - "@scure/bip39/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], - "ethereum-cryptography/@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="], - - "ethereum-cryptography/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - - "ethers/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], - - "ethers/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], - "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - - "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], } } diff --git a/docs/grvt/grvt-pysdk-main/.github/workflows/ci.yml b/docs/grvt/grvt-pysdk-main/.github/workflows/ci.yml new file mode 100644 index 0000000..0561eee --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: ci + +on: + push: + branches: + - main + +env: + UV_VERSION: "0.4.0" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + env: + UV_CACHE_DIR: /tmp/.uv-cache + runs-on: ubuntu-24.04 + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Set up python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version-file: ".python-version" + - name: Set up uv + run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh + - name: Restore uv cache + uses: actions/cache@v4 + with: + path: /tmp/.uv-cache + key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} + restore-keys: | + uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} + uv-${{ runner.os }} + - name: Install dependencies + run: uv sync --all-extras --dev --frozen + - name: Test with pytest + run: uv run pytest tests --cov=src + - name: Minimize uv cache + run: uv cache prune --ci + + build-image: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build docker image + uses: docker/build-push-action@v6 + with: + context: . + push: false diff --git a/docs/grvt/grvt-pysdk-main/.github/workflows/codeql.yml b/docs/grvt/grvt-pysdk-main/.github/workflows/codeql.yml new file mode 100644 index 0000000..9d24571 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/.github/workflows/codeql.yml @@ -0,0 +1,39 @@ +# name: "codeql" + +# on: +# push: +# branches: ["main"] +# pull_request: +# branches: ["main"] + +# jobs: +# analyze: +# name: Analyze +# runs-on: ubuntu-24.04 +# permissions: +# actions: read +# contents: read +# security-events: write + +# strategy: +# fail-fast: false +# matrix: +# language: [python] + +# steps: +# - name: Checkout +# uses: actions/checkout@v4 + +# - name: Initialize CodeQL +# uses: github/codeql-action/init@v3 +# with: +# languages: ${{ matrix.language }} +# queries: +security-and-quality + +# - name: Autobuild +# uses: github/codeql-action/autobuild@v3 + +# - name: Perform CodeQL Analysis +# uses: github/codeql-action/analyze@v3 +# with: +# category: "/language:${{ matrix.language }}" diff --git a/docs/grvt/grvt-pysdk-main/.github/workflows/pr.yml b/docs/grvt/grvt-pysdk-main/.github/workflows/pr.yml new file mode 100644 index 0000000..e29f558 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/.github/workflows/pr.yml @@ -0,0 +1,61 @@ +name: pr + +on: + pull_request: + branches: + - "**" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + env: + UV_CACHE_DIR: /tmp/.uv-cache + runs-on: ubuntu-24.04 + strategy: + matrix: + python-version: ["3.10.15", "3.10.x"] + uv-version: ["0.3.5", "0.4.0"] + fail-fast: false + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Set up python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Set up uv + run: curl -LsSf https://astral.sh/uv/${{ matrix.uv-version }}/install.sh | sh + - name: Restore uv cache + uses: actions/cache@v4 + with: + path: /tmp/.uv-cache + key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} + restore-keys: | + uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} + uv-${{ runner.os }} + - name: Install dependencies + run: uv sync --all-extras --dev --frozen + - name: Test with pytest + run: uv run pytest tests --cov=src + - name: Minimize uv cache + run: uv cache prune --ci + + build-image: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build docker image + uses: docker/build-push-action@v6 + with: + context: . + push: false diff --git a/docs/grvt/grvt-pysdk-main/.gitignore b/docs/grvt/grvt-pysdk-main/.gitignore new file mode 100644 index 0000000..33c82b6 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/.gitignore @@ -0,0 +1,287 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,visualstudiocode + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Tests +test_results.csv +test_results_sync.csv + +# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode diff --git a/docs/grvt/grvt-pysdk-main/.pre-commit-config.yaml b/docs/grvt/grvt-pysdk-main/.pre-commit-config.yaml new file mode 100644 index 0000000..072d7c8 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/.pre-commit-config.yaml @@ -0,0 +1,79 @@ +ci: + skip: [pytest] + +default_language_version: + python: python3.10 + +repos: + # general checks (see here: https://pre-commit.com/hooks.html) + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-yaml + args: [--allow-multiple-documents] + - id: end-of-file-fixer + - id: trailing-whitespace + + # ruff - linting + formatting + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.6.3" + hooks: + - id: ruff + name: ruff + - id: ruff-format + name: ruff-format + + # mypy - lint-like type checking + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.2 + hooks: + - id: mypy + name: mypy + + # docformatter - formats docstrings to follow PEP 257 + - repo: https://github.com/pycqa/docformatter + rev: v1.7.5 + hooks: + - id: docformatter + name: docformatter + args: + [ + -r, + -i, + --pre-summary-newline, + --make-summary-multi-line, + --wrap-summaries, + "90", + --wrap-descriptions, + "90", + src, + tests, + ] + exclude: ^(artifacts/.*)$ + + # bandit - find common security issues + - repo: https://github.com/pycqa/bandit + rev: 1.7.9 + hooks: + - id: bandit + name: bandit + exclude: ^tests/ + args: + - -r + - src + + - repo: local + hooks: + - id: pytest + name: pytest + entry: uv run pytest tests --cov=src + language: system + types: [python] + pass_filenames: false + + # prettier - formatting JS, CSS, JSON, Markdown, ... + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + exclude: ^(uv.lock)$ diff --git a/docs/grvt/grvt-pysdk-main/.python-version b/docs/grvt/grvt-pysdk-main/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/docs/grvt/grvt-pysdk-main/.renovaterc.json b/docs/grvt/grvt-pysdk-main/.renovaterc.json new file mode 100644 index 0000000..09da1a9 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/.renovaterc.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "config:base", + "schedule:daily", + "group:all", + ":prConcurrentLimitNone", + ":prHourlyLimitNone", + ":prImmediately" + ], + "labels": ["dependencies"] +} diff --git a/docs/grvt/grvt-pysdk-main/CHANGELOG.md b/docs/grvt/grvt-pysdk-main/CHANGELOG.md new file mode 100644 index 0000000..2b487da --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/CHANGELOG.md @@ -0,0 +1,78 @@ +# Changelog + +## Version [0.2.1] - 2025-07-24 + +### Changes in 0.2.1 + +- New methods in ccxt-compatible code for Strategy (Vault) managers to view investment history and current redemption queue of a Vault. + - Classes `GrvtCcxt` and `GrvtCcxtPro` + - Methods `fetch_vault_manager_investor_history` and `fetch_vault_redemption_queue` + +## Version [0.2.0] - 2025-07-16 + +### Changes in 0.2.0 + +- additional fixes for removal of explicit enums in `grvt_raw_types.py` +- Note: this update would `break existing integrations` for `non-create-order flows` (e.g., transfer/withdrawal history, transfer, get open orders, get-instrument filters) to handle currency strings directly instead of enums. +- users will be losing the currency enum when they upgrade, so they would need to call the currencies endpoin ( ), for which the support has been added in PySDK (`get_currency_v1()` in grvt_raw_async.py and grvt_raw_sync.py), for the metadata attached to the currency strings. + +## Version [0.1.32] - 2025-07-15 + +### Changes in 0.1.32 + +- removed explicit enums in `grvt_raw_types.py` (no need to update SDK for when new coins are listed) +- added vault-related functionality + +## Version [0.1.31] - 2025-07-08 + +### Changes in 0.1.31 + +- added AVAX in the raw code (testing purposes) + +## Version [0.1.30] - 2025-07-08 + +### Changes in 0.1.30 + +- added `H` in the **raw** code + +## Version [0.1.29] - 2025-06-30 + +### Changes in 0.1.29 + +- Added new currencies: `HYPE, UNI, MOODENG, LAUNCHCOIN` in the **raw** code +- Improvements and type fixes in `fetch_funding_rate_history()` methods of GrvtCcxt and GrvtCcxtPro. + +## Version [0.1.28] - 2025-06-02 + +### Changes in 0.1.28 + +- Renamed currency name `AI_16_Z` into `AI16Z` in the **raw** code to match the exchange currency name. + +## Version [0.1.27] - 2025-05-19 + +### Changes in 0.1.27 + +- `GrvtCcxt` and `GrvtCcxtPro` classes: + + - renamed method `fetch_balances()` to `fetch_balance()` as defined in ccxt. + +## Version [0.1.26] - 2025-05-15 + +### Added in 0.1.26 + +- `GrvtCcxt` and `GrvtCcxtPro` classes: + + - new method `describe()` - returns a list of public method names + - new method `fetch_balances()` - returns dict with balances in ccxt format. + - constructor parameter `order_book_ccxt_format: bool = False` . If = True then order book snapshots from `fetch_order_book()` are in ccxt format. + +### Fixed in 0.1.26 + +- Issues with typing and lynting errors + +## Version [0.1.25] - 2025-04-25 + +### Fixed in 0.1.25 + +- Issues with typing and lynting errors +- Fixed bug in test_grvt_ccxt.py diff --git a/docs/grvt/grvt-pysdk-main/LICENSE b/docs/grvt/grvt-pysdk-main/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/grvt/grvt-pysdk-main/Makefile b/docs/grvt/grvt-pysdk-main/Makefile new file mode 100644 index 0000000..b2c789b --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/Makefile @@ -0,0 +1,59 @@ +# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +.PHONY: help +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sed -e 's/^Makefile://' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + + +.PHONY: run +run: ## Run the project + uv run python -m pysdk.main + +.PHONY: test +test: ## Run the tests + uv run pytest tests --cov=src + +.PHONY: precommit +precommit: ## Run the pre-commit hooks + bash .git/hooks/pre-commit + +.PHONY: lint +lint: ## Run the linter + uv run ruff check . + +.PHONY: lint-fix +lint-fix: ## Run the linter and fix the issues + uv run ruff check --fix --unsafe-fixes . + +.PHONY: format +format: ## Run the formatter + uv run ruff format . + +.PHONY: typecheck +typecheck: ## Run the type checker + uv run mypy . + +.PHONY: security +security: ## Run the security checker + uv run bandit . + +.PHONY: clean +clean: ## Clean the project + uv run ruff clean . + +.PHONY: install +install: ## Install the project + uv sync --all-extras --dev --frozen + +.PHONY: build +build: ## Build the project + uv build + +.PHONY: publish +publish: ## Publish the project + python3 build_readme.py + make build + uv run twine upload --skip-existing dist/* + +# ============================================================================== +# always make sure the following is the last line on this file +.DEFAULT_GOAL := help diff --git a/docs/grvt/grvt-pysdk-main/README.md b/docs/grvt/grvt-pysdk-main/README.md new file mode 100644 index 0000000..e6038cc --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/README.md @@ -0,0 +1,300 @@ +

+ uv logo + pre-commit logo + ruff logo + bandit logo + pytest logo +

+ +

+ Docker logo + GitHub Actions logo +

+ +# GRVT Python SDK + +[![CodeQL](https://github.com/smarlhens/python-boilerplate/workflows/codeql/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml) +[![GitHub CI](https://github.com/smarlhens/python-boilerplate/workflows/ci/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml) +[![GitHub license](https://img.shields.io/github/license/smarlhens/python-boilerplate)](https://github.com/smarlhens/python-boilerplate) + +--- + +## Table of Contents + +- [GRVT Python SDK](#grvt-python-sdk) + - [Table of Contents](#table-of-contents) + - [What's in the library](#whats-in-the-library) + - [Environments](#environments) + - [Files](#files) + - [Installation via pip](#installation-via-pip) + - [Configuration](#configuration) + - [Usage](#usage) + - [Contributor's guide](#contributors-guide) + - [Prerequisites](#prerequisites) + - [Installation of a source code](#installation-of-a-source-code) + - [Manually run example files](#manually-run-example-files) + - [What's in the box ?](#whats-in-the-box-) + - [uv](#uv) + - [pre-commit](#pre-commit) + - [ruff](#ruff) + - [mypy](#mypy) + - [bandit](#bandit) + - [docformatter](#docformatter) + - [Testing](#testing) + - [Makefile](#makefile) + +--- + +## What's in the library + +GRVT Python SDK library provides Python classes and utility methods for easy access to GRVT API endpoints across all environments. + +### Environments + +- `prod` - Production environment. +- `testnet` - Testnet environment. +- `staging` - Development Integration environment. +- `dev` - Development environment. + +SDK Library provides two types of classes: + +1. **Raw** - thin wrapper classes around Rest API. +2. **Ccxt-compatible** - classes with ccxt-like methods. Provide access to both Rest API and WebSockets. + +### Files + +Raw access: + +- `grvt_raw_base.py` - base classes for Rest API access. +- `grvt_raw_env.py` - definitions of environments for raw access. +- `grvt_raw_signing.py` - utility methods for signing orders. +- `grvt_raw_sync.py` - class for raw synchronous calls to Rest API. +- `grvt_raw_async.py` - class for raw asynchronous calls to Rest API. + +CCXT-compatible access: + +- `grvt_ccxt_utils.py` - utility methods for signing orders. +- `grvt_ccxt_env.py` - definitions of environments for ccxt-like access. +- `grvt_ccxt_base.py` - base class for Rest API access. +- `grvt_ccxt.py` - class for synchronous calls to Rest API. +- `grvt_ccxt_pro.py` - class for asynchronous calls to Rest API. +- `grvt_ccxt_ws.py` - class for WebSocket calls. + +## Installation via pip + +```bash +pip install grvt-pysdk +``` + +### Configuration + +Setup these environment variables: + +```bash +export GRVT_PRIVATE_KEY="`Secret Private Key` in API setup" +export GRVT_API_KEY="`API Key` in API setup" +export GRVT_TRADING_ACCOUNT_ID=<`Trading account ID` in API> +export GRVT_ENV="testnet" +export GRVT_END_POINT_VERSION="v1" +export GRVT_WS_STREAM_VERSION="v1" +``` + +## Usage + +**Examples of how to use various methods to connect to GRVT API:** + +- [GRVT CCXT](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt.py) - Example of usage of CCXT-compatible Python class `GrvtCcxt` with `synchronous` Rest API calls. +- [GRVT CCXT Pro](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_pro.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtPro` with `asynchronous` Rest API calls. +- [GRVT CCXT WS](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_ws.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtWS` with `asynchronous` Rest API calls + Web Socket subscriptions + JSON RPC calls over Web Sockets. +- [GRVT API Sync](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_sync.py) - Synchronous API client for GRVT +- [GRVT API Async](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_async.py) - Asynchronous API client for GRVT + +## Contributor's guide + +### Prerequisites + +- [Python](https://www.python.org/downloads/) **>=3.10.0 < 3.13** (_tested with 3.10.15_) +- [pre-commit](https://pre-commit.com/#install) +- [uv](https://docs.astral.sh/uv/getting-started/installation/) **>=0.3.3** (_tested with 0.4.0_) +- [docker](https://docs.docker.com/get-docker/) (_optional_) + +### Installation of a source code + +1. Clone the git repository + + ```bash + git clone https://github.com/grvt-technologies/grvt-pysdk.git + ``` + +2. Go into the project directory + + ```bash + cd grvt-pysdk/ + ``` + +3. Checkout working branch + + ```bash + git checkout + ``` + +4. Install dependencies + + ```bash + make install + ``` + +5. Enable pre-commit hooks + + ```bash + pre-commit install + ``` + +--- + +### Manually run example files + +Example files run extensive testing of the SDK API classes and log details about details of using GRVT API. + +1. Change to tests folder + + ```bash + cd tests/pysdk + ``` + +2. Run example of using synchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt.py`, class `GrvtCcxt` + + ```bash + uv run python3 test_grvt_ccxt.py + ``` + +3. Run example of using asynchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt_pro.py`, class `GrvtCcxtPro` + + ```bash + uv run python3 test_grvt_ccxt_pro.py + ``` + +4. Run example of using WebSockets subscriptions and JSON RPC calls via `grvt_ccxt_ws.py`, class `GrvtCcxtWS` + + ```bash + uv run python3 test_grvt_ccxt_ws.py + ``` + +### What's in the box ? + +#### uv + +[uv](https://github.com/astral-sh/uv) is an extremely fast Python package and project manager, written in Rust. + +**pyproject.toml file** ([`pyproject.toml`](pyproject.toml)): orchestrate your project and its dependencies +**uv.lock file** ([`uv.lock`](uv.lock)): ensure that the package versions are consistent for everyone +working on your project + +For more configuration options and details, see the [configuration docs](https://docs.astral.sh/uv/). + +#### pre-commit + +[pre-commit](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks. + +**.pre-commit-config.yaml file** ([`.pre-commit-config.yaml`](.pre-commit-config.yaml)): describes what repositories and +hooks are installed + +For more configuration options and details, see the [configuration docs](https://pre-commit.com/). + +#### ruff + +[ruff](https://github.com/astral-sh/ruff) is an extremely fast Python linter, written in Rust. + +Rules are defined in the [`pyproject.toml`](pyproject.toml). + +For more configuration options and details, see the [configuration docs](https://github.com/astral-sh/ruff#configuration). + +#### mypy + +[mypy](http://mypy-lang.org/) is an optional static type checker for Python that aims to combine the benefits of +dynamic (or "duck") typing and static typing. + +Rules are defined in the [`pyproject.toml`](pyproject.toml). + +For more configuration options and details, see the [configuration docs](https://mypy.readthedocs.io/). + +#### bandit + +[bandit](https://bandit.readthedocs.io/) is a tool designed to find common security issues in Python code. + +Rules are defined in the [`pyproject.toml`](pyproject.toml). + +For more configuration options and details, see the [configuration docs](https://bandit.readthedocs.io/). + +#### docformatter + +[docformatter](https://github.com/PyCQA/docformatter) is a tool designed to format docstrings to +follow [PEP 257](https://peps.python.org/pep-0257/). + +Options are defined in the [`.pre-commit-config.yaml`](.pre-commit-config.yaml). + +--- + +#### Testing + +We are using [pytest](https://docs.pytest.org/) & [pytest-cov](https://github.com/pytest-dev/pytest-cov) to write tests. + +To run tests with coverage: + +```bash +make test +``` + +```text +Name Stmts Miss Cover +------------------------------------------------------------- +src/pysdk/__init__.py 0 0 100% +src/pysdk/grvt_ccxt.py 238 21 91% +src/pysdk/grvt_ccxt_base.py 196 57 71% +src/pysdk/grvt_ccxt_env.py 48 12 75% +src/pysdk/grvt_ccxt_logging_selector.py 15 7 53% +src/pysdk/grvt_ccxt_pro.py 245 69 72% +src/pysdk/grvt_ccxt_test_utils.py 30 4 87% +src/pysdk/grvt_ccxt_types.py 41 0 100% +src/pysdk/grvt_ccxt_utils.py 237 89 62% +src/pysdk/grvt_ccxt_ws.py 275 238 13% +src/pysdk/grvt_raw_async.py 149 109 27% +src/pysdk/grvt_raw_base.py 154 69 55% +src/pysdk/grvt_raw_env.py 27 5 81% +src/pysdk/grvt_raw_signing.py 33 17 48% +src/pysdk/grvt_raw_sync.py 149 109 27% +src/pysdk/grvt_raw_types.py 1031 0 100% +------------------------------------------------------------- +TOTAL 2868 806 72% + + +=================================================================================================== 8 passed in 58.20s ==================================================================================================== +``` + +#### Makefile + +We are using [Makefile](https://www.gnu.org/software/make/manual/make.html) to manage the project. + +To see the list of commands: + +```bash +make +``` + +```bash +➜ grvt-pysdk git:(main) ✗ make +help Show this help +run Run the project +test Run the tests +precommit Run the pre-commit hooks +lint Run the linter +format Run the formatter +typecheck Run the type checker +security Run the security checker +clean Clean the project +install Install the project +build Build the project +publish Publish the project +``` + +--- diff --git a/docs/grvt/grvt-pysdk-main/README_PYPI.md b/docs/grvt/grvt-pysdk-main/README_PYPI.md new file mode 100644 index 0000000..3dae020 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/README_PYPI.md @@ -0,0 +1,382 @@ +

+ uv logo + pre-commit logo + ruff logo + bandit logo + pytest logo +

+ +

+ Docker logo + GitHub Actions logo +

+ +# GRVT Python SDK + +[![CodeQL](https://github.com/smarlhens/python-boilerplate/workflows/codeql/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml) +[![GitHub CI](https://github.com/smarlhens/python-boilerplate/workflows/ci/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml) +[![GitHub license](https://img.shields.io/github/license/smarlhens/python-boilerplate)](https://github.com/smarlhens/python-boilerplate) + +--- + +## Table of Contents + +- [GRVT Python SDK](#grvt-python-sdk) + - [Table of Contents](#table-of-contents) + - [What's in the library](#whats-in-the-library) + - [Environments](#environments) + - [Files](#files) + - [Installation via pip](#installation-via-pip) + - [Configuration](#configuration) + - [Usage](#usage) + - [Contributor's guide](#contributors-guide) + - [Prerequisites](#prerequisites) + - [Installation of a source code](#installation-of-a-source-code) + - [Manually run example files](#manually-run-example-files) + - [What's in the box ?](#whats-in-the-box-) + - [uv](#uv) + - [pre-commit](#pre-commit) + - [ruff](#ruff) + - [mypy](#mypy) + - [bandit](#bandit) + - [docformatter](#docformatter) + - [Testing](#testing) + - [Makefile](#makefile) + +--- + +## What's in the library + +GRVT Python SDK library provides Python classes and utility methods for easy access to GRVT API endpoints across all environments. + +### Environments + +- `prod` - Production environment. +- `testnet` - Testnet environment. +- `staging` - Development Integration environment. +- `dev` - Development environment. + +SDK Library provides two types of classes: + +1. **Raw** - thin wrapper classes around Rest API. +2. **Ccxt-compatible** - classes with ccxt-like methods. Provide access to both Rest API and WebSockets. + +### Files + +Raw access: + +- `grvt_raw_base.py` - base classes for Rest API access. +- `grvt_raw_env.py` - definitions of environments for raw access. +- `grvt_raw_signing.py` - utility methods for signing orders. +- `grvt_raw_sync.py` - class for raw synchronous calls to Rest API. +- `grvt_raw_async.py` - class for raw asynchronous calls to Rest API. + +CCXT-compatible access: + +- `grvt_ccxt_utils.py` - utility methods for signing orders. +- `grvt_ccxt_env.py` - definitions of environments for ccxt-like access. +- `grvt_ccxt_base.py` - base class for Rest API access. +- `grvt_ccxt.py` - class for synchronous calls to Rest API. +- `grvt_ccxt_pro.py` - class for asynchronous calls to Rest API. +- `grvt_ccxt_ws.py` - class for WebSocket calls. + +## Installation via pip + +```bash +pip install grvt-pysdk +``` + +### Configuration + +Setup these environment variables: + +```bash +export GRVT_PRIVATE_KEY="`Secret Private Key` in API setup" +export GRVT_API_KEY="`API Key` in API setup" +export GRVT_TRADING_ACCOUNT_ID=<`Trading account ID` in API> +export GRVT_ENV="testnet" +export GRVT_END_POINT_VERSION="v1" +export GRVT_WS_STREAM_VERSION="v1" +``` + +## Usage + +**Examples of how to use various methods to connect to GRVT API:** + +- [GRVT CCXT](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt.py) - Example of usage of CCXT-compatible Python class `GrvtCcxt` with `synchronous` Rest API calls. +- [GRVT CCXT Pro](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_pro.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtPro` with `asynchronous` Rest API calls. +- [GRVT CCXT WS](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_ws.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtWS` with `asynchronous` Rest API calls + Web Socket subscriptions + JSON RPC calls over Web Sockets. +- [GRVT API Sync](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_sync.py) - Synchronous API client for GRVT +- [GRVT API Async](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_async.py) - Asynchronous API client for GRVT + +## Contributor's guide + +### Prerequisites + +- [Python](https://www.python.org/downloads/) **>=3.10.0 < 3.13** (_tested with 3.10.15_) +- [pre-commit](https://pre-commit.com/#install) +- [uv](https://docs.astral.sh/uv/getting-started/installation/) **>=0.3.3** (_tested with 0.4.0_) +- [docker](https://docs.docker.com/get-docker/) (_optional_) + +### Installation of a source code + +1. Clone the git repository + + ```bash + git clone https://github.com/grvt-technologies/grvt-pysdk.git + ``` + +2. Go into the project directory + + ```bash + cd grvt-pysdk/ + ``` + +3. Checkout working branch + + ```bash + git checkout + ``` + +4. Install dependencies + + ```bash + make install + ``` + +5. Enable pre-commit hooks + + ```bash + pre-commit install + ``` + +--- + +### Manually run example files + +Example files run extensive testing of the SDK API classes and log details about details of using GRVT API. + +1. Change to tests folder + + ```bash + cd tests/pysdk + ``` + +2. Run example of using synchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt.py`, class `GrvtCcxt` + + ```bash + uv run python3 test_grvt_ccxt.py + ``` + +3. Run example of using asynchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt_pro.py`, class `GrvtCcxtPro` + + ```bash + uv run python3 test_grvt_ccxt_pro.py + ``` + +4. Run example of using WebSockets subscriptions and JSON RPC calls via `grvt_ccxt_ws.py`, class `GrvtCcxtWS` + + ```bash + uv run python3 test_grvt_ccxt_ws.py + ``` + +### What's in the box ? + +#### uv + +[uv](https://github.com/astral-sh/uv) is an extremely fast Python package and project manager, written in Rust. + +**pyproject.toml file** ([`pyproject.toml`](pyproject.toml)): orchestrate your project and its dependencies +**uv.lock file** ([`uv.lock`](uv.lock)): ensure that the package versions are consistent for everyone +working on your project + +For more configuration options and details, see the [configuration docs](https://docs.astral.sh/uv/). + +#### pre-commit + +[pre-commit](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks. + +**.pre-commit-config.yaml file** ([`.pre-commit-config.yaml`](.pre-commit-config.yaml)): describes what repositories and +hooks are installed + +For more configuration options and details, see the [configuration docs](https://pre-commit.com/). + +#### ruff + +[ruff](https://github.com/astral-sh/ruff) is an extremely fast Python linter, written in Rust. + +Rules are defined in the [`pyproject.toml`](pyproject.toml). + +For more configuration options and details, see the [configuration docs](https://github.com/astral-sh/ruff#configuration). + +#### mypy + +[mypy](http://mypy-lang.org/) is an optional static type checker for Python that aims to combine the benefits of +dynamic (or "duck") typing and static typing. + +Rules are defined in the [`pyproject.toml`](pyproject.toml). + +For more configuration options and details, see the [configuration docs](https://mypy.readthedocs.io/). + +#### bandit + +[bandit](https://bandit.readthedocs.io/) is a tool designed to find common security issues in Python code. + +Rules are defined in the [`pyproject.toml`](pyproject.toml). + +For more configuration options and details, see the [configuration docs](https://bandit.readthedocs.io/). + +#### docformatter + +[docformatter](https://github.com/PyCQA/docformatter) is a tool designed to format docstrings to +follow [PEP 257](https://peps.python.org/pep-0257/). + +Options are defined in the [`.pre-commit-config.yaml`](.pre-commit-config.yaml). + +--- + +#### Testing + +We are using [pytest](https://docs.pytest.org/) & [pytest-cov](https://github.com/pytest-dev/pytest-cov) to write tests. + +To run tests with coverage: + +```bash +make test +``` + +```text +Name Stmts Miss Cover +------------------------------------------------------------- +src/pysdk/__init__.py 0 0 100% +src/pysdk/grvt_ccxt.py 238 21 91% +src/pysdk/grvt_ccxt_base.py 196 57 71% +src/pysdk/grvt_ccxt_env.py 48 12 75% +src/pysdk/grvt_ccxt_logging_selector.py 15 7 53% +src/pysdk/grvt_ccxt_pro.py 245 69 72% +src/pysdk/grvt_ccxt_test_utils.py 30 4 87% +src/pysdk/grvt_ccxt_types.py 41 0 100% +src/pysdk/grvt_ccxt_utils.py 237 89 62% +src/pysdk/grvt_ccxt_ws.py 275 238 13% +src/pysdk/grvt_raw_async.py 149 109 27% +src/pysdk/grvt_raw_base.py 154 69 55% +src/pysdk/grvt_raw_env.py 27 5 81% +src/pysdk/grvt_raw_signing.py 33 17 48% +src/pysdk/grvt_raw_sync.py 149 109 27% +src/pysdk/grvt_raw_types.py 1031 0 100% +------------------------------------------------------------- +TOTAL 2868 806 72% + + +=================================================================================================== 8 passed in 58.20s ==================================================================================================== +``` + +#### Makefile + +We are using [Makefile](https://www.gnu.org/software/make/manual/make.html) to manage the project. + +To see the list of commands: + +```bash +make +``` + +```bash +➜ grvt-pysdk git:(main) ✗ make +help Show this help +run Run the project +test Run the tests +precommit Run the pre-commit hooks +lint Run the linter +format Run the formatter +typecheck Run the type checker +security Run the security checker +clean Clean the project +install Install the project +build Build the project +publish Publish the project +``` + +--- + + +## Changelog + +# Changelog + +## Version [0.2.1] - 2025-07-24 + +### Changes in 0.2.1 + +- New methods in ccxt-compatible code for Strategy (Vault) managers to view investment history and current redemption queue of a Vault. + - Classes `GrvtCcxt` and `GrvtCcxtPro` + - Methods `fetch_vault_manager_investor_history` and `fetch_vault_redemption_queue` + +## Version [0.2.0] - 2025-07-16 + +### Changes in 0.2.0 + +- additional fixes for removal of explicit enums in `grvt_raw_types.py` +- Note: this update would `break existing integrations` for `non-create-order flows` (e.g., transfer/withdrawal history, transfer, get open orders, get-instrument filters) to handle currency strings directly instead of enums. +- users will be losing the currency enum when they upgrade, so they would need to call the currencies endpoin ( ), for which the support has been added in PySDK (`get_currency_v1()` in grvt_raw_async.py and grvt_raw_sync.py), for the metadata attached to the currency strings. + +## Version [0.1.32] - 2025-07-15 + +### Changes in 0.1.32 + +- removed explicit enums in `grvt_raw_types.py` (no need to update SDK for when new coins are listed) +- added vault-related functionality + +## Version [0.1.31] - 2025-07-08 + +### Changes in 0.1.31 + +- added AVAX in the raw code (testing purposes) + +## Version [0.1.30] - 2025-07-08 + +### Changes in 0.1.30 + +- added `H` in the **raw** code + +## Version [0.1.29] - 2025-06-30 + +### Changes in 0.1.29 + +- Added new currencies: `HYPE, UNI, MOODENG, LAUNCHCOIN` in the **raw** code +- Improvements and type fixes in `fetch_funding_rate_history()` methods of GrvtCcxt and GrvtCcxtPro. + +## Version [0.1.28] - 2025-06-02 + +### Changes in 0.1.28 + +- Renamed currency name `AI_16_Z` into `AI16Z` in the **raw** code to match the exchange currency name. + +## Version [0.1.27] - 2025-05-19 + +### Changes in 0.1.27 + +- `GrvtCcxt` and `GrvtCcxtPro` classes: + + - renamed method `fetch_balances()` to `fetch_balance()` as defined in ccxt. + +## Version [0.1.26] - 2025-05-15 + +### Added in 0.1.26 + +- `GrvtCcxt` and `GrvtCcxtPro` classes: + + - new method `describe()` - returns a list of public method names + - new method `fetch_balances()` - returns dict with balances in ccxt format. + - constructor parameter `order_book_ccxt_format: bool = False` . If = True then order book snapshots from `fetch_order_book()` are in ccxt format. + +### Fixed in 0.1.26 + +- Issues with typing and lynting errors + +## Version [0.1.25] - 2025-04-25 + +### Fixed in 0.1.25 + +- Issues with typing and lynting errors +- Fixed bug in test_grvt_ccxt.py diff --git a/docs/grvt/grvt-pysdk-main/build_readme.py b/docs/grvt/grvt-pysdk-main/build_readme.py new file mode 100644 index 0000000..c23bd8d --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/build_readme.py @@ -0,0 +1,9 @@ +from pathlib import Path + +readme = Path("README.md").read_text() +changelog = Path("CHANGELOG.md").read_text() + +combined = f"{readme}\n\n## Changelog\n\n{changelog}" + +Path("README_PYPI.md").write_text(combined) +print("✅ Combined README.md and CHANGELOG.md into README_PYPI.md") diff --git a/docs/grvt/grvt-pysdk-main/pyproject.toml b/docs/grvt/grvt-pysdk-main/pyproject.toml new file mode 100644 index 0000000..99fea3a --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/pyproject.toml @@ -0,0 +1,106 @@ +[project] +name = "grvt-pysdk" +version = "0.2.1" + +description = "GRVT Python SDK" +requires-python = ">=3.10" +license = { file = "LICENSE" } +authors = [ + { name = "GRVT", email = "contact@grvt.io" }, +] +readme = { file = "README_PYPI.md", content-type = "text/markdown" } +dependencies = [ + "aiohttp>=3.10.11", + "backports-weakref>=1.0.post1", + "dacite>=1.8.1", + "dataclasses-json>=0.6.7", + "eth-account>=0.13.4", + "inflection>=0.5.1", + "requests>=2.32.3", + "websockets==13.1", +] + +[project.urls] +homepage = "https://github.com/gravity-technologies/grvt-pysdk" +repository = "https://github.com/gravity-technologies/grvt-pysdk" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/pysdk"] + +[tool.uv] +dev-dependencies = [ + "pytest>=8.3.2", + "pytest-cov>=5.0.0", + "mypy>=1.11.2", + "bandit>=1.7.9", + "docformatter>=1.7.5", + "ruff>=0.6.2", + "twine>=5.1.1", +] + +[tool.pytest.ini_options] +addopts = "-vvv" +testpaths = "tests" + +[tool.ruff] +extend-exclude = [ + "__pycache__", + "build", + "dist", +] +target-version = "py312" +line-length = 90 +src = ["src", "tests"] + +[tool.ruff.lint] +extend-select = [ + "C4", + "D200", + "D201", + "D204", + "D205", + "D206", + "D210", + "D211", + "D213", + "D300", + "D400", + "D402", + "D403", + "D404", + "D419", + "E", + "F", + "G010", + "I001", + "INP001", + "N805", + "PERF101", + "PERF102", + "PERF401", + "PERF402", + "PGH004", + "PGH005", + "PIE794", + "PIE796", + "PIE807", + "PIE810", + "RET502", + "RET503", + "RET504", + "RET505", + "RUF015", + "RUF100", + "S101", + "T20", + "UP", + "W", +] + +[tool.mypy] +files = ["src", "tests"] +strict = "true" diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/__init__.py b/docs/grvt/grvt-pysdk-main/src/pysdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt.py new file mode 100644 index 0000000..5f5d7ec --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt.py @@ -0,0 +1,812 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 +import json +import logging +from typing import Any, Literal + +import requests + +from .grvt_ccxt_base import GrvtCcxtBase +from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint +from .grvt_ccxt_types import ( + Amount, + GrvtInstrumentKind, + GrvtInvalidOrder, + GrvtOrderSide, + GrvtOrderType, + Num, +) +from .grvt_ccxt_utils import ( + EnumEncoder, + GrvtOrder, + get_cookie_with_expiration, + get_grvt_order, + get_order_payload, +) + + +class GrvtCcxt(GrvtCcxtBase): + """ + GrvtCcxt class to interact with Grvt Rest API in synchronous mode. + + Args: + env: GrvtEnv (DEV, TESTNET, PROD) + logger: logging.Logger + parameters: dict with trading_account_id, private_key, api_key etc + + Examples: + >>> from grvt_api import GrvtCcxt + >>> from grvt_env import GrvtEnv + >>> grvt = GrvtCcxt(env=GrvtEnv.TESTNET) + >>> grvt.fetch_markets() + """ + + def __init__( + self, + env: GrvtEnv, + logger: logging.Logger | None = None, + parameters: dict = {}, + order_book_ccxt_format: bool = False, + ): + """Initialize the GrvtCcxt instance.""" + super().__init__(env, logger, parameters, order_book_ccxt_format) + self._clsname: str = type(self).__name__ + self._session: requests.Session = requests.Session() + self._session.headers.update({"Content-Type": "application/json"}) + self.refresh_cookie() + # Assign markets here + self.markets: dict[str, dict] = self.load_markets() + + def refresh_cookie(self) -> dict | None: + """Refresh the session cookie.""" + if not self.should_refresh_cookie(): + return self._cookie + path = get_grvt_endpoint(self.env, "AUTH") + self._cookie = get_cookie_with_expiration(path, self._api_key) + self._path_return_value_map[path] = self._cookie + if self._cookie: + self._session.cookies.update({"gravity": self._cookie["gravity"]}) + if self._cookie["X-Grvt-Account-Id"]: + self._session.headers.update( + {"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]} + ) + self.logger.info( + f"refresh_cookie {self._cookie=} {self._session.cookies=} {self._session.headers=}" + ) + return self._cookie + + # PRIVATE API CALLS + def _auth_and_post(self, path: str, payload: dict) -> dict: + FN = f"_auth_and_post {path=}" + MAX_LEN_TO_LOG = 1280 + response: dict = {} + if not path: + self.logger.warning(f"{FN} Invalid path {path=} {payload=}") + raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}") + # Always see if need to referesh cookie before sending a request + self.refresh_cookie() + payload_json = json.dumps(payload, cls=EnumEncoder) + self.logger.info(f"{FN} {payload=}\n{payload_json=}") + return_value = self._session.post(path, data=payload_json, timeout=5) + return_text: str = "" + try: + return_text = return_value.text + response = return_value.json() + except Exception as err: + self.logger.warning(f"{FN} Unable to parse {return_value=} as json. {err=}") + if not return_value.ok: + self.logger.warning(f"{FN} ERROR {payload_json=}\n{return_value=}\n{response=}") + else: + if len(return_text) > MAX_LEN_TO_LOG: + self.logger.debug(f"{FN} OK {return_value=} {response=}") + self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**") + else: + self.logger.info(f"{FN} OK {return_value=} {response=}") + self._path_return_value_map[path] = response + return response + + def _create_grvt_order(self, order: GrvtOrder) -> dict: + """ + Send a GrvtOrder object to the exchange. + :param order: The GrvtOrder object. + Return: dictionary representing the order response. + """ + FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}" + order_payload = get_order_payload( + order, + private_key=self._private_key, + env=self.env, + instruments=self.markets, + ) + path = get_grvt_endpoint(self.env, "CREATE_ORDER") + self.logger.info(f"{FN} {path=} {order_payload=}") + response: dict = self._auth_and_post(path, payload=order_payload) + if response.get("result") is None: + self.logger.error(f"{FN} Error: {response}") + return {} + self.logger.info( + f"{FN} Order created:" + f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}" + ) + return response.get("result", {}) + + def create_order( + self, + symbol: str, + order_type: GrvtOrderType, + side: GrvtOrderSide, + amount: Amount, + price: Num = None, + params={}, + ) -> dict: + """Ccxt compliant signature.""" + self._check_account_auth() + self._check_valid_symbol(symbol) + # Validate order fields + self._check_order_arguments(order_type, side, amount, price) + # create GrvtOrder object + order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60) + order = get_grvt_order( + sub_account_id=self.get_trading_account_id(), + symbol=symbol, + order_type=order_type, + side=side, + amount=amount, + limit_price=price, + order_duration_secs=order_duration_secs, + params=params, + ) + return self._create_grvt_order(order) + + def create_limit_order( + self, + symbol: str, + side: GrvtOrderSide, + amount: Amount, + price: Num = None, + params={}, + ) -> dict: + return self.create_order(symbol, "limit", side, amount, price, params) + + def cancel_all_orders( + self, + params: dict = {}, + ) -> bool: + """ + Ccxt compliant signature BUT lacks symbol + Cancel all orders for a sub-account. + params: dictionary with parameters. Valid keys:
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Defaults to all.
+ """ + self._check_account_auth() + FN = f"{self._clsname} cancel_all_orders" + payload: dict = self._get_payload_cancel_all_orders(params) + path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS") + response: dict = self._auth_and_post(path, payload) + cancel_ack = response.get("result", {}).get("ack") + + if not cancel_ack: + self.logger.warning(f"{FN} failed to cancel orders: {response=}") + return False + self.logger.info(f"{FN} Cancelled {response=}") + return True + + def cancel_order( + self, + id: str | None = None, + symbol: str | None = None, + params: dict = {}, + ) -> bool: + """ + Ccxt compliant signature + Cancel specific order for the account.
+ Private call requires authorization.
+ See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order) + for details.
. + + Args: + id (str): exchange assigned order ID
+ symbol (str): trading symbol
+ params: + * client_order_id (str): client assigned order ID
+ * time_to_live_ms (str): lifetime of cancel requiest in millisecs
+ Returns: + True if cancel request was acked by exchange. False otherwise.
+ """ + FN = f"{self._clsname} cancel_order" + self._check_account_auth() + payload: dict = { + "sub_account_id": self.get_trading_account_id(), + } + if id: + payload["order_id"] = str(id) + elif "client_order_id" in params: + payload["client_order_id"] = str(params["client_order_id"]) + else: + raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id") + + if "time_to_live_ms" in params: + payload["time_to_live_ms"] = str(params["time_to_live_ms"]) + + path = get_grvt_endpoint(self.env, "CANCEL_ORDER") + self.logger.info( + f"{FN} Send cancel {payload=} for trading_account_id={self.get_trading_account_id()}" + ) + response: dict = self._auth_and_post(path, payload) + cancel_ack = response.get("result", {}).get("ack") + + if not cancel_ack: + self.logger.warning(f"{FN} failed to cancel order: {response=}") + return False + self.logger.info(f"{FN} Cancelled {response=}") + return True + + def set_derisk_mm_ratio(self, ratio: str) -> bool: + """ + Ccxt compliant signature + Set the Derisk to Maintenance marginb ratio for the account. + Private call requires authorization. + See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio) + for details. + + Args: + ratio (Amount): The new derisking market making ratio. + + Returns: + True if the request was acknowledged by the exchange. False otherwise. + """ + FN = f"{self._clsname} set_derisk_mm_ratio" + self._check_account_auth() + payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio)) + path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO") + self.logger.info( + f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}" + ) + response: dict = self._auth_and_post(path, payload) + # set_ack = response.get("result", {}).get("ack") + # if not set_ack: + # self.logger.warning(f"{FN} failed to set derisk_mm_ratio: {response=}") + # return False + self.logger.info(f"{FN} Set derisk_mm_ratio {response=}") + return True + + def fetch_open_orders( + self, + symbol: str | None = None, + since: int | None = None, + limit: int | None = None, + params: dict = {}, + ) -> list[dict]: + """ + Ccxt compliant signature + Fetch open orders for the account.
+ Private call requires authorization.
+ See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders) + for details.
. + + Args: + symbol: (str) get orders for this symbol only.
+ since: ccxt-compliant argument, NOT SUPPORTED.
+ limit: ccxt-compliant argument, NOT SUPPORTED.
+ params: dictionary with parameters. Valid keys:
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Defaults to all.
+ Returns: + a list of dictionaries, each dict represent an order.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_open_orders(symbol, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS") + response: dict = self._auth_and_post(path, payload) + open_orders: list = response.get("result", []) + if symbol: + open_orders = [ + o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol + ] + return open_orders + + def fetch_order( + self, + id: str | None = None, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature + Get Order status by order_id or client_order_id + Private call requires authorization.
+ See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders) + for details.
+ Args: + id: (str) order_id to fetch.
+ params: dictionary with parameters. Valid keys:
+ `client_order_id` (int): client assigned order ID.
+ Return: dict with order's details or {} if order was NOT found. + """ + self._check_account_auth() + payload = { + "sub_account_id": self.get_trading_account_id(), + } + if id: + payload["order_id"] = id + elif "client_order_id" in params: + payload["client_order_id"] = str(params["client_order_id"]) + else: + raise GrvtInvalidOrder( + f"{self._clsname} fetch_order() requires order_id or params['client_order_id']" + ) + path = get_grvt_endpoint(self.env, "GET_ORDER") + response: dict = self._auth_and_post(path, payload) + return response + + def fetch_order_history(self, params: dict = {}) -> dict: + """ + Ccxt compliant signature, HISTORICAL data.
+ Get Order status by order_id or client_order_id + Private call requires authorization.
+ See [Order History](https://api-docs.grvt.io/trading_api/#order-history) + for details.
+ Args: + params: dictionary with parameters. Valid keys:
+ `kind`: (str) - The kind filter to apply. Defaults to all kinds.
+ `base`: (str) - The base currency filter. Defaults to all base currencies.
+ `quote`: (str) - The quote currency filter. Defaults to all quote currencies.
+ `expiration`: (int) The expiration time in nanoseconds. Defaults to all.
+ `strike_price`: (str) The strike price to apply. Defaults to all strike prices.
+ `limit`: (int) The limit to query for. Defaults to 500; Max 1000.
+ `cursor`: (str) The cursor to use for pagination. If nil, return the first page.
+ Return: a dictionary with keys: + `total` : total number of account history snapshots.
+ `next` : cursor for the next page.
+ `result` : a list of dictionaries, each dict represent an order state.
. + """ + self._check_account_auth() + payload = self._get_payload_fetch_order_history(params) + path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY") + response: dict = self._auth_and_post(path, payload) + return response + + def get_account_summary( + self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account" + ) -> dict: + """ + Return: The account summary. + Private call requires authorization.
+ See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary) + for details.
+ Returns: dictionary with account data.
. + """ + FN = f"{self._clsname} get_account_summary {type=}" + self._check_account_auth() + payload = {} + if type == "sub-account": + path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY") + payload = {"sub_account_id": self.get_trading_account_id()} + elif type == "funding": + path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY") + elif type == "aggregated": + path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY") + else: + raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}") + + response: dict = self._auth_and_post(path, payload=payload) + sub_account: dict = response.get("result", {}) + if not sub_account: + self.logger.info(f"{FN} No account summary for {path=} {payload=}") + return sub_account + + def fetch_balance( + self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account" + ) -> dict: + """ + Ccxt compliant signature + Fetch balances for the account.
+ Private call requires authorization.
+ See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary) + for details.
. + + Args: + type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'. + Valid values: 'sub-account', 'funding', 'aggregated'. + + Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.
. + """ + account_summary: dict = self.get_account_summary(type) + return self._get_balances_from_account_summary(account_summary) + + def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict: + """ + HISTORICAL data.
+ Get account history.
+ Private call requires authorization.
+ See [Account History](https://api-docs.grvt.io/trading_api/#account-history) + for details.
. + + Args: + limit: maximum number of account snapshots per page to fetch.
+ params: dictionary with parameters. Valid keys:
+ `start_time` (int): fetch orders since this timestamp in nanoseconds.
+ `end_time` (int): fetch orders until this timestamp in nanoseconds.
+ `cursor` (str): cursor for the pagination. If cursor is present then we ignore + `start_time` and `end_time`.
+ Returns: + a dictionary with keys: + `total` : total number of account history snapshots.
+ `next` : cursor for the next page.
+ `result` : list of account history snapshots.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_account_history(limit, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY") + response: dict = self._auth_and_post(path, payload=payload) + return response + + def fetch_positions(self, symbols: list[str] = [], params={}): + """ + Ccxt compliant signature + Fetch positions for the account.
+ Private call requires authorization.
+ See [Positions](https://api-docs.grvt.io/trading_api/#positions) + for details.
. + + Args: + symbols: list(str) get positions for these symbols only.
+ + Returns: list of dictionaries, each dict represent a position.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_positions(symbols, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_POSITIONS") + response: dict = self._auth_and_post(path, payload) + positions: list = response.get("result", []) + if symbols: + self.logger.info(f"fetch_positions filter positions by {symbols=}") + positions = [p for p in positions if p.get("instrument") in symbols] + return positions + + def fetch_my_trades( + self, + symbol: str | None = None, + since: int | None = None, + limit: int | None = None, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature, HISTORICAL data.
+ Fetch past trades for the account.
+ Private call requires authorization.
+ See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history) + for details.
. + + Args: + symbol: get trades for this symbol only.
+ since: fetch trades since this timestamp in nanoseconds.
+ limit: maximum number of trades to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Default: 'USDT'.
+ + Returns: + a dictionary with keys: + `total` : total number of account history snapshots.
+ `next` : cursor for the next page.
+ `result` : a list of dictionaries, each dict represent a trade.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_my_trades(symbol, since, limit, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY") + response: dict = self._auth_and_post(path, payload=payload) + if symbol: + # filter result by symbol + trades: list = response.get("result", []) + trades = [t for t in trades if t.get("instrument") == symbol] + response["result"] = trades + return response + + # **************** PUBLIC API CALLS + def load_markets(self) -> dict[str, dict]: + self.logger.info("load_markets START") + instruments = self.fetch_markets( + params={ + "kind": GrvtInstrumentKind.PERPETUAL, + # "base": "BTC", + # "quote": "USDT", + } + ) + if instruments: + self.markets = { + str(i.get("instrument", "")): i for i in instruments if i.get("instrument") + } + self.logger.info(f"load_markets: loaded {len(self.markets)} markets.") + else: + self.logger.warning("load_markets: No markets found.") + return self.markets + + def fetch_markets( + self, + params: dict = {}, + ) -> list[dict]: + """ + Ccxt compliant signature + Retrieve the list of all instruments of matching kind, base and quote + supported by the exchange. + + Params: dict with keys:
+ `is_active` (bool) - defaults to True.
+ `limit` (int) - defaiults to 20.
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Default: 'USDT'.
+ + Returns: list of dictionaries per instrument with keys:
+ `instrument`: symbol e.g. 'BTC_USDT_Perp'.
+ `instrument_hash`: hashed symbol for order signing e.g. '0x030501'.
+ `base`: base currency e.g. 'BTC'.
+ `quote`: quote currency e.g. 'USDT'.
+ `kind`: kind of instrument 'PERPETUAL'/'FUTURE'.
+ 'base_decimals': size multiplier for order signing.
+ `tick_size`: price tick size.
+ `min_size`: minimum order size.
+ """ + # Prepare request payload + payload = self._get_payload_fetch_markets(params) + # Make the POST request to get all instruments + path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS") + response: dict = self._auth_and_post(path, payload) + return response.get("result", []) + + def fetch_all_markets( + self, + is_active: bool | None = True, + ) -> list[dict]: + """ + Retrieve the list of all instruments supported by the exchange.
+ Params:
+ `is_active` (bool) - defaults to True.
. + + Returns: list of dictionaries per instrument. See fetch_markets().
+ """ + # Prepare request payload + payload = {"is_active": is_active} + # Make the POST request to get all instruments + path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS") + response: dict = self._auth_and_post(path, payload) + return response.get("result", []) + + def fetch_market(self, symbol: str) -> dict: + """ + Retrieve the instrument object for a given symbol. + :param symbol: The symbol of the instrument. + """ + # Make the POST request to get all instruments + path = get_grvt_endpoint(self.env, "GET_INSTRUMENT") + response: dict = self._auth_and_post(path, payload={"instrument": symbol}) + return response.get("result", []) + + def fetch_ticker(self, symbol: str, params: dict = {}) -> dict: + """ + Ccxt compliant signature + Retrieve the ticker of a given symbol. + :param symbol: The instrument name. + :return: The ticker dictionary of the instrument. + """ + # {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp', + # 'mark_price': '59373870996065', 'index_price': '59395287961367', + # 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000', + # 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price': + # '592737000', 'best_ask_size': '21678', 'funding_rate_curr': 2544, 'funding_rate_avg': 0, + # 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000', + # 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500', + # 'sell_volume_q': '68764000329900', 'high_price': '3435450000', 'low_price': '100000', + # 'open_price': '32554000000000', 'open_interest': '8174350000000', + # 'long_short_ratio': 1.0948905} + path = get_grvt_endpoint(self.env, "GET_TICKER") + response: dict = self._auth_and_post(path, payload={"instrument": symbol}) + return response.get("result", []) + + def fetch_mini_ticker(self, symbol: str) -> dict: + """ + Retrieve the mini-ticker of a given symbol. + :param symbol: The instrument name. + :return: The mini-ticker dictionary of the instrument. + """ + # {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp', + # 'mark_price': '59373870996065', 'index_price': '59395287961367', + # 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000', + # 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price': + # '59273700000000', 'best_ask_size': '21678000000'} + path = get_grvt_endpoint(self.env, "GET_MINI_TICKER") + response: dict = self._auth_and_post(path, payload={"instrument": symbol}) + return response.get("result", []) + + def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict: + """ + Ccxt compliant signature + Retrieve the order book of a given symbol. + :param symbol: The instrument name. + :return: The order book dictionary of the instrument. + """ + # {'event_time': '0', 'instrument': 'BTC_USDT_Perp', + # 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...] + # 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...] + payload = {"instrument": symbol, "aggregate": 1} + if limit: + payload["depth"] = limit + path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK") + response: dict = self._auth_and_post(path, payload=payload) + if self.is_order_book_ccxt_format(): + # Convert to ccxt format + return self.convert_grvt_ob_to_ccxt(response.get("result", {})) + return response.get("result", {}) + + def fetch_recent_trades( + self, + symbol: str, + limit: int | None = None, + ) -> list: + """ + Retrieve recent trades of a given instrument. + :param symbol: The instrument name. + :return: The list of trades for the instrument. + """ + # List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp', + # 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000', + # 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0, + # 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'} + payload: dict[str, str | int] = {"instrument": symbol} + if limit: + payload["limit"] = limit + path = get_grvt_endpoint(self.env, "GET_TRADES") + response: dict = self._auth_and_post(path, payload=payload) + return response.get("result", []) + + def fetch_trades( + self, + symbol: str, + since: int | None = None, + limit: int = 10, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature, HISTORICAL data.
+ Retrieve trade history of a given instrument. + :param symbol: The instrument name. + :return: dict with field 'result' containing a list of trades. + """ + # List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp', + # 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000', + # 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0, + # 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'} + payload: dict = self._get_payload_fetch_trades( + symbol, + since=since, + limit=limit, + params=params, + ) + path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY") + response: dict = self._auth_and_post(path, payload=payload) + return response + + def fetch_funding_rate_history( + self, + symbol: str, + since: int = 0, + limit: int = 1_000, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature, HISTORICAL data.
+ Retrieve the funding rates history of a given instrument.
+ Args: + symbol (str): The instrument name.
+ since (int): fetch trades since this timestamp in nanoseconds.
+ limit: int - maximum number of trades to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `end_time` (int): end time in nanoseconds.
+ Returns: + dict with field 'result' containing list of dictionaries repesenting funding rate + at a point in time with fields:
+ `instrument` (str): instrument name.
+ 'funding_rate' (float): funding rate.
+ 'funding_time' (int): funding time in nanoseconds.
+ 'mark_price' (float): mark price.
. + """ + payload: dict[str, str | int] = {"instrument": symbol} + if params.get("cursor"): + payload["cursor"] = params["cursor"] + else: + if since: + payload["start_time"] = str(since) + if params.get("end_time"): + payload["end_time"] = str(params["end_time"]) + if limit: + payload["limit"] = int(limit) + path = get_grvt_endpoint(self.env, "GET_FUNDING") + response: dict = self._auth_and_post(path, payload=payload) + return response + + def fetch_ohlcv( + self, + symbol: str, + timeframe="1m", + since: int = 0, + limit: int = 10, + params={}, + ) -> dict: + """ + Ccxt compliant signature, HISTORICAL data. + + Retrieve the ohlc history of a given instrument. + + Args: + symbol: The instrument name. + timeframe: The timeframe of the ohlc. See `ccxt_interval_to_grvt_candlestick_interval`. + since: fetch ohlc since this timestamp in nanoseconds. + limit: maximum number of ohlc to fetch. + params: dictionary with parameters. Valid keys: + `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters. + `end_time` (int): end time in nanoseconds. + `candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'. + Returns: + dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:
+ `instrument` - instrument name.
+ `open_time` - start of interval in nanoseconds.
+ `close_time` - end of interval in nanoseconds.
+ `open` - opening price.
+ `close` - closing price.
+ `high` - highest price.
+ `low` - lowest price.
+ `volume_u` - volume in units.
+ `volume_q` - volume in quote(USDT).
+ `trades` - number of trades.
+ """ + FN = f"{self._clsname} fetch_ohlcv" + payload: dict[str, Any] = self._get_payload_fetch_ohlcv( + symbol, timeframe, since, limit, params + ) + self.logger.info(f"{FN} {payload=}") + path = get_grvt_endpoint(self.env, "GET_CANDLESTICK") + return self._auth_and_post(path, payload=payload) + + # Vault Management APIs + def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict: + payload: dict = self._get_fetch_vault_manager_investor_history_payload( + vault_id=self.get_trading_account_id(), + only_own_investments=only_own_investments, # Default to False to fetch all investments + ) + path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY") + # path = "https://trades.grvt.io/full/v1/vault_manager_investor_history" + return self._auth_and_post(path, payload=payload) + + def fetch_vault_redemption_queue(self): + payload: dict = self._get_fetch_vault_redemption_queue_payload( + vault_id=self.get_trading_account_id() + ) + path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE") + # path = "https://trades.grvt.io/full/v1/vault_view_redemption_queue" + return self._auth_and_post(path, payload=payload) diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_base.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_base.py new file mode 100644 index 0000000..03e8530 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_base.py @@ -0,0 +1,586 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 + +import logging +import time +from datetime import datetime +from decimal import Decimal +from typing import Any, get_args + +from .grvt_ccxt_env import GrvtEnv +from .grvt_ccxt_types import ( + CandlestickInterval, + CandlestickType, + GrvtInvalidOrder, + GrvtOrderSide, + GrvtOrderType, + Num, + ccxt_interval_to_grvt_candlestick_interval, +) +from .grvt_ccxt_utils import get_kuq_from_symbol, sign_derisk_mm_ratio_request + +# COOKIE_REFRESH_INTERVAL_SECS = 60 * 60 # 30 minutes + + +class GrvtCcxtBase: + """ + GrvtCcxtBase is an abstract class for other Grvt Rest + and WebSocket connectivity classes. + + Args: + env: GrvtCcxtBase (DEV, TESTNET, PROD) + logger (logging.Logger, optional). Defaults to None. + parameters: (dict, optional). Dict with trading_account_id, private_key, api_key etc + defaults to empty. + """ + + def __init__( + self, + env: GrvtEnv, + logger: logging.Logger | None = None, + parameters: dict = {}, + order_book_ccxt_format: bool = False, + ): + """Initialize the GrvtCcxtBase part.""" + self.name: str = "GRVT" + self.logger = logger or logging.getLogger(__name__) + self.env: GrvtEnv = env + self._trading_account_id: str | None = parameters.get("trading_account_id") + self._private_key: str = str(parameters.get("private_key", "")) + self._api_key: str = str(parameters.get("api_key", "")) + self._order_book_ccxt_format: bool = order_book_ccxt_format + + self._path_return_value_map: dict = {} + self._cookie: dict | None = None + self.markets: dict = {} + self._clsname: str = type(self).__name__ + self.logger.info(f"GrvtCcxtBase: {self.env=}, {self._trading_account_id=}") + + def describe(self) -> list[str]: + """Returns the description of the class methods.""" + return [ + "create_order", + "create_limit_order", + "cancel_all_orders", + "cancel_order", + "fetch_balance", + "fetch_open_orders", + "fetch_order", + "fetch_order_history", + "get_account_summary", + "fetch_account_history", + "fetch_positions", + "fetch_my_trades", + "load_markets", + "fetch_markets", + "fetch_all_markets", + "fetch_market", + "fetch_ticker", + "fetch_mini_ticker", + "fetch_order_book", + "fetch_recent_trades", + "fetch_trades", + "fetch_funding_rate_history", + "fetch_ohlcv", + ] + + def get_trading_account_id(self) -> str: + """Returns the trading account id.""" + return self._trading_account_id or "" + + def is_order_book_ccxt_format(self) -> bool: + """Returns True if order book should be returned in CCXT format.""" + return self._order_book_ccxt_format + + def should_refresh_cookie(self) -> bool: + """ + Retuns: + True if this object has API key and the session cookie should be refreshed. + False - otherwise. + """ + if not self._api_key: + return False + time_till_expiration = None + if self._cookie and "expires" in self._cookie: + time_till_expiration = self._cookie["expires"] - time.time() + is_cookie_fresh = time_till_expiration is not None and time_till_expiration > 5 + if not is_cookie_fresh: + self.logger.info( + f"cookie should be refreshed {self._cookie=} now={time.time()}" + f" {time_till_expiration=} secs" + ) + return not is_cookie_fresh + + def get_path_return_value_map(self) -> dict: + """Returns the path return value map.""" + return self._path_return_value_map + + def get_endpoint_return_value(self, endpoint: str) -> dict: + """Returns the return value for the endpoint.""" + return self._path_return_value_map.get(endpoint, {}) + + def was_path_called(self, path: str) -> bool: + """Returns True if the path was called.""" + return path in self._path_return_value_map + + # PRIVATE API CALLS + + def _check_order_arguments( + self, order_type: GrvtOrderType, side: GrvtOrderSide, amount: Num, price: Num + ) -> None: + FN = f"{self._clsname} _check_order_arguments" + if order_type not in get_args(GrvtOrderType): + raise GrvtInvalidOrder(f"{FN}: order_type should be one of {get_args(GrvtOrderType)}") + if side not in get_args(GrvtOrderSide): + raise GrvtInvalidOrder(f"{FN}: side should be one of {get_args(GrvtOrderSide)}") + if order_type == "limit": + if price is None or Decimal(price) <= Decimal("0"): + raise GrvtInvalidOrder(f"{FN}: requires a price argument for a limit order") + elif order_type == "market": + if price: + raise GrvtInvalidOrder( + f"{FN}: should not have a positive price argument for a market order" + ) + if not amount or Decimal(amount) < Decimal("0"): + raise GrvtInvalidOrder(f"{FN}: amount should be above 0") + + def _check_account_auth(self) -> bool: + if not self.get_trading_account_id(): + raise GrvtInvalidOrder(f"{self._clsname}: this action requires a trading_account_id") + return True + + def _check_valid_symbol(self, symbol: str) -> bool: + if not self.markets: + raise GrvtInvalidOrder(f"{self._clsname}: markets not loaded") + market = self.markets.get(symbol) + if not market: + raise GrvtInvalidOrder(f"{self._clsname}: {symbol=} not found") + return True + + def _get_payload_cancel_all_orders( + self, + params: dict = {}, + ) -> dict: + """ + Prepares payload for fetch_order_history() method.
. + + Args: + params: (dict) with possible keys as:.
+ `kind`: (str) - The kind filter to apply. Defaults to all kinds.
+ `base`: (str) - The base currency filter. Defaults to all base currencies.
+ `quote`: (str) - The quote currency filter. Defaults to all quote currencies.
+ Returns: a dictionary with a payload for Rest API call to cancel all orders.
+ """ + payload: dict[str, str | int | bool | list] = { + "sub_account_id": str(self.get_trading_account_id()) + } + + if "kind" in params: + payload["kind"] = [params["kind"]] + if "base" in params: + payload["base"] = [params["base"]] + if "quote" in params: + payload["quote"] = [params["quote"]] + return payload + + def _get_payload_fetch_markets(self, params: dict) -> dict: + payload: dict[str, str | int | bool | list] = {} + if params.get("kind"): + payload["kind"] = [params.get("kind")] + if params.get("base"): + payload["base"] = [params.get("base")] + if params.get("quote"): + payload["quote"] = [params.get("quote")] + payload["limit"] = int(params.get("limit", 1_000)) + payload["is_active"] = bool(params.get("is_active", True)) + return payload + + def _get_payload_fetch_my_trades( + self, + symbol: str | None = None, + since: int | None = None, + limit: int | None = None, + params: dict = {}, + ) -> dict: + """ + Prepares payload for fetch_my_trades() method.
. + + Args: + symbol: get trades for this symbol only.
+ since: fetch trades since this timestamp in nanoseconds.
+ limit: maximum number of trades to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Default: 'USDT'.
+ `end_time` (int): fetch trades until this timestamp in nanoseconds.
+ + Returns: + a dictionary with a payload for Rest API call to fetch trades.
+ """ + payload: dict[str, str | int | list] = { + "sub_account_id": str(self.get_trading_account_id()) + } + if params.get("cursor"): + payload["cursor"] = params["cursor"] + else: + if symbol: + payload["instrument"] = symbol + else: + if "kind" in params: + payload["kind"] = [params["kind"]] + if "base" in params: + payload["base"] = [params["base"]] + if "quote" in params: + payload["quote"] = [params["quote"]] + if since: + payload["start_time"] = str(since) + if params.get("end_time"): + payload["end_time"] = str(params["end_time"]) + if limit: + payload["limit"] = int(limit) + return payload + + def _get_payload_fetch_trades( + self, + symbol: str, + since: int | None = None, + limit: int = 1_000, + params: dict = {}, + ) -> dict: + """ + Prepares payload for fetch_trades() method.
. + + Args: + symbol: get trades for this symbol only.
+ since: fetch trades since this timestamp in nanoseconds.
+ limit: maximum number of trades to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Default: 'USDT'.
+ + Returns: + a dictionary with a payload for Rest API call to fetch trades.
+ """ + payload: dict[str, str | int] = { + "sub_account_id": str(self.get_trading_account_id()), + "instrument": symbol, + } + if params.get("cursor"): + payload["cursor"] = params["cursor"] + else: + if since: + payload["start_time"] = str(since) + if params.get("end_time"): + payload["end_time"] = str(params["end_time"]) + payload["limit"] = limit + return payload + + def _get_payload_fetch_account_history( + self, + # since: int | None = None, + limit: int = 500, + params: dict = {}, + ) -> dict: + """ + Prepares payload for fetch_account_history() method.
. + + Args: + limit: maximum number of trades to fetch.
+ params: dictionary with parameters. Valid keys:
+ `start_time` (int): fetch orders since this timestamp in nanoseconds.
+ `end_time` (int): fetch orders until this timestamp in nanoseconds.
+ `cursor` (int):cursor for the pagination. If cursor is present then we ignore + `start_time` and `end_time`.
+ Returns: + a dictionary with a payload for Rest API call to fetch account history.
+ """ + payload: dict[str, str | int] = {"sub_account_id": str(self.get_trading_account_id())} + + if params.get("cursor"): + payload["cursor"] = params["cursor"] + else: + start_time = params.get("start_time") + end_time = params.get("end_time") + if start_time: + payload["start_time"] = str(start_time) + if end_time: + payload["end_time"] = str(end_time) + payload["limit"] = limit | 500 + return payload + + def _get_payload_fetch_positions(self, symbols: list[str] = [], params={}) -> dict: + """ + Prepares payload for fetch_positions() method.
. + + Args: + symbols: list(str) get positions for these symbols only.
+ + Returns: a dictionary with a payload for Rest API call to fetch positions.
+ """ + payload: dict[str, str | int | bool | list] = { + "sub_account_id": str(self.get_trading_account_id()) + } + if symbols: + ks, us, qs = [], [], [] + for symbol in symbols: + try: + k, u, q = get_kuq_from_symbol(symbol) + ks.append(k) + us.append(u) + qs.append(q) + except Exception as e: + raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_positions {e}") + payload["kind"] = list(set(ks)) + payload["base"] = list(set(us)) + payload["quote"] = list(set(qs)) + else: + if "kind" in params: + payload["kind"] = [params["kind"]] + if "base" in params: + payload["base"] = [params["base"]] + if "quote" in params: + payload["quote"] = [params["quote"]] + return payload + + def _get_payload_fetch_order_history( + self, + params: dict, + ) -> dict: + """ + Prepares payload for fetch_order_history() method.
. + + Args: + params: (dict) with possible keys as:.
+ `kind`: (str) - The kind filter to apply. Defaults to all kinds.
+ `base`: (str) - The base currency filter. Defaults to all base currencies.
+ `quote`: (str) - The quote currency filter. Defaults to all quote currencies.
+ `expiration`: (int) The expiration time in nanoseconds. Defaults to all.
+ `strike_price`: (str) The strike price to apply. Defaults to all strike prices.
+ `limit`: (int) The limit to query for. Defaults to 500; Max 1000.
+ `cursor`: (str) The cursor to use for pagination. If nil, return the first page.
+ Returns: a dictionary with a payload for Rest API call to fetch order history.
+ """ + payload: dict[str, str | int | bool | list] = { + "sub_account_id": str(self.get_trading_account_id()) + } + if "limit" in params: + payload["limit"] = params["limit"] + if params.get("cursor"): + payload["cursor"] = params["cursor"] + else: + if "kind" in params: + payload["kind"] = [params["kind"]] + if "base" in params: + payload["base"] = [params["base"]] + if "quote" in params: + payload["quote"] = [params["quote"]] + if "expiration" in params: + payload["expiration"] = [params["expiration"]] + if "strike_price" in params: + payload["strike_price"] = [params["strike_price"]] + return payload + + def _get_payload_fetch_open_orders( + self, + symbol: str | None = None, + params: dict = {}, + ) -> dict: + """ + Prepares payload for fetch_order_history() method.
. + + Args: + params: (dict) with possible keys as:.
+ `kind`: (str) - The kind filter to apply. Defaults to all kinds.
+ `base`: (str) - The base currency filter. Defaults to all base currencies.
+ `quote`: (str) - The quote currency filter. Defaults to all quote currencies.
+ Returns: a dictionary with a payload for Rest API call to fetch order history.
+ """ + payload: dict[str, str | int | bool | list] = { + "sub_account_id": str(self.get_trading_account_id()) + } + if symbol: + try: + k, u, q = get_kuq_from_symbol(symbol) + payload["kind"] = [k] + payload["base"] = [u] + payload["quote"] = [q] + except Exception as e: + raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_open_orders {e}") + else: + if "kind" in params: + payload["kind"] = [params["kind"]] + if "base" in params: + payload["base"] = [params["base"]] + if "quote" in params: + payload["quote"] = [params["quote"]] + return payload + + def _get_payload_fetch_ohlcv( + self, + symbol: str, + timeframe: str, + since: int, + limit: int, + params={}, + ) -> dict: + """ + Prepares payload for fetch_ohlcv() method.
. + + Args: + symbol: The instrument name.
+ timeframe: The timeframe of the ohlc. + See `ccxt_interval_to_grvt_candlestick_interval`.
+ since: fetch ohlc since this timestamp in nanoseconds.
+ limit: maximum number of ohlc to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `end_time` (int): end time in nanoseconds.
+ `candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.
+ + Returns: a dictionary with a payload for Rest API call to fetch_ohlcv.
+ See [Candlestick] (https://api-docs.grvt.io/market_data_api/#candlestick_1) + for more details.
+ """ + if timeframe not in ccxt_interval_to_grvt_candlestick_interval: + raise ValueError(f"Invalid timeframe {timeframe}") + + interval: CandlestickInterval = ccxt_interval_to_grvt_candlestick_interval[timeframe] + payload: dict[str, str | int | bool | list] = {"instrument": symbol} + if params.get("cursor"): + payload["cursor"] = params["cursor"] + else: + if interval: + payload["interval"] = interval.value + candle_type = CandlestickType.TRADE + if "candle_type" in params: + candle_type = CandlestickType[params["candle_type"]] + payload["type"] = candle_type.value + if since: + payload["start_time"] = str(since) + if "end_time" in params: + payload["end_time"] = str(params["end_time"]) + if limit: + payload["limit"] = int(limit) + return payload + + def _get_balances_from_account_summary(self, account_summary: dict) -> dict: + balances: dict = {} + balances["info"] = account_summary.get("spot_balances", []) + balances["timestamp"] = int(int(account_summary.get("event_time", 0)) / 1_000_000) + balances["datetime"] = ( + datetime.fromtimestamp(balances["timestamp"] / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[ + :-3 + ] + + "Z" + ) + balances["total"] = {} + balances["free"] = {} + balances["used"] = {} + for currency_balance in account_summary.get("spot_balances", []): + if not currency_balance or not isinstance(currency_balance, dict): + continue + currency: str = currency_balance.get("currency", "") + if not currency: + continue + balances[currency] = {"total": currency_balance.get("balance", "0.0")} + balances["total"][currency] = balances[currency]["total"] + if currency == "USDT": + balances[currency]["free"] = account_summary.get("available_balance", "0.0") + balances[currency]["used"] = str( + Decimal(balances[currency]["total"]) - Decimal(balances[currency]["free"]) + ) + else: + balances[currency]["free"] = balances[currency]["total"] + balances[currency]["used"] = "0.0" + + balances["free"][currency] = balances[currency]["free"] + balances["used"][currency] = balances[currency]["used"] + return balances + + def _get_set_derisk_mm_ratio_payload( + self, + ratio: str, + ) -> dict[str, str | dict]: + """ + Returns a payload for setting the derisking market making ratio. + """ + payload: dict[str, str | dict] = { + "sub_account_id": self.get_trading_account_id(), + "ratio": str(ratio), + } + signature: dict = sign_derisk_mm_ratio_request( + self.env, int(self.get_trading_account_id()), str(ratio), self._private_key + ) + payload["signature"] = signature + return payload + + def convert_grvt_ob_to_ccxt(self, order_book: dict) -> dict: + """ + Converts GRVT-specific order book format to CCXT format. + """ + ob_time_ms: int = int(order_book["event_time"]) // 1_000_000 + ccxt_ob = { + "symbol": order_book["instrument"], + "bids": [], + "asks": [], + "timestamp": ob_time_ms, + "datetime": datetime.fromtimestamp(ob_time_ms / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[ + :-3 + ] + + "Z", + "nonce": int(order_book["event_time"]), + } + ccxt_ob["bids"] = [[bid["price"], bid["size"]] for bid in order_book["bids"]] + ccxt_ob["asks"] = [[ask["price"], ask["size"]] for ask in order_book["asks"]] + return ccxt_ob + + # Vault Management APIs + def _get_fetch_vault_manager_investor_history_payload( + self, + vault_id: str, + only_own_investments: bool = False, + ) -> dict: + """ + Prepares payload for fetch_vault_manager_investor_history() method.
. + + Args: + vault_id: The vault id to fetch history for.
+ only_own_investments: If True, fetch only investments by the manager.
+ + Returns: + A dictionary with a payload for Rest API call to fetch vault investor history. + """ + payload: dict[str, str | bool] = { + "vault_id": vault_id, + "only_own_investments": only_own_investments, + } + return payload + + def _get_fetch_vault_redemption_queue_payload( + self, + vault_id: str, + ) -> dict: + """ + Prepares payload for fetch_vault_redemption_queue() method.
. + + Args: + vault_id: The vault id to fetch redemption queue for.
+ + Returns: + A dictionary with a payload for Rest API call to fetch vault redemption queue. + """ + return {"vault_id": vault_id} diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_env.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_env.py new file mode 100644 index 0000000..1771ba0 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_env.py @@ -0,0 +1,195 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 + +import os +from enum import Enum + + +class GrvtEnv(str, Enum): + PROD = "prod" + TESTNET = "testnet" + STAGING = "staging" + DEV = "dev" + +# GrvtEndpointType defines the root path for a family of endpoints +class GrvtEndpointType(str, Enum): + EDGE = "edge" + TRADE_DATA = "tdg" + MARKET_DATA = "mdg" + + +class GrvtWSEndpointType(str, Enum): + TRADE_DATA = "tdg" + MARKET_DATA = "mdg" + TRADE_DATA_RPC_FULL = "tdg_rpc_full" + MARKET_DATA_RPC_FULL = "mdg_rpc_full" + + +END_POINT_VERSION = os.getenv("GRVT_END_POINT_VERSION", "v1") + + +def get_grvt_endpoint_domains(env_name: str) -> dict[GrvtEndpointType, str]: + if env_name == GrvtEnv.PROD.value: + return { + GrvtEndpointType.EDGE: "https://edge.grvt.io", + GrvtEndpointType.TRADE_DATA: "https://trades.grvt.io", + GrvtEndpointType.MARKET_DATA: "https://market-data.grvt.io", + } + if env_name == GrvtEnv.TESTNET.value: + return { + GrvtEndpointType.EDGE: f"https://edge.{env_name}.grvt.io", + GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.grvt.io", + GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.grvt.io", + } + if env_name == GrvtEnv.STAGING.value: + return { + GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io", + GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io", + GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io", + } + if env_name == GrvtEnv.DEV.value: + return { + GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io", + GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io", + GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io", + } + return {} + + +def get_grvt_ws_endpoint( + env: str, + endpoint_type: GrvtWSEndpointType, +) -> str: + """Returns string pointing to WS endpoint for given environment and endpoint type.""" + if env == GrvtEnv.PROD.value: + return { + GrvtWSEndpointType.TRADE_DATA: "wss://trades.grvt.io/ws", + GrvtWSEndpointType.MARKET_DATA: "wss://market-data.grvt.io/ws", + GrvtWSEndpointType.TRADE_DATA_RPC_FULL: "wss://trades.grvt.io/ws/full", + GrvtWSEndpointType.MARKET_DATA_RPC_FULL: "wss://market-data.grvt.io/ws/full", + }.get(endpoint_type, "") + if env == GrvtEnv.TESTNET.value: + return { + GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.grvt.io/ws", + GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.grvt.io/ws", + GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.grvt.io/ws/full", + GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.grvt.io/ws/full", + }.get(endpoint_type, "") + if env == GrvtEnv.STAGING.value: + return { + GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws", + GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws", + GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full", + GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full", + }.get(endpoint_type, "") + if env == GrvtEnv.DEV.value: + return { + GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws", + GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws", + GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full", + GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full", + }.get(endpoint_type, "") + return "" + +# Mapping of WS stream names to DEFAULT endpoint types +GRVT_WS_STREAMS = { + # ******* Market Data ******** + "mini.s": GrvtWSEndpointType.MARKET_DATA, + "mini.d": GrvtWSEndpointType.MARKET_DATA, + "ticker.s": GrvtWSEndpointType.MARKET_DATA, + "ticker.d": GrvtWSEndpointType.MARKET_DATA, + "book.s": GrvtWSEndpointType.MARKET_DATA, + "book.d": GrvtWSEndpointType.MARKET_DATA, + "trade": GrvtWSEndpointType.MARKET_DATA, + "candle": GrvtWSEndpointType.MARKET_DATA, + # ******* Trade Data ******** + "order": GrvtWSEndpointType.TRADE_DATA, + "state": GrvtWSEndpointType.TRADE_DATA, + "cancel": GrvtWSEndpointType.TRADE_DATA, + "position": GrvtWSEndpointType.TRADE_DATA, + "fill": GrvtWSEndpointType.TRADE_DATA, + "transfer": GrvtWSEndpointType.TRADE_DATA, + "deposit": GrvtWSEndpointType.TRADE_DATA, + "withdrawal": GrvtWSEndpointType.TRADE_DATA, +} + + +def is_trading_ws_endpoint(end_point_type: GrvtWSEndpointType) -> bool: + return end_point_type in [ + GrvtWSEndpointType.TRADE_DATA, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + ] + + +# "wss://market-data.testnet.grvt.io/ws" +# wss://trades.testnet.grvt.io/ws +# GRVT_ENDPOINTS defines the endpoint paths grouped by endpoint type +GRVT_ENDPOINTS = { + GrvtEndpointType.EDGE: { + "GRAPHQL": "query", + "AUTH": "auth/api_key/login", + }, + GrvtEndpointType.TRADE_DATA: { + "CREATE_ORDER": f"full/{END_POINT_VERSION}/create_order", + "CANCEL_ALL_ORDERS": f"full/{END_POINT_VERSION}/cancel_all_orders", + "CANCEL_ORDER": f"full/{END_POINT_VERSION}/cancel_order", + "GET_OPEN_ORDERS": f"full/{END_POINT_VERSION}/open_orders", + "GET_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/account_summary", + "GET_FUNDING_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/funding_account_summary", + "GET_AGGREGATED_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/aggregated_account_summary", + "GET_ACCOUNT_HISTORY": f"full/{END_POINT_VERSION}/account_history", + "GET_POSITIONS": f"full/{END_POINT_VERSION}/positions", + "GET_ORDER": f"full/{END_POINT_VERSION}/order", + "GET_ORDER_HISTORY": f"full/{END_POINT_VERSION}/order_history", + "GET_FILL_HISTORY": f"full/{END_POINT_VERSION}/fill_history", + "SET_DERISK_MM_RATIO": f"full/{END_POINT_VERSION}/set_derisk_mm_ratio", + "GET_VAULT_MANAGER_INVESTOR_HISTORY": f"full/{END_POINT_VERSION}/vault_manager_investor_history", + "GET_VAULT_REDEMPTION_QUEUE": f"full/{END_POINT_VERSION}/vault_view_redemption_queue", + }, + GrvtEndpointType.MARKET_DATA: { + "GET_ALL_INSTRUMENTS": f"full/{END_POINT_VERSION}/all_instruments", + "GET_INSTRUMENTS": f"full/{END_POINT_VERSION}/instruments", + "GET_INSTRUMENT": f"full/{END_POINT_VERSION}/instrument", + "GET_TICKER": f"full/{END_POINT_VERSION}/ticker", + "GET_MINI_TICKER": f"full/{END_POINT_VERSION}/mini", + "GET_ORDER_BOOK": f"full/{END_POINT_VERSION}/book", + "GET_TRADES": f"full/{END_POINT_VERSION}/trade", + "GET_TRADE_HISTORY": f"full/{END_POINT_VERSION}/trade_history", + "GET_FUNDING": f"full/{END_POINT_VERSION}/funding", + "GET_CANDLESTICK": f"full/{END_POINT_VERSION}/kline", + }, +} + + +def get_grvt_endpoint(environment: GrvtEnv, end_point: str) -> str: + # if end_point == "GET_ALL_INSTRUMENTS": + # return "https://market-data.testnet.grvt.io/full/v1/instruments" + endpoint_domains = get_grvt_endpoint_domains(environment.value) + for endpoints_type, endpoints in GRVT_ENDPOINTS.items(): + if end_point in endpoints: + return f"{endpoint_domains[endpoints_type]}/{endpoints[end_point]}" + return "" + + +def get_all_grvt_endpoints(environment: GrvtEnv) -> dict[str, str]: + endpoint_domains = get_grvt_endpoint_domains(environment.value) + endpoints = {} + for endpoints_type, endpoints_map in GRVT_ENDPOINTS.items(): + for endpoint, path in endpoints_map.items(): + endpoints[endpoint] = f"{endpoint_domains[endpoints_type]}/{path}" + return endpoints + + +CHAIN_IDS = { + GrvtEnv.DEV.value: 327, + GrvtEnv.STAGING.value: 327, + GrvtEnv.TESTNET.value: 326, + GrvtEnv.PROD.value: 325, +} + +######################################################## diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_logging_selector.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_logging_selector.py new file mode 100644 index 0000000..40ddd72 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_logging_selector.py @@ -0,0 +1,32 @@ +import logging +import os +import sys +from datetime import datetime + +LOG_FILE = os.getenv("LOG_FILE", "FALSE").upper() +GRVT_ENV = os.getenv("GRVT_ENV") + +if LOG_FILE == "TRUE": + LOG_TIMESTAMP = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + fn = sys.argv[0].split("/")[-1] + fn_base = fn.split(".")[0] + if GRVT_ENV: + filename = f"logs/{fn_base}_{GRVT_ENV}_{LOG_TIMESTAMP}.log" + else: + filename = f"logs/{fn_base}_{LOG_TIMESTAMP}.log" + os.makedirs("logs", exist_ok=True) + logging.basicConfig( + filename=filename, + level=os.getenv("LOGGING_LEVEL", "INFO"), + format="%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + logger = logging.getLogger(__name__) + logger.info(f"Using FILE logger {LOG_FILE=}") +else: + logging.basicConfig( + level=os.getenv("LOGGING_LEVEL", "INFO"), + format="%(asctime)s - %(levelname)s - %(message)s", + ) + logger = logging.getLogger(__name__) + logger.info(f"Using CONSOLE logger {LOG_FILE=}") diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_pro.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_pro.py new file mode 100644 index 0000000..a50f0e8 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_pro.py @@ -0,0 +1,841 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 + +import asyncio +import json +import logging +from typing import Literal + +import aiohttp + +from .grvt_ccxt_base import GrvtCcxtBase + +# import requests +# from env import ENDPOINTS +from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint +from .grvt_ccxt_types import ( + Amount, + GrvtInstrumentKind, + GrvtInvalidOrder, + GrvtOrderSide, + GrvtOrderType, + Num, +) +from .grvt_ccxt_utils import ( + EnumEncoder, + GrvtOrder, + get_cookie_with_expiration, + get_cookie_with_expiration_async, + get_grvt_order, + get_order_payload, +) + + +class GrvtCcxtPro(GrvtCcxtBase): + """ + GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode. + + Args: + env: GrvtCcxtPro (DEV, TESTNET, PROD) + parameters: dict with trading_account_id, private_key, api_key etc + + Examples: + >>> from grvt_api_pro import GrvtCcxtPro + >>> from grvt_env import GrvtEnv + >>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET) + >>> await grvt.fetch_markets() + """ + + def __init__( + self, + env: GrvtEnv, + logger: logging.Logger | None = None, + parameters: dict = {}, + order_book_ccxt_format: bool = False, + ): + """Initialize the GrvtCcxt instance.""" + super().__init__(env, logger, parameters, order_book_ccxt_format) + self._clsname: str = type(self).__name__ + self._session = aiohttp.ClientSession(headers={"Content-Type": "application/json"}) + # Force sync call to get cookie here + self._cookie = get_cookie_with_expiration( + get_grvt_endpoint(self.env, "AUTH"), self._api_key + ) + self.update_session_with_cookie() + + def __del__(self): + """Close the aiohttp session when the instance is deleted.""" + self.logger.info(f"{self._clsname} __del__() called") + if self._session: + self.logger.info(f"{self._clsname} closing session") + asyncio.get_running_loop().create_task(self._session.close()) + + def update_session_with_cookie(self) -> None: + if self._cookie: + self._session.cookie_jar.update_cookies({"gravity": self._cookie["gravity"]}) + if self._cookie["X-Grvt-Account-Id"]: + self._session.headers.update( + {"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]} + ) + self.logger.info( + f"update_session_with_cookie {self._cookie=} {self._session.cookie_jar=}" + f" {self._session.headers=}" + ) + + async def refresh_cookie(self) -> dict | None: + """Refresh the session cookie.""" + if not self.should_refresh_cookie(): + return self._cookie + path: str = get_grvt_endpoint(self.env, "AUTH") + self._cookie = await get_cookie_with_expiration_async(path, self._api_key) + self._path_return_value_map[path] = self._cookie + self.update_session_with_cookie() + return self._cookie + + # PRIVATE API CALLS + async def _auth_and_post(self, path: str, payload: dict) -> dict: + FN = f"{self._clsname} _auth_and_post {path=}" + MAX_LEN_TO_LOG = 1280 + response: dict = {} + if not path: + self.logger.warning(f"{FN} Invalid path {path=} {payload=}") + raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}") + # Always see if need to referesh cookie before sending a request + await self.refresh_cookie() + payload_json = json.dumps(payload, cls=EnumEncoder) + self.logger.info(f"{FN} {payload=}\n{payload_json=}") + return_text: str = "" + async with self._session.post( + url=path, + data=payload_json, + headers={"Content-Type": "application/json"}, + timeout=5, + ) as return_value: + return_text: str = "" + try: + return_text = await return_value.text() + response = await return_value.json(content_type="application/json") + except Exception as err: + self.logger.warning( + f"{FN} Unable to parse {return_value=} as " + f" json(content_type='application/json'). {err=}" + ) + if not return_value.ok: + self.logger.warning(f"{FN} {payload_json=}\n{return_value=}\n{response=}") + else: + if len(return_text) > MAX_LEN_TO_LOG: + self.logger.debug(f"{FN} OK {return_value=} response={response}") + self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**") + else: + self.logger.info(f"{FN} OK {return_value=} response={response}") + self._path_return_value_map[path] = response + return response or {} + + async def _create_grvt_order(self, order: GrvtOrder) -> dict: + """ + Send a GrvtOrder object to the exchange. + :param order: The GrvtOrder object. + Return: dictionary representing the order response. + """ + FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}" + order_payload = get_order_payload( + order, + private_key=self._private_key, + env=self.env, + instruments=self.markets, + ) + path = get_grvt_endpoint(self.env, "CREATE_ORDER") + self.logger.info(f"{FN} {path=} {order_payload=}") + response: dict = await self._auth_and_post(path, payload=order_payload) + if response.get("result") is None: + self.logger.error(f"Error creating order, {response}") + return {} + self.logger.info( + f"{FN} Order created:" + f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}" + ) + return response.get("result", {}) + + def _get_order_with_validations( + self, + symbol: str, + order_type: GrvtOrderType, + side: GrvtOrderSide, + amount: Amount, + price: Num = None, + params: dict = {}, + ) -> GrvtOrder: + self._check_account_auth() + self._check_valid_symbol(symbol) + # Validate order fields + self._check_order_arguments(order_type, side, amount, price) + # create GrvtOrder object + order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60) + return get_grvt_order( + sub_account_id=self.get_trading_account_id(), + symbol=symbol, + order_type=order_type, + side=side, + amount=amount, + limit_price=price, + order_duration_secs=order_duration_secs, + params=params, + ) + + async def create_order( + self, + symbol: str, + order_type: GrvtOrderType, + side: GrvtOrderSide, + amount: Amount, + price: Num = None, + params={}, + ) -> dict: + """Ccxt compliant signature.""" + order = self._get_order_with_validations(symbol, order_type, side, amount, price, params) + return await self._create_grvt_order(order) + + async def create_limit_order( + self, + symbol: str, + side: GrvtOrderSide, + amount: Amount, + price: Num = None, + params={}, + ) -> dict: + return await self.create_order(symbol, "limit", side, amount, price, params) + + async def cancel_all_orders( + self, + params: dict = {}, + ) -> bool: + """ + Ccxt compliant signature BUT lacks symbol + Cancel all orders for a sub-account. + params: dictionary with parameters. Valid keys:
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Defaults to all.
+ """ + self._check_account_auth() + FN = f"{self._clsname} cancel_all_orders" + payload: dict = self._get_payload_cancel_all_orders(params) + path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS") + response: dict = await self._auth_and_post(path, payload=payload) + cancel_ack = response.get("result", {}).get("ack") + + if not cancel_ack: + self.logger.warning(f"{FN} failed to cancel orders: {response=}") + return False + self.logger.info(f"{FN} Cancelled {response=}") + return True + + async def cancel_order( + self, + id: str | None = None, + symbol: str | None = None, + params: dict = {}, + ) -> bool: + """ + Ccxt compliant signature + Cancel specific order for the account.
+ Private call requires authorization.
+ See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order) + for details.
. + + Args: + id (str): exchange assigned order ID
+ symbol (str): trading symbol
+ params: + * client_order_id (str): client assigned order ID
+ * time_to_live_ms (str): lifetime of cancel requiest in millisecs
+ Returns: + True if cancel request was acked by exchange. False otherwise.
+ """ + FN = f"{self._clsname} cancel_order" + self._check_account_auth() + # Prepare payload + payload: dict = { + "sub_account_id": str(self._trading_account_id), + } + if id: + payload["order_id"] = str(id) + elif "client_order_id" in params: + payload["client_order_id"] = str(params["client_order_id"]) + else: + raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id") + if "time_to_live_ms" in params: + payload["time_to_live_ms"] = str(params["time_to_live_ms"]) + + # Send cancel request + path = get_grvt_endpoint(self.env, "CANCEL_ORDER") + self.logger.info( + f"{FN} Send cancel {payload=} for trading_account_id={self._trading_account_id}" + ) + response: dict = await self._auth_and_post(path, payload) + cancel_ack = response.get("result", {}).get("ack") + + if not cancel_ack: + self.logger.warning(f"{FN} failed to cancel order: {response=}") + return False + self.logger.info(f"{FN} Cancelled {response=}") + return True + + async def set_derisk_mm_ratio(self, ratio: str) -> bool: + """ + + Set the Derisk to Maintenance marginb ratio for the account. + Private call requires authorization. + See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio) + for details. + + Args: + ratio (Amount): The new derisking market making ratio. + + Returns: + True if the request was acknowledged by the exchange. False otherwise. + """ + FN = f"{self._clsname} set_derisk_mm_ratio" + self._check_account_auth() + payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio)) + path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO") + self.logger.info( + f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}" + ) + response: dict = await self._auth_and_post(path, payload) + self.logger.info(f"{FN} Set derisk_mm_ratio {response=}") + return True + + async def fetch_open_orders( + self, + symbol: str | None = None, + since: int | None = None, + limit: int | None = None, + params: dict = {}, + ) -> list[dict]: + """ + Ccxt compliant signature + Fetch open orders for the account.
+ Private call requires authorization.
+ See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders) + for details.
. + + Args: + symbol: get orders for this symbol only.
+ since: ccxt-compliant argument, NOT SUPPORTED.
+ limit: ccxt-compliant argument, NOT SUPPORTED.
+ params: dictionary with parameters. Valid keys:
+ `kind` (str): instrument kind. Valid values are 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch orders + for all base currencies.
+ `quote` (str): quote currency. Defaults to all.
+ Returns: + a list of dictionaries, each dict represent an order.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_open_orders(symbol, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS") + response: dict = await self._auth_and_post(path, payload) + open_orders: list = response.get("result", []) + if symbol: + open_orders = [ + o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol + ] + return open_orders + + async def fetch_order( + self, + id: str | None = None, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature + Private call requires authorization.
+ See [Get Order](https://api-docs.grvt.io/trading_api/#get-order) + for details.
. + + Get Order status by either order_id or client_order_id + Args: + id: (str) order_id to fetch.
+ symbol: (str) NOT SUPPRTED.
+ params: dictionary with parameters. Valid keys:
+ `client_order_id` (int): client assigned order ID.
+ Returns: + dict with order details or {} if order was NOT found.
+ """ + FN = f"{self._clsname} fetch_order" + self._check_account_auth() + payload = { + "sub_account_id": str(self._trading_account_id), + } + if id: + payload["order_id"] = id + elif "client_order_id" in params: + payload["client_order_id"] = str(params["client_order_id"]) + else: + raise GrvtInvalidOrder(f"{FN} requires either order_id or params['client_order_id']") + path = get_grvt_endpoint(self.env, "GET_ORDER") + response: dict = await self._auth_and_post(path, payload) + return response + + async def fetch_order_history(self, params: dict = {}) -> dict: + """ + Ccxt compliant signature, HISTORICAL data.
+ Get Order history of orders by kind/base/quote.
+ Private call requires authorization.
+ See [Order History](https://api-docs.grvt.io/trading_api/#order-history) + for details.
+ Args: + params: dictionary with parameters. Valid keys:
+ `kind`: (str) - The kind filter to apply. Defaults to all kinds.
+ `base`: (str) - The base currency filter. Defaults to all base currencies.
+ `quote`: (str) - The quote currency filter. Defaults to all quote currencies.
+ `expiration`: (int) The expiration time in nanoseconds. Defaults to all.
+ `strike_price`: (str) The strike price to apply. Defaults to all strike prices.
+ `limit`: (int) The limit to query for. Defaults to 500; Max 1000.
+ `cursor`: (str) The cursor to use for pagination. If nil, return the first page.
+ Return: a dictionary with keys: + `total` : total number of account history snapshots.
+ `next` : cursor for the next page.
+ `result` : a list of dictionaries, each dict represent an order state.
. + """ + self._check_account_auth() + payload = self._get_payload_fetch_order_history(params) + path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY") + response: dict = await self._auth_and_post(path, payload) + return response + + async def get_account_summary( + self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account" + ) -> dict: + """ + Return: The account summary. + Private call requires authorization.
+ See [Account Summary](https://api-docs.grvt.io/trading_api/#account_summary) + for details.
+ Returns: dictionary with account data.
. + """ + FN = f"{self._clsname} get_account_summary {type=}" + self._check_account_auth() + payload = {} + if type == "sub-account": + path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY") + payload = {"sub_account_id": str(self._trading_account_id)} + elif type == "funding": + path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY") + elif type == "aggregated": + path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY") + else: + raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}") + + response: dict = await self._auth_and_post(path, payload=payload) + sub_account: dict = response.get("result", {}) + if not sub_account: + self.logger.info(f"{FN} No account summary for {path=} {payload=}") + return sub_account + + async def fetch_balance( + self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account" + ) -> dict: + """ + Ccxt compliant signature + Fetch balances for the account.
+ Private call requires authorization.
+ See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary) + for details.
. + + Args: + type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'. + Valid values: 'sub-account', 'funding', 'aggregated'. + + Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.
. + """ + account_summary: dict = await self.get_account_summary(type) + return self._get_balances_from_account_summary(account_summary) + + async def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict: + """ + HISTORICAL data.
+ Get account history.
+ Private call requires authorization.
+ See [Account History](https://api-docs.grvt.io/trading_api/#account-history) + for details.
. + + Args: + limit: maximum number of account snapshots per page to fetch.
+ params: dictionary with parameters. Valid keys:
+ `start_time` (int): fetch orders since this timestamp in nanoseconds.
+ `end_time` (int): fetch orders until this timestamp in nanoseconds.
+ `cursor` (str): cursor for the pagination. If cursor is present then we ignore + `start_time` and `end_time`.
+ Returns: + a dictionary with keys: + `total` : total number of account history snapshots.
+ `next` : cursor for the next page.
+ `result` : list of account history snapshots.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_account_history(limit, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY") + response: dict = await self._auth_and_post(path, payload=payload) + return response + + async def fetch_positions(self, symbols: list[str] = [], params={}) -> list[dict]: + """ + Ccxt compliant signature + Fetch positions for the account.
+ Private call requires authorization.
+ See [Positions](https://api-docs.grvt.io/trading_api/#positions) + for details.
. + + Args: + symbols: list(str) get positions for these symbols only.
+ + Returns: list of dictionaries, each dict represent a position.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_positions(symbols, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_POSITIONS") + response: dict = await self._auth_and_post(path, payload) + positions: list = response.get("result", []) + if symbols: + self.logger.info(f"fetch_positions filter positions by {symbols=}") + positions = [p for p in positions if p.get("instrument") in symbols] + return positions + + async def fetch_my_trades( + self, + symbol: str | None = None, + since: int | None = None, + limit: int | None = None, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature, HISTORICAL data.
+ Fetch past trades for the account.
+ Private call requires authorization.
+ See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history) + for details.
. + + Args: + symbol: get trades for this symbol only.
+ since: fetch trades since this timestamp in nanoseconds.
+ limit: maximum number of trades to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Default: 'USDT'.
+ + Returns: + a dictionary with keys: + `total` : total number of account history snapshots.
+ `next` : cursor for the next page.
+ `result` : a list of dictionaries, each dict represent a trade.
+ """ + self._check_account_auth() + # Prepare request payload + payload = self._get_payload_fetch_my_trades(symbol, since, limit, params) + # Post payload and parse the response + path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY") + response: dict = await self._auth_and_post(path, payload=payload) + if symbol: + # filter result by symbol + trades: list = response.get("result", []) + trades = [t for t in trades if t.get("instrument") == symbol] + response["result"] = trades + return response + + # **************** PUBLIC API CALLS + async def load_markets(self) -> dict | None: + self.logger.info("load_markets START") + instruments = await self.fetch_markets( + params={ + "kind": GrvtInstrumentKind.PERPETUAL, + } + ) + if instruments: + self.markets = {i.get("instrument"): i for i in instruments} + self.logger.info(f"load_markets: loaded {len(self.markets)} markets.") + else: + self.logger.warning("load_markets: No markets found.") + return self.markets + + async def fetch_markets( + self, + params: dict = {}, + ) -> list[dict]: + """ + ccxt-compliant signature + Retrieve the list of all instruments of matching kind, base and quote + supported by the exchange. + + Params: dict with keys:
+ `is_active` (bool) - defaults to True.
+ `limit` (int) - defaiults to 20.
+ `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
+ `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
+ `quote` (str): quote currency. Default: 'USDT'.
+ + Returns: list of dictionaries per instrument with keys:
+ `instrument`: symbol e.g. 'BTC_USDT_Perp'.
+ `instrument_hash`: hashed symbol for order signing e.g. '0x030501'.
+ `base`: base currency e.g. 'BTC'.
+ `quote`: quote currency e.g. 'USDT'.
+ `kind`: kind of instrument 'PERPETUAL'/'FUTURE'.
+ 'base_decimals': size multiplier for order signing.
+ `tick_size`: price tick size.
+ `min_size`: minimum order size.
+ """ + # Prepare payload + payload = self._get_payload_fetch_markets(params) + # Make the POST request to get all instruments + path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS") + response: dict = await self._auth_and_post(path, payload=payload) + return response.get("result", []) + + async def fetch_all_markets( + self, + is_active: bool | None = True, + ) -> list[dict]: + """ + Retrieve the list of all instruments supported by the exchange.
+ Params:
+ `is_active` (bool) - defaults to True.
. + + Returns: list of dictionaries per instrument. See fetch_markets().
+ """ + # Prepare payload + payload = {"is_active": is_active} + # Make the POST request to get all instruments + path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS") + response: dict = await self._auth_and_post(path, payload=payload) + # Extract and return the list of instruments + return response.get("result", []) + + async def fetch_market(self, symbol: str) -> dict: + """ + Retrieve the instrument object for a given symbol. + :param symbol: The symbol of the instrument. + """ + # Make the POST request to get all instruments + path = get_grvt_endpoint(self.env, "GET_INSTRUMENT") + response: dict = await self._auth_and_post(path, payload={"instrument": symbol}) + return response.get("result", []) + + async def fetch_ticker(self, symbol: str, params: dict = {}) -> dict: + """ + ccxt-compliant signature + Retrieve the ticker of a given symbol. + :param symbol: The instrument name. + :return: The ticker dictionary of the instrument. + """ + # {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp', + # 'mark_price': '59373870996065', 'index_price': '59395287961367', + # 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000', + # 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price': + # '59273700', 'best_ask_size': '21670', 'funding_rate_curr': 2544, 'funding_rate_avg': 0, + # 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000', + # 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500', + # 'sell_volume_q': '687640900', 'high_price': '343545000', 'low_price': '100000', + # 'open_price': '32554000000000', 'open_interest': '8174350000000', + # 'long_short_ratio': 1.0948905} + path = get_grvt_endpoint(self.env, "GET_TICKER") + response: dict = await self._auth_and_post(path, payload={"instrument": symbol}) + return response.get("result", {}) + + async def fetch_mini_ticker(self, symbol: str) -> dict: + """ + Retrieve the mini-ticker of a given symbol. + :param symbol: The instrument name. + :return: The mini-ticker dictionary of the instrument. + """ + # {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp', + # 'mark_price': '59373870996065', 'index_price': '59395287961367', + # 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000', + # 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price': + # '59273700000000', 'best_ask_size': '21678000000'} + path = get_grvt_endpoint(self.env, "GET_MINI_TICKER") + response: dict = await self._auth_and_post(path, payload={"instrument": symbol}) + return response.get("result", {}) + + async def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict: + """ + ccxt-compliant signature + Retrieve the order book of a given symbol. + :param symbol: The instrument name. + :return: The order book dictionary of the instrument. + """ + # {'event_time': '0', 'instrument': 'BTC_USDT_Perp', + # 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...] + # 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...] + payload = {"instrument": symbol, "aggregate": 1} + if limit: + payload["depth"] = limit + path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK") + response: dict = await self._auth_and_post(path, payload=payload) + if self.is_order_book_ccxt_format(): + # Convert to ccxt format + return self.convert_grvt_ob_to_ccxt(response.get("result", {})) + return response.get("result", {}) + + async def fetch_recent_trades( + self, + symbol: str, + limit: int | None = None, + ) -> list: + """ + Retrieve the recent trades a given instrument.
+ :param instrument: The instrument name. + :return: The order book dictionary of the instrument. + """ + # List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp', + # 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000', + # 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0, + # 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'} + payload: dict[str, str | int] = {"instrument": symbol} + if limit: + payload["limit"] = limit + path = get_grvt_endpoint(self.env, "GET_TRADES") + response: dict = await self._auth_and_post(path, payload=payload) + return response.get("result", []) + + async def fetch_trades( + self, + symbol: str, + since: int | None = None, + limit: int = 10, + params: dict = {}, + ) -> dict: + """ + Ccxt-compliant signature, HISTORICAL data.
+ Retrieve trade history of a given instrument. + :param symbol: The instrument name. + :return: dict with field 'result' containing a list of trades. + """ + # List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp', + # 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000', + # 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0, + # 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'} + payload: dict = self._get_payload_fetch_trades( + symbol, + since=since, + limit=limit, + params=params, + ) + path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY") + response: dict = await self._auth_and_post(path, payload=payload) + return response + + async def fetch_funding_rate_history( + self, + symbol: str, + since: int = 0, + limit: int = 1_000, + params: dict = {}, + ) -> dict: + """ + ccxt-compliant signature, HISTORICAL data.
+ Retrieve the funding rates history of a given instrument.
+ Args: + symbol (str): The instrument name.
+ since (int): fetch trades since this timestamp in nanoseconds.
+ limit: int - maximum number of trades to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `end_time` (int): end time in nanoseconds.
+ Returns: + dict with field 'result' containing list of dictionaries repesenting funding rate + at a point in time with fields:
+ `instrument` (str): instrument name.
+ 'funding_rate' (float): funding rate.
+ 'funding_time' (int): funding time in nanoseconds.
+ 'mark_price' (float): mark price.
. + """ + payload: dict[str, str | int] = {"instrument": symbol} + if params.get("cursor"): + payload["cursor"] = params["cursor"] + else: + if since: + payload["start_time"] = str(since) + if params.get("end_time"): + payload["end_time"] = str(params["end_time"]) + if limit: + payload["limit"] = int(limit) + path: str = get_grvt_endpoint(self.env, "GET_FUNDING") + response: dict = await self._auth_and_post(path, payload=payload) + return response + + async def fetch_ohlcv( + self, + symbol: str, + timeframe: str = "1m", + since: int = 0, + limit: int = 10, + params={}, + ) -> dict: + """ + ccxt-compliant signature, HISTORICAL data.
+ Retrieve the ohlc history of a given instrument.
+ Args: + symbol: The instrument name.
+ timeframe: The timeframe of the ohlc. + See `ccxt_interval_to_grvt_candlestick_interval`.
+ since: fetch ohlc since this timestamp in nanoseconds.
+ limit: maximum number of ohlc to fetch.
+ params: dictionary with parameters. Valid keys:
+ `cursor` (str): cursor for the pagination. + If cursor is present then we ignore other filters.
+ `end_time` (int): end time in nanoseconds.
+ `candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.
+ Returns: + dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:
+ `instrument` - instrument name.
+ `open_time` - start of interval in nanoseconds.
+ `close_time` - end of interval in nanoseconds.
+ `open` - opening price.
+ `close` - closing price.
+ `high` - highest price.
+ `low` - lowest price.
+ `volume_u` - volume in units.
+ `volume_q` - volume in quote(USDT).
+ `trades` - number of trades.
. + """ + FN: str = f"{self._clsname} fetch_ohlcv" + payload: dict = self._get_payload_fetch_ohlcv(symbol, timeframe, since, limit, params) + self.logger.info(f"{FN} {payload=}") + path: str = get_grvt_endpoint(self.env, "GET_CANDLESTICK") + response: dict = await self._auth_and_post(path, payload=payload) + return response + + # Vault Management APIs + async def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict: + payload: dict = self._get_fetch_vault_manager_investor_history_payload( + vault_id=self.get_trading_account_id(), + only_own_investments=only_own_investments, # Default to False to fetch all investments + ) + path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY") + return await self._auth_and_post(path, payload=payload) + + async def fetch_vault_redemption_queue(self): + payload: dict = self._get_fetch_vault_redemption_queue_payload( + vault_id=self.get_trading_account_id() + ) + path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE") + return await self._auth_and_post(path, payload=payload) \ No newline at end of file diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_test_utils.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_test_utils.py new file mode 100644 index 0000000..981f49f --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_test_utils.py @@ -0,0 +1,75 @@ +import logging +from collections.abc import Callable + +from .grvt_ccxt import GrvtCcxt +from .grvt_ccxt_env import get_all_grvt_endpoints +from .grvt_ccxt_pro import GrvtCcxtPro + + +def default_check(return_value: dict) -> str: + if not isinstance(return_value, list | dict): + return "return_value is not a list or dict" + if not return_value: + return "return_value is empty" + return "OK" + + +def validate_return_values(api: GrvtCcxt | GrvtCcxtPro, result_filename: str) -> None: + logging.info("validate_return_values: START") + endpoint_check_map: dict[str, Callable] = { + "GRAPHQL": default_check, + "AUTH": default_check, + "CREATE_ORDER": default_check, + "CANCEL_ALL_ORDERS": default_check, + "CANCEL_ORDER": default_check, + "GET_OPEN_ORDERS": default_check, + "GET_ACCOUNT_SUMMARY": default_check, + "GET_FUNDING_ACCOUNT_SUMMARY": default_check, + "GET_AGGREGATED_ACCOUNT_SUMMARY": default_check, + "GET_ACCOUNT_HISTORY": default_check, + "GET_POSITIONS": default_check, + "GET_ORDER": default_check, + "GET_ORDER_HISTORY": default_check, + "GET_FILL_HISTORY": default_check, + "GET_ALL_INSTRUMENTS": default_check, + "GET_INSTRUMENTS": default_check, + "GET_INSTRUMENT": default_check, + "GET_TICKER": default_check, + "GET_MINI_TICKER": default_check, + "GET_ORDER_BOOK": default_check, + "GET_TRADES": default_check, + "GET_TRADE_HISTORY": default_check, + "GET_FUNDING": default_check, + "GET_CANDLESTICK": default_check, + } + all_endpoints = get_all_grvt_endpoints(api.env) + end_point_status = {} + for short_name, endpoint in all_endpoints.items(): + if not api.was_path_called(endpoint): + logging.info(f"validate_return_values: {short_name=}, {endpoint=}, not called") + end_point_status[short_name] = [endpoint, "not called"] + else: + return_value = api.get_endpoint_return_value(endpoint) + if short_name in endpoint_check_map: + check_function = endpoint_check_map.get(short_name) + if not check_function or not callable(check_function): + logging.error( + f"validate_return_values: {short_name=} " + f"not found in {endpoint_check_map.keys()=}" + ) + continue + check_result = check_function(return_value) + logging.info( + f"validate_return_values: {short_name=}, {endpoint=}, {check_result=}" + ) + end_point_status[short_name] = [endpoint, check_result] + else: + logging.error( + f"validate_return_values: NO {short_name=} in {endpoint_check_map.keys()=}" + ) + end_point_status[short_name] = [endpoint, "no check"] + with open(result_filename, "w") as file_handle: + file_handle.write("NAME, URL, STATUS\n") + for short_name, status in end_point_status.items(): + file_handle.write(f"{short_name}, {status[0]}, {status[1]}\n") + logging.info("validate_return_values: END") diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_types.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_types.py new file mode 100644 index 0000000..342b0fd --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_types.py @@ -0,0 +1,81 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 + +from decimal import Decimal +from enum import Enum +from typing import Literal + +Num = None | str | float | int | Decimal +Amount = Decimal | int | float | str +GrvtOrderSide = Literal["buy", "sell"] +GrvtOrderType = Literal["limit", "market"] + +DURATION_SECOND_IN_NSEC = 1_000_000_000 +PRICE_MULTIPLIER = 1_000_000_000 +BTC_ETH_SIZE_MULTIPLIER = 1_000_000_000 + + +class GrvtInvalidOrder(Exception): + pass + + +class CandlestickInterval(Enum): + CI_1_M = "CI_1_M" + CI_3_M = "CI_3_M" + CI_5_M = "CI_5_M" + CI_15_M = "CI_15_M" + CI_30_M = "CI_30_M" + CI_1_H = "CI_1_H" + CI_2_H = "CI_2_H" + CI_4_H = "CI_4_H" + CI_6_H = "CI_6_H" + CI_8_H = "CI_8_H" + CI_12_H = "CI_12_H" + CI_1_D = "CI_1_D" + CI_3_D = "CI_3_D" + CI_5_D = "CI_5_D" + CI_1_W = "CI_1_W" + CI_2_W = "CI_2_W" + CI_3_W = "CI_3_W" + CI_4_W = "CI_4_W" + + +ccxt_interval_to_grvt_candlestick_interval = { + "1m": CandlestickInterval.CI_1_M, + "3m": CandlestickInterval.CI_3_M, + "5m": CandlestickInterval.CI_5_M, + "15m": CandlestickInterval.CI_15_M, + "30m": CandlestickInterval.CI_30_M, + "1h": CandlestickInterval.CI_1_H, + "2h": CandlestickInterval.CI_2_H, + "4h": CandlestickInterval.CI_4_H, + "6h": CandlestickInterval.CI_6_H, + "8h": CandlestickInterval.CI_8_H, + "12h": CandlestickInterval.CI_12_H, + "1d": CandlestickInterval.CI_1_D, + "3d": CandlestickInterval.CI_3_D, + "5d": CandlestickInterval.CI_5_D, + "1w": CandlestickInterval.CI_1_W, + "2w": CandlestickInterval.CI_2_W, + "3w": CandlestickInterval.CI_3_W, + "4w": CandlestickInterval.CI_4_W, +} + + +class CandlestickType(Enum): + TRADE = "TRADE" + MARK = "MARK" + INDEX = "INDEX" + MID = "MID" + + +class GrvtInstrumentKind(Enum): + PERPETUAL = "PERPETUAL" + FUTURE = "FUTURE" + CALL = "CALL" + PUT = "PUT" diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_utils.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_utils.py new file mode 100644 index 0000000..42d5014 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_utils.py @@ -0,0 +1,544 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 + +import json +import logging +import random +import time +from dataclasses import dataclass +from datetime import datetime +from decimal import Decimal +from enum import Enum +from http.cookies import SimpleCookie +from typing import Any + +import aiohttp +import requests +from eth_account import Account +from eth_account.messages import encode_typed_data, SignableMessage + +from .grvt_ccxt_env import CHAIN_IDS, GrvtEnv +from .grvt_ccxt_types import ( + BTC_ETH_SIZE_MULTIPLIER, + DURATION_SECOND_IN_NSEC, + Amount, + GrvtOrderSide, + GrvtOrderType, + Num, +) + + +def rand_uint32(): + return random.randint(0, 2**32 - 1) + + +class TimeInForce(Enum): + """ + | | Must Fill All | Can Fill Partial | + | - | - | - | + | Must Fill Immediately | FOK | IOC | + | Can Fill Till Time | AON | GTC |. + + """ + + # GTT - Remains open until it is cancelled, or expired + GOOD_TILL_TIME = "GOOD_TILL_TIME" + # AON - Either fill the whole order or none of it (Block Trades Only) + ALL_OR_NONE = "ALL_OR_NONE" + # IOC - Fill the order as much as possible, when hitting the orderbook. Then cancel it + IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL" + # FOK - Both AoN and IoC. Either fill the full order when hitting the orderbook, or cancel it + FILL_OR_KILL = "FILL_OR_KILL" + + +class SignTimeInForce(Enum): + GOOD_TILL_TIME = 1 + ALL_OR_NONE = 2 + IMMEDIATE_OR_CANCEL = 3 + FILL_OR_KILL = 4 + + +TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = { + TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME, + TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE, + TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL, + TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL, +} + + +def get_EIP712_domain_data(env: GrvtEnv) -> dict[str, str | int]: + # DO NOT MODIFY THESE VALUES ############## + return { + "name": "GRVT Exchange", + "version": "0", + "chainId": CHAIN_IDS[env.value], + } + + +def get_cookie_with_expiration( + path: str, api_key: str | None +) -> dict[str, str | float | None] | None: + """ + Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token. + :return: The session cookie. + """ + FN = f"get_cookie_with_expiration {path=}" + if api_key: + data = {} + try: + data = {"api_key": api_key} + session = requests.Session() + return_value = session.post( + path, + json=data, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + if return_value.ok: + cookie = SimpleCookie() + cookie.load(return_value.headers.get("Set-Cookie", "")) + cookie_value: str = cookie["gravity"].value + cookie_expiry: datetime = datetime.strptime( + cookie["gravity"]["expires"], + "%a, %d %b %Y %H:%M:%S %Z", + ) + grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "") + logging.info( + f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}" + ) + return { + "gravity": cookie_value, + "expires": cookie_expiry.timestamp(), + "X-Grvt-Account-Id": grvt_account_id, + } + logging.warning(f"{FN} Invalid return_value {data=} {path=} {return_value=}") + return None + except Exception as e: + logging.error(f"{FN} Error getting cookie: {e}") + return None + else: + return None + + +async def get_cookie_with_expiration_async( + path: str, api_key: str | None +) -> dict[str, str | float | None] | None: + """ + Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token. + :return: The session cookie. + """ + FN = f"get_cookie_with_expiration_async {path=}" + if api_key: + data = {} + try: + data = {"api_key": api_key} + logging.info(f"{FN} ask for cookie {path=} {data=}") + async with aiohttp.ClientSession() as session: + async with session.post(url=path, json=data, timeout=5) as return_value: + logging.info(f"{FN} {return_value=}") + if return_value.ok: + cookie = SimpleCookie() + cookie.load(return_value.headers.get("Set-Cookie", "")) + cookie_value: str = cookie["gravity"].value + cookie_expiry: datetime = datetime.strptime( + cookie["gravity"]["expires"], + "%a, %d %b %Y %H:%M:%S %Z", + ) + grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "") + logging.info( + f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}" + ) + return { + "gravity": cookie_value, + "expires": cookie_expiry.timestamp(), + "X-Grvt-Account-Id": grvt_account_id, + } + except Exception as e: + logging.error(f"{FN} Error getting cookie: {e}") + return None + else: + return None + + +class GrvtKind(Enum): + PERPETUAL = 1 + FUTURE = 2 + CALL = 3 + PUT = 4 + SPOT = 5 + + +class GrvtCurrency(Enum): + USD = 1 + USDC = 2 + USDT = 3 + ETH = 4 + BTC = 5 + + +def hexlify(data: bytes) -> str: + """Convert a byte array to a hex string with a 0x prefix.""" + return f"0x{data.hex()}" + + +class EnumEncoder(json.JSONEncoder): + def default(self, o): + """ + Custom JSON encoder for Enum types. + :param obj: Object to serialize. + :return: Serialized object. + """ + if isinstance(o, Enum): + return o.value + return super().default(o) + + +def get_kuq_from_symbol(symbol: str) -> tuple[str, str, str]: + parts = symbol.split("_") + if len(parts) == 3: + underlying, quote, kind = parts + if kind == "Perp": + kind = "PERPETUAL" + else: + raise ValueError(f"Invalid {symbol=} {kind=}") + elif len(parts) == 4: + underlying, quote, kind, time_str = parts + if kind == "Fut": + kind = "FUTURE" + else: + raise ValueError(f"Invalid {symbol=} {kind=}") + elif len(parts) == 5: + underlying, quote, kind, time_str, strike_price = parts + if kind in {"Call", "Put"}: + kind = kind.upper() + else: + raise ValueError(f"Invalid {symbol=} {kind=}") + else: + raise ValueError(f"Invalid {symbol=}") + return kind, underlying, quote + + +# Custom types +EIP712_ORDER_MESSAGE_TYPE = { + "Order": [ + {"name": "subAccountID", "type": "uint64"}, + {"name": "isMarket", "type": "bool"}, + {"name": "timeInForce", "type": "uint8"}, + {"name": "postOnly", "type": "bool"}, + {"name": "reduceOnly", "type": "bool"}, + {"name": "legs", "type": "OrderLeg[]"}, + {"name": "nonce", "type": "uint32"}, + {"name": "expiration", "type": "int64"}, + ], + "OrderLeg": [ + {"name": "assetID", "type": "uint256"}, + {"name": "contractSize", "type": "uint64"}, + {"name": "limitPrice", "type": "uint64"}, + {"name": "isBuyingContract", "type": "bool"}, + ], +} + + +@dataclass +class GrvtSignature: + # The address (public key) of the wallet signing the payload + signer: str + r: str + s: str + v: int + # Timestamp after which this signature expires, expressed in unix nanoseconds. + # Must be capped at 30 days + expiration: str + """ + Users can randomly generate this value, used as a signature deconflicting key. + ie. You can send the same exact instruction twice with different nonces. + When the same nonce is used, the same payload will generate the same signature. + Our system will consider the payload a duplicate, and ignore it. + """ + nonce: int + + +@dataclass +class OrderMetadata: + """ + Metadata fields are used to support Backend only operations. + Hence, fields in here are never signed, and is never transmitted to the smart contract. + """ + + """ + `client_order_id`: A unique identifier of an active order, specified by the client + This is used to identify the order in the client's system + This value must be unique for all active orders in a subaccount, + otehrwise amendment / cancellation will not work as expected + Gravity UI will generate a random clientOrderID for each order in the range [0, 2^63 - 1] + To prevent any conflicts, client machines should generate a random clientOrderID + in the range [2^63, 2^64 - 1]. + When GRVT Backend receives an order with duplicate `client_order_id`, it will reject the order + with rejectReason set to duplicate `client_order_id`. + """ + client_order_id: str + # [Filled by GRVT Backend] Time at which the order was received by GRVT in unix nanoseconds + create_time: str | None = None + + +@dataclass +class GrvtOrderLeg: + # The instrument to trade in this leg + instrument: str + # The total number of contracts to trade in this leg, expressed in base currency units. + size: Decimal + # Specifies if the order leg is a buy or sell + is_buying_asset: bool + """ + The limit price of the order leg, expressed in `9` decimals. + This is the number of quote currency units to pay/receive for this leg. + This should be `null/0` if the order is a market order + """ + limit_price: Decimal + + +@dataclass +class GrvtOrder: + """ + Order is a typed payload used throughout the GRVT platform to express all orders. + GRVT orders are capable of expressing both single-legged, and multi-legged orders by default. + All fields in the Order payload (except `id`, `metadata`, and `state`) are trustlessly enforced + on our Hyperchain. + This minimizes the amount of trust users have to offer to GRVT. + """ + + # The subaccount initiating the order + sub_account_id: str + # Supported time_in_force : GTT, IOC, FOK:
    + time_in_force: TimeInForce + legs: list[GrvtOrderLeg] + # The signature approving this order + signature: GrvtSignature + # Order Metadata, ignored by the smart contract, and unsigned by the client + metadata: OrderMetadata + # is_market: If the order is a market order + is_market: bool + post_only: bool = False + # If True, Order must reduce the position size, or be cancelled + reduce_only: bool = False + + +def get_signable_message( + order: GrvtOrder, env: GrvtEnv, instruments: dict[str, dict] +) -> bytes | None: + FN = f"get_signable_message {order=}" + size_multiplier = BTC_ETH_SIZE_MULTIPLIER + PRICE_MULTIPLIER = 1_000_000_000 + legs = [] + for leg in order.legs: + instrument = instruments.get(leg.instrument) + if not instrument or not isinstance(instrument, dict): + logging.error(f"{FN}: {leg.instrument=} not found in {instruments=}") + return None + if "base_decimals" not in instrument: + logging.error(f"{FN}: no 'base_decimals' in {instrument=}") + return None + size_multiplier = 10 ** instrument["base_decimals"] + if "instrument_hash" not in instrument: + logging.error(f"{FN}: no 'instrument_hash' in {instrument=}") + return None + legs.append( + { + "assetID": instrument["instrument_hash"], + "contractSize": int(Decimal(leg.size) * Decimal(size_multiplier)), + "limitPrice": int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER)), + "isBuyingContract": leg.is_buying_asset, + } + ) + message_data = { + "subAccountID": order.sub_account_id, + "isMarket": order.is_market or False, + "timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value, + "postOnly": order.post_only or False, + "reduceOnly": order.reduce_only or False, + "legs": legs, + "nonce": order.signature.nonce, + "expiration": order.signature.expiration, + } + domain_data: dict[str, str | int]= get_EIP712_domain_data(env) + logging.info(f"{FN} {domain_data=}\n{EIP712_ORDER_MESSAGE_TYPE=}\n{message_data=}") + return encode_typed_data(domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data) + + +def get_order_payload( + order: GrvtOrder, private_key: str, env: GrvtEnv, instruments: dict[str, dict] +) -> dict: + signable_message = get_signable_message(order, env, instruments) + if signable_message is None: + raise ValueError("Failed to create signable message") + signed_message = Account.sign_message(signable_message, private_key) + order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex() + order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex() + order.signature.v = signed_message.v + order.signature.signer = Account.from_key(private_key).address + + return { + "order": { + "sub_account_id": str(order.sub_account_id), + "is_market": order.is_market, + "time_in_force": order.time_in_force.name, + "post_only": order.post_only, + "reduce_only": order.reduce_only, + "legs": [ + { + "instrument": leg.instrument, + "size": str(leg.size), + "limit_price": str(leg.limit_price), + "is_buying_asset": bool(leg.is_buying_asset), + } + for leg in order.legs + ], + "signature": { + "r": order.signature.r, + "s": order.signature.s, + "v": order.signature.v, + "expiration": order.signature.expiration, + "nonce": order.signature.nonce, + "signer": order.signature.signer, + }, + "metadata": { + "client_order_id": order.metadata.client_order_id, + }, + } + } + + +def get_order_rpc_payload( + order: GrvtOrder, + private_key: str, + env: GrvtEnv, + instruments: dict[str, dict], + version: str = "v1", +) -> dict: + order_payload = get_order_payload(order, private_key, env, instruments) + return { + "jsonrpc": "2.0", + "method": f"{version}/create_order", + "params": order_payload, + } + + +def get_grvt_order( + sub_account_id: str, + symbol: str, + order_type: GrvtOrderType, + side: GrvtOrderSide, + amount: Amount, + limit_price: Num, + order_duration_secs: float = 5 * 60, + params: dict = {}, +) -> GrvtOrder: + """ + Creates an order for a specified symbol with the given limit price and size. + + Args: + symbol . + limit_price (int): The limit price for the order. + size (float): The size of the order. + is_buying_asset(bool) : Buy or Sell. + + Returns: + Order: The created perpetual order. + """ + limit_price = limit_price or 0 + is_buying_asset = side == "buy" + is_market = order_type == "market" + leg = GrvtOrderLeg( + instrument=symbol, + size=round(Decimal(amount), 9), + is_buying_asset=is_buying_asset, + limit_price=round(Decimal(limit_price), 9), + ) + + # create an expiry time + time_in_force = TimeInForce.GOOD_TILL_TIME + if "time_in_force" in params: + time_in_force = TimeInForce[params["time_in_force"]] + post_only: bool = False + if "post_only" in params: + post_only = params["post_only"] + reduce_only: bool = False + if "reduce_only" in params: + reduce_only = params["reduce_only"] + expiry_ns: int = 0 + if order_duration_secs: + expiry_ns = time.time_ns() + int(order_duration_secs * DURATION_SECOND_IN_NSEC) + if "client_order_id" in params: + client_order_id = int(params["client_order_id"]) + else: + client_order_id = rand_uint32() + signature = GrvtSignature( + signer="", + r="", + s="", + v=0, + expiration=str(expiry_ns), + nonce=rand_uint32(), + ) + metadata = OrderMetadata(client_order_id=str(client_order_id)) + return GrvtOrder( + sub_account_id=sub_account_id, + time_in_force=time_in_force, + legs=[leg], + signature=signature, + metadata=metadata, + is_market=is_market, + post_only=post_only, + reduce_only=reduce_only, + ) + +def sign_derisk_mm_ratio_request( + env: GrvtEnv, sub_account_id: int, ratio: str, private_key_hex: str +): + """ + Generate a signature for setting the derisk to maintenance margin ratio. + + :param sub_account_id: The sub-account ID to set the ratio for. + :param ratio: The derisk to maintenance margin ratio as a string (e.g., "2.0"). + :param private_key_hex: The private key in hexadecimal format. + :return: A dictionary containing the signature for the payload. + """ + derisk_ratio_int = int(Decimal(ratio) * 1_000_000) + expiration_ns = int((time.time() + 86400) * 1_000_000_000) + nonce = random.randint(1, 2**32 - 1) + + domain_data = get_EIP712_domain_data(env) + + types = { + "SetDeriskToMaintenanceMarginRatio": [ + {"name": "subAccountID", "type": "uint64"}, + {"name": "deriskToMaintenanceMarginRatio", "type": "uint32"}, + {"name": "nonce", "type": "uint32"}, + {"name": "expiration", "type": "int64"}, + ] + } + + signature_payload = { + "subAccountID": sub_account_id, + "deriskToMaintenanceMarginRatio": derisk_ratio_int, + "nonce": nonce, + "expiration": expiration_ns, + } + + message = encode_typed_data(domain_data, types, signature_payload) + signed = Account.sign_message(message, private_key_hex) + signer = Account.from_key(private_key_hex) + + return { + "signer": signer.address.lower(), + "r": hex(signed.r), + "s": hex(signed.s), + "v": signed.v, + "expiration": str(expiration_ns), + "nonce": nonce, + } \ No newline at end of file diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_ws.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_ws.py new file mode 100644 index 0000000..132b921 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_ccxt_ws.py @@ -0,0 +1,785 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 + +import asyncio +import json +import logging +import traceback +from asyncio.events import AbstractEventLoop +from collections.abc import Callable +from decimal import Decimal + +import websockets + +# import requests +# from env import ENDPOINTS +from .grvt_ccxt_env import ( + GRVT_WS_STREAMS, + GrvtEnv, + GrvtWSEndpointType, + get_grvt_ws_endpoint, + is_trading_ws_endpoint, +) +from .grvt_ccxt_pro import GrvtCcxtPro +from .grvt_ccxt_types import ( + GrvtInvalidOrder, + GrvtOrderSide, + GrvtOrderType, + Num, +) +from .grvt_ccxt_utils import get_order_rpc_payload + +WS_READ_TIMEOUT = 5 + + +class GrvtCcxtWS(GrvtCcxtPro): + """ + GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode. + + Args: + env: GrvtCcxtPro (DEV, TESTNET, PROD) + parameters: dict with trading_account_id, private_key, api_key etc + + Examples: + >>> from grvt_api_pro import GrvtCcxtPro + >>> from grvt_env import GrvtEnv + >>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET) + >>> await grvt.fetch_markets() + """ + + def __init__( + self, + env: GrvtEnv, + loop: AbstractEventLoop, + logger: logging.Logger | None = None, + parameters: dict = {}, + ): + """Initialize the GrvtCcxt instance.""" + super().__init__(env, logger, parameters) + self._loop = loop + self._clsname: str = type(self).__name__ + self.api_ws_version = parameters.get("api_ws_version", "v1") + self.force_reconnect_flag: bool = False + self.ws: dict[GrvtWSEndpointType, websockets.WebSocketClientProtocol | None] = {} + self.callbacks: dict[GrvtWSEndpointType, dict[str, dict[str, Callable]]] = {} + self.subscribed_streams: dict[GrvtWSEndpointType, dict] = {} + self.api_url: dict[GrvtWSEndpointType, str] = {} + self._last_message: dict[str, dict] = {} + self._request_id = 0 + self.endpoint_types = [ + GrvtWSEndpointType.MARKET_DATA, + GrvtWSEndpointType.TRADE_DATA, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + ] + # Initialize dictionaries for each endpoint type + for grvt_endpoint_type in self.endpoint_types: + self.api_url[grvt_endpoint_type] = get_grvt_ws_endpoint( + self.env.value, grvt_endpoint_type + ) + self.callbacks[grvt_endpoint_type] = {} + self.subscribed_streams[grvt_endpoint_type] = {} + self.ws[grvt_endpoint_type] = None + self._loop.create_task(self._read_messages(grvt_endpoint_type)) + self.logger.info(f"{self._clsname} initialized {self.api_url=}") + self.logger.info(f"{self._clsname} initialized {self.ws=}") + + def __repr__(self) -> str: + return f"{self._clsname} {self.env=} {self.api_ws_version=}" + + async def __aexit__(self): + for grvt_endpoint_type in self.endpoint_types: + await self._close_connection(grvt_endpoint_type) + + def force_reconnect(self) -> None: + self.force_reconnect_flag = True + + async def initialize(self): + """ + Prepares the GrvtCcxtPro instance and connects to WS server. + """ + await self.load_markets() + await self.refresh_cookie() + self._loop.create_task(self.connect_all_channels()) + + def is_connection_open(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool: + return ( + self.ws[grvt_endpoint_type] is not None and self.ws[grvt_endpoint_type].open + ) + + def is_endpoint_connected(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool: + """ + For MARKET_DATA returns True if connection is open. + for TRADE_DATA returns True if one of the following is true: + 1. No cookie - this means this is public connection and we can't connect to TRADE_DATA + 2. Connection to TRADE_DATA is open + """ + if grvt_endpoint_type in [ + GrvtWSEndpointType.MARKET_DATA, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + ]: + return self.is_connection_open(grvt_endpoint_type) + if grvt_endpoint_type in [ + GrvtWSEndpointType.TRADE_DATA, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + ]: + return bool(not self._cookie or self.is_connection_open(grvt_endpoint_type)) + raise ValueError(f"Unknown endpoint type {grvt_endpoint_type}") + + def are_endpoints_connected( + self, grvt_endpoint_types: list[GrvtWSEndpointType] + ) -> bool: + return all( + self.is_endpoint_connected(endpoint) for endpoint in grvt_endpoint_types + ) + + async def connect_all_channels(self) -> None: + """ + Connects to all channels that are possible to connect. + If cookie is NOT available, it will NOT connect to GrvtWSEndpointType.TRADE_DATA + For trading connection: run this method after cookie is available. + """ + FN = "connect_all_channels" + while True: + try: + for end_point_type in self.endpoint_types: + if ( + not self.is_endpoint_connected(end_point_type) + or self.force_reconnect_flag + ): + await self._reconnect(end_point_type) + all_are_connected = self.are_endpoints_connected(self.endpoint_types) + self.logger.info( + f"{FN} Connection status: {all_are_connected=} {self.force_reconnect_flag=}" + ) + self.force_reconnect_flag = False + except Exception as e: + self.logger.exception(f"{FN} {e=}") + finally: + await asyncio.sleep(5) + + async def connect_channel(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool: + FN = f"{self._clsname} connect_channel {grvt_endpoint_type}" + try: + if self.is_endpoint_connected(grvt_endpoint_type): + self.logger.info(f"{FN} Already connected") + return True + self.subscribed_streams[grvt_endpoint_type] = {} + extra_headers = {} + if self._cookie: + extra_headers = {"Cookie": f"gravity={self._cookie['gravity']}"} + if self._cookie["X-Grvt-Account-Id"]: + extra_headers.update( + {"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]} + ) + if grvt_endpoint_type in [ + GrvtWSEndpointType.TRADE_DATA, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + ]: + if self._cookie: + self.ws[grvt_endpoint_type] = await websockets.connect( + uri=self.api_url[grvt_endpoint_type], + extra_headers=extra_headers, + logger=self.logger, + open_timeout=5, + ) + self.logger.info( + f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}" + ) + else: + self.logger.info(f"{FN} Waiting for cookie.") + elif grvt_endpoint_type in [ + GrvtWSEndpointType.MARKET_DATA, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + ]: + self.ws[grvt_endpoint_type] = await websockets.connect( + uri=self.api_url[grvt_endpoint_type], + extra_headers=extra_headers, + logger=self.logger, + open_timeout=5, + ) + self.logger.info(f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}") + except ( + websockets.exceptions.ConnectionClosedOK, + websockets.exceptions.ConnectionClosed, + ) as e: + self.logger.info(f"{FN} connection already closed:{e}") + self.ws[grvt_endpoint_type] = None + except Exception as e: + self.logger.warning(f"{FN} error:{e} traceback:{traceback.format_exc()}") + self.ws[grvt_endpoint_type] = None + # return True if connection successful + return self.is_endpoint_connected(grvt_endpoint_type) + + async def _close_connection(self, grvt_endpoint_type: GrvtWSEndpointType): + try: + if self.ws[grvt_endpoint_type]: + self.logger.info(f"{self._clsname} Closing connection...") + await self.ws[grvt_endpoint_type].close() + self.subscribed_streams[grvt_endpoint_type] = {} + self.logger.info(f"{self._clsname} Connection closed") + else: + self.logger.info(f"{self._clsname} No connection to close") + except Exception: + self.logger.exception( + f"{self._clsname} Error when closing connection {traceback.format_exc()}" + ) + + async def _reconnect(self, grvt_endpoint_type: GrvtWSEndpointType): + FN = f"{self._clsname} _reconnect {grvt_endpoint_type=}" + try: + self.logger.info(f"{FN} STARTS") + await self._close_connection(grvt_endpoint_type) + success: bool = await self.connect_channel(grvt_endpoint_type) + if success: + await self._resubscribe(grvt_endpoint_type) + except Exception: + self.logger.exception(f"{FN} failed {traceback.format_exc()}") + + async def _resubscribe(self, grvt_endpoint_type: GrvtWSEndpointType): + if self.is_connection_open(grvt_endpoint_type): + for versioned_stream in self.callbacks[grvt_endpoint_type]: + for selector in self.callbacks[grvt_endpoint_type][versioned_stream]: + self.logger.info( + f"{self._clsname} _resubscribe {grvt_endpoint_type=}" + f" {versioned_stream=}/{selector=}" + ) + await self._subscribe_to_stream( + grvt_endpoint_type, versioned_stream, selector + ) + else: + self.logger.warning(f"{self._clsname} _resubscribe - No connection.") + + # **************** PUBLIC API CALLS + def is_stream_subscribed( + self, grvt_endpoint_type: GrvtWSEndpointType, stream: str + ) -> bool: + versioned_stream = self.get_versioned_stream(stream) + return self.subscribed_streams.get(grvt_endpoint_type, {}).get( + versioned_stream, False + ) + + def _check_susbcribed_stream( + self, grvt_endpoint_type: GrvtWSEndpointType, message: dict + ) -> None: + stream_subscribed: str = "" + if "stream" in message: + stream_subscribed = message["stream"] + elif "result" in message and "stream" in message["result"]: + stream_subscribed = message.get("result", {}).get("stream", "") + if stream_subscribed: + if not self.subscribed_streams[grvt_endpoint_type].get(stream_subscribed): + self.logger.info( + f"{self._clsname} subscribed to stream:{stream_subscribed}" + ) + self.subscribed_streams[grvt_endpoint_type][stream_subscribed] = True + + async def _read_messages(self, grvt_endpoint_type: GrvtWSEndpointType): + FN = f"{self._clsname} _read_messages {grvt_endpoint_type.value}" + while True: + if self.is_connection_open(grvt_endpoint_type): + try: + self.logger.debug(f"{FN} waiting for message") + response = await asyncio.wait_for( + self.ws[grvt_endpoint_type].recv(), timeout=WS_READ_TIMEOUT + ) + message = json.loads(response) + self.logger.debug(f"{FN} received {message=}") + self._check_susbcribed_stream(grvt_endpoint_type, message) + if "feed" in message: + stream_subscribed: str | None = message.get("stream") + selector: str = message.get("selector") + if stream_subscribed is None: + self.logger.warning(f"{FN} missing stream in {message=}") + if selector is None: + self.logger.warning(f"{FN} missing selector in {message=}") + if stream_subscribed and selector: + callback = ( + self.callbacks[grvt_endpoint_type] + .get(stream_subscribed, {}) + .get(selector, None) + ) + if callback: + await callback(message) + stream: str = self.get_non_versioned_stream( + stream_subscribed + ) + self._last_message[stream] = message + else: + self.logger.warning( + f"{FN} No callback for {stream_subscribed=}/{selector=}" + ) + elif "jsonrpc" in message: + """ + {'jsonrpc': '', 'result': {'result': + {'order_id': '0x00', 'sub_account_id': '8751933338735530', + 'is_market': False, 'time_in_force': 'GOOD_TILL_TIME', 'post_only': False, + 'reduce_only': False, 'legs': [{'instrument': 'BTC_USDT_Perp', 'size': '0.001', + 'limit_price': '50000.0', 'is_buying_asset': True}], + 'signature': {'signer': '0x2989e3783e2ae05f9a1538dd411a22a4cd9554ad', + 'r': '0xa566702c1e5557ab96e8d5197b6871456765a80556bba46c9d4928bd573ca66c', + 's': '0x6f6e0be6dca125643fce884ca28c0ae341b201efe49e10a9626859517b4a09af', + 'v': 28, 'expiration': '1729005262433997000', 'nonce': 3898454329}, + 'metadata': {'client_order_id': '123', 'create_time': '1728918862633971628'}, + 'state': {'status': 'OPEN', 'reject_reason': 'UNSPECIFIED', + 'book_size': ['0.001'], 'traded_size': ['0.0'], 'update_time': '1728918862633971628'}}}, + 'id': 2} + """ + self.logger.debug(f"{FN} jsonrpc result:{message.get('result')}") + else: + self.logger.info(f"{FN} Non-actionable message:{message}") + except ( + websockets.exceptions.ConnectionClosedError, + websockets.exceptions.ConnectionClosedOK, + ): + self.logger.exception( + f"{FN} connection closed {traceback.format_exc()}" + ) + await self._reconnect(grvt_endpoint_type) + except asyncio.TimeoutError: # noqa: UP041 + self.logger.debug(f"{FN} Timeout {WS_READ_TIMEOUT} secs") + pass + except Exception: + self.logger.exception( + f"{FN} connection failed {traceback.format_exc()}" + ) + await asyncio.sleep(1) + else: + self.logger.info(f"{FN} connection not open") + await asyncio.sleep(2) + + async def _send(self, end_point_type: GrvtWSEndpointType, message: str): + try: + if self.ws[end_point_type] and self.ws[end_point_type].open: + self.logger.info( + f"{self._clsname} _send() {end_point_type=}" + f" url:{self.api_url[end_point_type]} {message=}" + ) + await self.ws[end_point_type].send(message) + except websockets.exceptions.ConnectionClosedError as e: + self.logger.info(f"{self._clsname} _send() Restarted connection {e}") + await self._reconnect(end_point_type) + if self.ws[end_point_type]: + self.logger.info( + f"{self._clsname} _send() RESEND on RECONNECT {end_point_type=}" + f" url:{self.api_url[end_point_type]} {message=}" + ) + await self.ws[end_point_type].send(message) + except Exception: + self.logger.exception(f"{self._clsname} send failed {traceback.format_exc()}") + await self._reconnect(end_point_type) + + def _construct_selector(self, stream: str, params: dict) -> str: + feed: str = "" + # ******** Market Data ******** + if stream.endswith(("mini.s", "mini.d", "ticker.s", "ticker.d")): + feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}" + if stream.endswith("book.s"): + feed = ( + f"{params.get('instrument', '')}@{params.get('rate', '500')}-" + f"{params.get('depth', '10')}" + ) + if stream.endswith("book.d"): + feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}" + if stream.endswith("trade"): + feed = f"{params.get('instrument', '')}@{params.get('limit', '50')}" + if stream.endswith("candle"): + feed = ( + f"{params.get('instrument', '')}@{params.get('interval', 'CI_1_M')}-" + f"{params.get('type', 'TRADE')}" + ) + # ******** Trade Data ******** + if stream.endswith(("order", "state", "position", "fill")): + if not params: + feed = f"{self._trading_account_id}" + elif params.get("instrument"): + feed = f"{self._trading_account_id}-{params.get('instrument', '')}" + else: + feed = ( + f"{self._trading_account_id}-{params.get('kind', '')}-" + f"{params.get('base', '')}-{params.get('quote', '')}" + ) + # Deposit, Transfer, Withdrawal + if stream.endswith(("deposit", "transfer", "withdrawal")): + feed = "" + # f"{params.get('sub_account_id', '')}-{params.get('main_account_id', '')}" + + return feed + + async def subscribe( + self, + stream: str, + callback: Callable, + ws_end_point_type: GrvtWSEndpointType | None = None, + params: dict = {}, + ) -> None: + """ + Subscribe to a stream with optional parameters. + Call the callback function when a message is received. + callback function should have the following signature: + (dict) -> None. + """ + FN = f"{self._clsname} subscribe {stream=}" + if not ws_end_point_type: # use default endpoint type + ws_end_point_type = GRVT_WS_STREAMS.get(stream) + if not ws_end_point_type: + self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}") + return + is_trade_data = is_trading_ws_endpoint(ws_end_point_type) + if is_trade_data and not self._trading_account_id: + self.logger.error( + f"{FN} {stream=} is a trading data connection. Requires trading_account_id." + ) + return + # create selector string and register callback + selector: str = self._construct_selector(stream, params) + versioned_stream: str = self.get_versioned_stream(stream) + if versioned_stream not in self.callbacks[ws_end_point_type]: + self.callbacks[ws_end_point_type][versioned_stream] = {} + self.callbacks[ws_end_point_type][versioned_stream][selector] = callback + self.logger.info( + f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}" + ) + # check if connection is open and subscribe + if self.is_connection_open(ws_end_point_type): + await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector) + else: + self.logger.info(f"{FN} Connection not open. Will subscribe on connect.") + + async def re_subscribe_stream( + self, + stream: str, + callback: Callable, + ws_end_point_type: GrvtWSEndpointType | None = None, + params: dict = {}, + ) -> None: + """ This method should be called in a separate task - + otherwise it will block the event loop for 5 seconds. + Unsubscribe from a specific stream and subscribe again with optional parameters. + Call the callback function when a message is received. + callback function should have the following signature: + (dict) -> None. + """ + FN = f"{self._clsname} re_subscribe {stream=}" + if not ws_end_point_type: # use default endpoint type + ws_end_point_type = GRVT_WS_STREAMS.get(stream) + if not ws_end_point_type: + self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}") + return + if not self.is_connection_open(ws_end_point_type): + self.logger.info(f"{FN} {ws_end_point_type=} not open. Try again after connect.") + return + is_trade_data = is_trading_ws_endpoint(ws_end_point_type) + if is_trade_data and not self._trading_account_id: + self.logger.error( + f"{FN} {stream=} is a trading data connection. Requires trading_account_id." + ) + return + # create selector string and register callback + selector: str = self._construct_selector(stream, params) + versioned_stream: str = self.get_versioned_stream(stream) + if versioned_stream not in self.callbacks[ws_end_point_type]: + self.callbacks[ws_end_point_type][versioned_stream] = {} + self.callbacks[ws_end_point_type][versioned_stream][selector] = callback + # self.logger.info( + # f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}" + # ) + await self._unsubscribe_to_stream(ws_end_point_type, versioned_stream, selector) + await asyncio.sleep(5) # wait for unsubscribe to complete + await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector) + + + def get_versioned_stream(self, stream: str) -> str: + return ( + stream if self.api_ws_version == "v0" else f"{self.api_ws_version}.{stream}" + ) + + def get_non_versioned_stream(self, versioned_stream: str) -> str: + if self.api_ws_version == "v0": + return versioned_stream + return versioned_stream.split(".")[1] + + async def _subscribe_to_stream( + self, + ws_end_point_type: GrvtWSEndpointType, + versioned_stream: str, + selector: str, + ) -> None: + FN = ( + f"{self._clsname} _subscribe_to_stream {ws_end_point_type=}" + f" {versioned_stream=} {selector=}" + ) + self._request_id += 1 + if ws_end_point_type in [ + GrvtWSEndpointType.TRADE_DATA, + GrvtWSEndpointType.MARKET_DATA, + ]: # Legacy subscription + subscribe_json = json.dumps( + { + "request_id": self._request_id, + "stream": versioned_stream, + "feed": [selector], + "method": "subscribe", + "is_full": True, + } + ) + self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}") + else: # RPC WS format + self._request_id += 1 + subscribe_json = json.dumps( + { + "jsonrpc": "2.0", + "method": "subscribe", + "params": { + "stream": versioned_stream, + "selectors": [selector], + }, + "id": self._request_id, + } + ) + self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}") + await self._send(ws_end_point_type, subscribe_json) + stream: str = self.get_non_versioned_stream(versioned_stream) + if stream not in self._last_message: + self._last_message[stream] = {} + + async def _unsubscribe_to_stream( + self, + ws_end_point_type: GrvtWSEndpointType, + versioned_stream: str, + selector: str, + ) -> None: + FN = ( + f"{self._clsname} _unsubscribe_to_stream {ws_end_point_type=}" + f" {versioned_stream=} {selector=}" + ) + self._request_id += 1 + if ws_end_point_type in [ + GrvtWSEndpointType.TRADE_DATA, + GrvtWSEndpointType.MARKET_DATA, + ]: # Legacy subscription + subscribe_json = json.dumps( + { + "request_id": self._request_id, + "stream": versioned_stream, + "feed": [selector], + "method": "unsubscribe", + "is_full": True, + } + ) + self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}") + else: # RPC WS format + self._request_id += 1 + subscribe_json = json.dumps( + { + "jsonrpc": "2.0", + "method": "unsubscribe", + "params": { + "stream": versioned_stream, + "selectors": [selector], + }, + "id": self._request_id, + } + ) + self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}") + await self._send(ws_end_point_type, subscribe_json) + + def jsonrpc_wrap_payload( + self, payload: dict, method: str, version: str = "v1" + ) -> dict: + """ + Wrap the payload in JSON-RPC format. + """ + self._request_id += 1 + return { + "jsonrpc": "2.0", + "method": f"{version}/{method}", + "params": payload, + "id": self._request_id, + } + + async def send_rpc_message( + self, end_point_type: GrvtWSEndpointType, message: dict + ) -> None: + """ + Send a message to the server. + """ + await self._send(end_point_type, json.dumps(message)) + self.logger.info(f"{self._clsname} send_rpc_message {end_point_type=} {message=}") + + async def rpc_create_order( + self, + symbol: str, + order_type: GrvtOrderType, + side: GrvtOrderSide, + amount: float | Decimal | str | int, + price: Num = None, + params={}, + ) -> dict: + """ + Create an order. + """ + FN = f"{self._clsname} rpc_create_order" + if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL): + raise GrvtInvalidOrder("Trade data connection not available.") + order = self._get_order_with_validations( + symbol, order_type, side, amount, price, params + ) + self.logger.info(f"{FN} {order=}") + payload = get_order_rpc_payload(order, self._private_key, self.env, self.markets) + self._request_id += 1 + payload["id"] = self._request_id + self.logger.info(f"{FN} {payload=}") + await self.send_rpc_message(GrvtWSEndpointType.TRADE_DATA_RPC_FULL, payload) + return payload + + async def rpc_create_limit_order( + self, + symbol: str, + side: GrvtOrderSide, + amount: float | Decimal | str | int, + price: Num, + params={}, + ) -> dict: + return await self.rpc_create_order(symbol, "limit", side, amount, price, params) + + async def rpc_cancel_all_orders( + self, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature BUT lacks symbol + Cancel all orders for a sub-account. + params: dictionary with parameters. Valid keys:
    + `kind` (str): instrument kind. Valid values: 'PERPETUAL'.
    + `base` (str): base currency. If missing/empty then fetch + orders for all base currencies.
    + `quote` (str): quote currency. Defaults to all.
    + """ + self._check_account_auth() + # FN = f"{self._clsname} rpc_cancel_all_orders" + payload: dict = self._get_payload_cancel_all_orders(params) + jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="cancel_all_orders") + await self.send_rpc_message( + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload + ) + return jsonrpc_payload + + async def rpc_cancel_order( + self, + id: str | None = None, + symbol: str | None = None, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature + Cancel specific order for the account by sending JsonRpc call on WebSocket.
    + Private call requires authorization.
    + See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order) + for details.
    . + + Args: + id (str): exchange assigned order ID
    + symbol (str): trading symbol
    + params: + * client_order_id (str): client assigned order ID
    + * time_to_live_ms (str): lifetime of cancel requiest in millisecs
    + Returns: + payload used to cancel order.
    + """ + FN = f"{self._clsname} rpc_cancel_order" + if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL): + raise GrvtInvalidOrder("Trade data connection not available.") + self._check_account_auth() + # Prepare payload + payload: dict = { + "sub_account_id": str(self._trading_account_id), + } + if id: + payload["order_id"] = str(id) + elif "client_order_id" in params: + payload["client_order_id"] = str(params["client_order_id"]) + else: + raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id") + if "time_to_live_ms" in params: + payload["time_to_live_ms"] = str(params["time_to_live_ms"]) + # Send cancel requiest + jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="cancel_order") + await self.send_rpc_message( + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload + ) + return jsonrpc_payload + + async def rpc_fetch_open_orders( + self, + params: dict = {}, + ) -> dict: + """ + Fetch open orders for the account.
    + Private call requires authorization.
    + See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders) + for details.
    . + Fetches open orders for the account.
    + Sends JsonRpc call on WebSocket.
    + Args: + params: dictionary with parameters. Valid keys:
    + `kind` (str): instrument kind. Valid values are 'PERPETUAL'.
    + `base` (str): base currency. If missing/empty then fetch orders + for all base currencies.
    + `quote` (str): quote currency. Defaults to all.
    + Returns: + payload used to fetch open orders.

    + """ + self._check_account_auth() + # Prepare request payload + payload: dict = self._get_payload_fetch_open_orders(symbol=None, params=params) + jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="open_orders") + await self.send_rpc_message( + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload + ) + return jsonrpc_payload + + async def rpc_fetch_order( + self, + id: str | None = None, + symbol: str | None = None, + params: dict = {}, + ) -> dict: + """ + Ccxt compliant signature.
    + Private call requires authorization.
    + See [Get Order](https://api-docs.grvt.io/trading_api/#get-order) + for details.
    . + Get Order status by either order_id or client_order_id.
    + Sends JsonRpc call on WebSocket.
    + Args: + id: (str) order_id to fetch.
    + symbol: (str) NOT SUPPRTED.
    + params: dictionary with parameters. Valid keys:
    + `client_order_id` (int): client assigned order ID.
    + Returns: + payload used to fetch order.
    + """ + FN = f"{self._clsname} rpc_fetch_order" + self._check_account_auth() + payload = { + "sub_account_id": str(self._trading_account_id), + } + if id: + payload["order_id"] = id + elif "client_order_id" in params: + payload["client_order_id"] = str(params["client_order_id"]) + else: + raise GrvtInvalidOrder( + f"{FN} requires either order_id or params['client_order_id']" + ) + jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="order") + await self.send_rpc_message( + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload + ) + return jsonrpc_payload diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_fixed_types.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_fixed_types.py new file mode 100644 index 0000000..9700e96 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_fixed_types.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass + +from .grvt_raw_types import Signature, TransferType + + +@dataclass +class Transfer: + # The account to transfer from + from_account_id: str + # The subaccount to transfer from (0 if transferring from main account) + from_sub_account_id: str + # The account to deposit into + to_account_id: str + # The subaccount to transfer to (0 if transferring to main account) + to_sub_account_id: str + # The token currency to transfer + currency: str + # The number of tokens to transfer + num_tokens: str + # The signature of the transfer + signature: Signature + # The type of transfer + transfer_type: TransferType + # The metadata of the transfer + transfer_metadata: str diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_async.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_async.py new file mode 100644 index 0000000..a3dd798 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_async.py @@ -0,0 +1,365 @@ +from enum import Enum + +from dacite import Config, from_dict + +from . import grvt_raw_types as types +from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawAsyncBase + +# mypy: disable-error-code="no-any-return" + + +class GrvtRawAsync(GrvtRawAsyncBase): + def __init__(self, config: GrvtApiConfig): + super().__init__(config) + self.md_rpc = self.env.market_data.rpc_endpoint + self.td_rpc = self.env.trade_data.rpc_endpoint + + async def get_instrument_v1( + self, req: types.ApiGetInstrumentRequest + ) -> types.ApiGetInstrumentResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/instrument", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum])) + + async def get_all_instruments_v1( + self, req: types.ApiGetAllInstrumentsRequest + ) -> types.ApiGetAllInstrumentsResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/all_instruments", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum])) + + async def get_filtered_instruments_v1( + self, req: types.ApiGetFilteredInstrumentsRequest + ) -> types.ApiGetFilteredInstrumentsResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/instruments", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum]) + ) + + async def get_currency_v1( + self, req: types.ApiGetCurrencyRequest + ) -> types.ApiGetCurrencyResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/currency", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum])) + + async def mini_ticker_v1( + self, req: types.ApiMiniTickerRequest + ) -> types.ApiMiniTickerResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/mini", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum])) + + async def ticker_v1( + self, req: types.ApiTickerRequest + ) -> types.ApiTickerResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/ticker", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum])) + + async def orderbook_levels_v1( + self, req: types.ApiOrderbookLevelsRequest + ) -> types.ApiOrderbookLevelsResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/book", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum])) + + async def trade_v1( + self, req: types.ApiTradeRequest + ) -> types.ApiTradeResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/trade", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum])) + + async def trade_history_v1( + self, req: types.ApiTradeHistoryRequest + ) -> types.ApiTradeHistoryResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/trade_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum])) + + async def candlestick_v1( + self, req: types.ApiCandlestickRequest + ) -> types.ApiCandlestickResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/kline", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum])) + + async def funding_rate_v1( + self, req: types.ApiFundingRateRequest + ) -> types.ApiFundingRateResponse | GrvtError: + resp = await self._post(False, self.md_rpc + "/full/v1/funding", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum])) + + async def create_order_v1( + self, req: types.ApiCreateOrderRequest + ) -> types.ApiCreateOrderResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/create_order", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum])) + + async def cancel_order_v1( + self, req: types.ApiCancelOrderRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/cancel_order", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def cancel_all_orders_v1( + self, req: types.ApiCancelAllOrdersRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def get_order_v1( + self, req: types.ApiGetOrderRequest + ) -> types.ApiGetOrderResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/order", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum])) + + async def open_orders_v1( + self, req: types.ApiOpenOrdersRequest + ) -> types.ApiOpenOrdersResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/open_orders", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum])) + + async def order_history_v1( + self, req: types.ApiOrderHistoryRequest + ) -> types.ApiOrderHistoryResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/order_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum])) + + async def cancel_on_disconnect_v1( + self, req: types.ApiCancelOnDisconnectRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def fill_history_v1( + self, req: types.ApiFillHistoryRequest + ) -> types.ApiFillHistoryResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/fill_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum])) + + async def positions_v1( + self, req: types.ApiPositionsRequest + ) -> types.ApiPositionsResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/positions", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum])) + + async def funding_payment_history_v1( + self, req: types.ApiFundingPaymentHistoryRequest + ) -> types.ApiFundingPaymentHistoryResponse | GrvtError: + resp = await self._post( + True, self.td_rpc + "/full/v1/funding_payment_history", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum]) + ) + + async def deposit_history_v1( + self, req: types.ApiDepositHistoryRequest + ) -> types.ApiDepositHistoryResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/deposit_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum])) + + async def transfer_v1( + self, req: types.ApiTransferRequest + ) -> types.ApiTransferResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/transfer", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum])) + + async def transfer_history_v1( + self, req: types.ApiTransferHistoryRequest + ) -> types.ApiTransferHistoryResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/transfer_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum])) + + async def withdrawal_v1( + self, req: types.ApiWithdrawalRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def withdrawal_history_v1( + self, req: types.ApiWithdrawalHistoryRequest + ) -> types.ApiWithdrawalHistoryResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum])) + + async def sub_account_summary_v1( + self, req: types.ApiSubAccountSummaryRequest + ) -> types.ApiSubAccountSummaryResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/account_summary", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum])) + + async def sub_account_history_v1( + self, req: types.ApiSubAccountHistoryRequest + ) -> types.ApiSubAccountHistoryResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/account_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum])) + + async def aggregated_account_summary_v1( + self, req: types.EmptyRequest + ) -> types.ApiAggregatedAccountSummaryResponse | GrvtError: + resp = await self._post( + True, self.td_rpc + "/full/v1/aggregated_account_summary", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum]) + ) + + async def funding_account_summary_v1( + self, req: types.EmptyRequest + ) -> types.ApiFundingAccountSummaryResponse | GrvtError: + resp = await self._post( + True, self.td_rpc + "/full/v1/funding_account_summary", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum]) + ) + + async def set_derisk_mm_ratio_v1( + self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest + ) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum]) + ) + + async def get_all_initial_leverage_v1( + self, req: types.ApiGetAllInitialLeverageRequest + ) -> types.ApiGetAllInitialLeverageResponse | GrvtError: + resp = await self._post( + True, self.td_rpc + "/full/v1/get_all_initial_leverage", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum]) + ) + + async def set_initial_leverage_v1( + self, req: types.ApiSetInitialLeverageRequest + ) -> types.ApiSetInitialLeverageResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum])) + + async def vault_burn_tokens_v1( + self, req: types.ApiVaultBurnTokensRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def vault_invest_v1( + self, req: types.ApiVaultInvestRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/vault_invest", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def vault_investor_summary_v1( + self, req: types.ApiVaultInvestorSummaryRequest + ) -> types.ApiVaultInvestorSummaryResponse | GrvtError: + resp = await self._post( + True, self.td_rpc + "/full/v1/vault_investor_summary", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum])) + + async def vault_redeem_v1( + self, req: types.ApiVaultRedeemRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def vault_redeem_cancel_v1( + self, req: types.ApiVaultRedeemCancelRequest + ) -> types.AckResponse | GrvtError: + resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + async def vault_redemption_queue_v1( + self, req: types.ApiVaultViewRedemptionQueueRequest + ) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError: + resp = await self._post( + True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum]) + ) + + async def query_vault_manager_investor_history_v1( + self, req: types.ApiQueryVaultManagerInvestorHistoryRequest + ) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError: + resp = await self._post( + True, self.td_rpc + "/full/v1/vault_manager_investor_history", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum]) + ) diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_base.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_base.py new file mode 100644 index 0000000..b5b5d1f --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_base.py @@ -0,0 +1,267 @@ +import dataclasses +import json +import logging +import time +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from http.cookies import SimpleCookie +from typing import Any + +import aiohttp +import requests # type: ignore +from eth_account import Account + +from .grvt_raw_env import GrvtEnv, GrvtEnvConfig, get_env_config + + +@dataclass +class GrvtApiConfig: + env: GrvtEnv + trading_account_id: str | None + private_key: str | None + api_key: str | None + logger: logging.Logger | None + + +@dataclass +class GrvtError: + code: int + message: str + status: int + + +@dataclass +class GrvtCookie: + gravity: str + expires: datetime + grvt_account_id: str | None = None + + +class GrvtRawBase: + """ + GrvtRawBase is base class for Grvt Rest API classes. + + This should not be used directly, but rather through a derivative API class. + """ + + def __init__(self, config: GrvtApiConfig): + self.config = config + self.env: GrvtEnvConfig = get_env_config(config.env) + self.logger: logging.Logger = config.logger or logging.getLogger(__name__) + self._cookie: GrvtCookie | None = None + if self.config.private_key is not None: + self.account: Account = Account.from_key(self.config.private_key) + + """ + Cookie handling + """ + + def _should_refresh_cookie(self) -> bool: + if not self.config.api_key: + raise ValueError("Attempting to use Authenticated API without API key set") + time_till_expiration = None + if self._cookie and self._cookie.expires: + time_till_expiration = self._cookie.expires.timestamp() - time.time() + is_cookie_fresh = time_till_expiration is not None and time_till_expiration > 5 + if not is_cookie_fresh: + self.logger.info( + f"cookie should be refreshed now={time.time()}" + f" {time_till_expiration=} secs" + ) + return not is_cookie_fresh + + +class GrvtRawSyncBase(GrvtRawBase): + def __init__(self, config: GrvtApiConfig): + super().__init__(config) + # Sync API session + self._session: requests.Session = requests.Session() + self._session.headers.update({"Content-Type": "application/json"}) + + """ + Cookie handling + """ + + def _refresh_cookie(self) -> None: + if not self._should_refresh_cookie(): + return None + # Get cookie + self._cookie = self._get_cookie( + self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key) + ) + self.logger.info(f"refresh_cookie cookie={self._cookie}") + # Update cookie in session + if self._cookie: + self._session.cookies.update({"gravity": self._cookie.gravity}) + if self._cookie.grvt_account_id: + self._session.headers.update( + {"X-Grvt-Account-Id": self._cookie.grvt_account_id} + ) + return None + + def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None: + FN = f"_get_cookie {path=}" + try: + return_value = self._session.post( + path, + json={"api_key": api_key}, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + self.logger.info(f"{FN} {return_value=}") + if return_value.ok: + cookie = SimpleCookie() + cookie_header = return_value.headers.get("Set-Cookie") + grvt_cookie = return_value.cookies.get("gravity") + self.logger.info( + f"{FN} OK {return_value.headers=} \n " + f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}" + ) + cookie.load(cookie_header) + cookie_value = cookie["gravity"].value + cookie_expiry = datetime.strptime( + cookie["gravity"]["expires"], + "%a, %d %b %Y %H:%M:%S %Z", + ) + grvt_account_id: str | None = return_value.headers.get( + "X-Grvt-Account-Id" + ) + return GrvtCookie( + gravity=cookie_value, + expires=cookie_expiry, + grvt_account_id=grvt_account_id, + ) + return None + except Exception as e: + self.logger.error(f"{FN} Error getting cookie: {e}") + return None + + """ + Post handling + """ + + def _post(self, is_auth: bool, path: str, req: Any) -> Any: + FN = f"_post {path=}" + # Always see if need to referesh cookie before sending an authenticated request + if is_auth: + self._refresh_cookie() + + req_json = json.dumps(req, cls=DataclassJSONEncoder) + resp_json: Any = {} + + self.logger.debug(f"{FN} {req_json=}") + resp: requests.Response = self._session.post(path, data=req_json, timeout=5) + try: + resp_json = resp.json() + if not resp.ok: + self.logger.warning(f"{FN} Error {resp_json=}") + else: + self.logger.debug(f"{FN} OK {resp_json=}") + except Exception as err: + self.logger.error(f"{FN} Unable to parse {resp.text=} as json:{err=}") + return resp_json + + +class GrvtRawAsyncBase(GrvtRawBase): + def __init__(self, config: GrvtApiConfig): + super().__init__(config) + # Async API session + self._session: aiohttp.ClientSession = aiohttp.ClientSession( + headers={"Content-Type": "application/json"} + ) + + """ + Cookie handling + """ + + async def _refresh_cookie(self) -> None: + if not self._should_refresh_cookie(): + return None + + # Get cookie + self._cookie = await self._get_cookie( + self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key) + ) + self.logger.info(f"refresh_cookie cookie={self._cookie}") + + # Update cookie in session + if self._cookie: + self._session.cookie_jar.update_cookies({"gravity": self._cookie.gravity}) + if self._cookie.grvt_account_id: + self._session.headers.update( + {"X-Grvt-Account-Id": self._cookie.grvt_account_id} + ) + return None + + async def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None: + FN = f"_get_cookie {path=}" + try: + data = {"api_key": api_key} + self.logger.info(f"{FN} ask for cookie {path=} {data=}") + async with aiohttp.ClientSession() as session: + async with session.post(url=path, json=data, timeout=5) as return_value: + self.logger.info(f"{FN} {return_value=}") + if return_value.ok: + cookie = SimpleCookie() + cookie_header = return_value.headers.get("Set-Cookie") + grvt_cookie = return_value.cookies.get("gravity") + self.logger.info( + f"{FN} OK {return_value.headers=} \n " + f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}" + ) + cookie.load(cookie_header) + cookie_value = cookie["gravity"].value + cookie_expiry = datetime.strptime( + cookie["gravity"]["expires"], + "%a, %d %b %Y %H:%M:%S %Z", + ) + grvt_account_id: str | None = return_value.headers.get( + "X-Grvt-Account-Id" + ) + return GrvtCookie( + gravity=cookie_value, + expires=cookie_expiry, + grvt_account_id=grvt_account_id, + ) + return None + except Exception as e: + self.logger.error(f"{FN} Error getting cookie: {e}") + return None + + """ + Post handling + """ + + async def _post(self, is_auth: bool, path: str, req: Any) -> Any: + FN = f"_post {path=}" + # Always see if need to referesh cookie before sending an authenticated request + if is_auth: + await self._refresh_cookie() + + req_json = json.dumps(req, cls=DataclassJSONEncoder) + resp_json: Any = {} + + self.logger.debug(f"{FN} {req_json=}") + resp: aiohttp.ClientResponse = await self._session.post( + path, data=req_json, timeout=5 + ) + try: + resp_text = await resp.text() + resp_json = json.loads(resp_text) + if not resp.ok: + self.logger.warning(f"{FN} Error {resp_text=}") + else: + self.logger.debug(f"{FN} OK {resp_text=}") + except Exception as err: + self.logger.error(f"{FN} Unable to parse {resp_text=} as json:{err=}") + return resp_json + + +class DataclassJSONEncoder(json.JSONEncoder): + def default(self, o: Any) -> Any: + if dataclasses.is_dataclass(o): + return dataclasses.asdict(o) # type: ignore + if isinstance(o, Enum): + return o.value + return super().default(o) diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_env.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_env.py new file mode 100644 index 0000000..1c3d5d8 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_env.py @@ -0,0 +1,77 @@ +from dataclasses import dataclass +from enum import Enum + + +class GrvtEnv(Enum): + DEV = "dev" + STAGING = "staging" + TESTNET = "testnet" + PROD = "prod" + + +@dataclass +class GrvtEndpointConfig: + rpc_endpoint: str + ws_endpoint: str | None + + +@dataclass +class GrvtEnvConfig: + edge: GrvtEndpointConfig + trade_data: GrvtEndpointConfig + market_data: GrvtEndpointConfig + chain_id: int + + +def get_env_config(environment: GrvtEnv) -> GrvtEnvConfig: + match environment: + case GrvtEnv.PROD: + return GrvtEnvConfig( + edge=GrvtEndpointConfig( + rpc_endpoint="https://edge.grvt.io", + ws_endpoint=None, + ), + trade_data=GrvtEndpointConfig( + rpc_endpoint="https://trades.grvt.io", + ws_endpoint="wss://trades.grvt.io/ws", + ), + market_data=GrvtEndpointConfig( + rpc_endpoint="https://market-data.grvt.io", + ws_endpoint="wss://market-data.grvt.io/ws", + ), + chain_id=325, + ) + case GrvtEnv.TESTNET: + return GrvtEnvConfig( + edge=GrvtEndpointConfig( + rpc_endpoint=f"https://edge.{environment.value}.grvt.io", + ws_endpoint=None, + ), + trade_data=GrvtEndpointConfig( + rpc_endpoint=f"https://trades.{environment.value}.grvt.io", + ws_endpoint=f"wss://trades.{environment.value}.grvt.io/ws", + ), + market_data=GrvtEndpointConfig( + rpc_endpoint=f"https://market-data.{environment.value}.grvt.io", + ws_endpoint=f"wss://market-data.{environment.value}.grvt.io/ws", + ), + chain_id=326, + ) + case GrvtEnv.DEV | GrvtEnv.STAGING: + return GrvtEnvConfig( + edge=GrvtEndpointConfig( + rpc_endpoint=f"https://edge.{environment.value}.gravitymarkets.io", + ws_endpoint=None, + ), + trade_data=GrvtEndpointConfig( + rpc_endpoint=f"https://trades.{environment.value}.gravitymarkets.io", + ws_endpoint=f"wss://trades.{environment.value}.gravitymarkets.io/ws", + ), + market_data=GrvtEndpointConfig( + rpc_endpoint=f"https://market-data.{environment.value}.gravitymarkets.io", + ws_endpoint=f"wss://market-data.{environment.value}.gravitymarkets.io/ws", + ), + chain_id=327 if environment == GrvtEnv.DEV else 328, + ) + case _: + raise ValueError(f"Unknown environment={environment}") diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_signing.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_signing.py new file mode 100644 index 0000000..1000bd3 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_signing.py @@ -0,0 +1,248 @@ +from enum import Enum +from decimal import Decimal +from typing import Any, Optional + +from eth_account import Account +from eth_account.messages import encode_typed_data + +from .grvt_ccxt_utils import GrvtCurrency +from .grvt_raw_base import GrvtApiConfig, GrvtEnv +from .grvt_raw_types import Instrument, Order, Withdrawal, TimeInForce +from .grvt_fixed_types import Transfer + +######################### +# INSTRUMENT CONVERSION # +######################### + + +PRICE_MULTIPLIER = 1_000_000_000 + + +class SignTimeInForce(Enum): + GOOD_TILL_TIME = 1 + ALL_OR_NONE = 2 + IMMEDIATE_OR_CANCEL = 3 + FILL_OR_KILL = 4 + + +TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = { + TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME, + TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE, + TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL, + TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL, +} + + +##################### +# EIP-712 chain IDs # +##################### +CHAIN_IDS = { + GrvtEnv.DEV: 327, + GrvtEnv.STAGING: 327, + GrvtEnv.TESTNET: 326, + GrvtEnv.PROD: 325, +} + + +def get_EIP712_domain_data(env: GrvtEnv, chainId: int | None) -> dict[str, str | int]: + return { + "name": "GRVT Exchange", + "version": "0", + "chainId": chainId or CHAIN_IDS[env], + } + + +##################### +# Sign Order # +##################### + +EIP712_ORDER_MESSAGE_TYPE = { + "Order": [ + {"name": "subAccountID", "type": "uint64"}, + {"name": "isMarket", "type": "bool"}, + {"name": "timeInForce", "type": "uint8"}, + {"name": "postOnly", "type": "bool"}, + {"name": "reduceOnly", "type": "bool"}, + {"name": "legs", "type": "OrderLeg[]"}, + {"name": "nonce", "type": "uint32"}, + {"name": "expiration", "type": "int64"}, + ], + "OrderLeg": [ + {"name": "assetID", "type": "uint256"}, + {"name": "contractSize", "type": "uint64"}, + {"name": "limitPrice", "type": "uint64"}, + {"name": "isBuyingContract", "type": "bool"}, + ], +} + + +def sign_order( + order: Order, + config: GrvtApiConfig, + account: Account, + instruments: dict[str, Instrument], +) -> Order: + if config.private_key is None: + raise ValueError("Private key is not set") + + message_data = build_EIP712_order_message_data(order, instruments) + + domain_data = get_EIP712_domain_data(config.env, CHAIN_IDS[config.env]) + signable_message = encode_typed_data( + domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data + ) + signed_message = account.sign_message(signable_message) + + order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex() + order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex() + order.signature.v = signed_message.v + order.signature.signer = str(account.address) + + return order + + +def build_EIP712_order_message_data( + order: Order, instruments: dict[str, Instrument] +) -> dict[str, Any]: + legs = [] + for leg in order.legs: + instrument = instruments[leg.instrument] + size_multiplier = 10**instrument.base_decimals + + # use Decimal() instead of float() to avoid precision loss + # int(float("1.013") * 1e9) = 1012999999 + # int(Decimal("1.013") * Decimal(1e9) = 1013000000 + size_int = int(Decimal(leg.size) * Decimal(size_multiplier)) + price_int = int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER)) + legs.append( + { + "assetID": instrument.instrument_hash, + "contractSize": size_int, + "limitPrice": price_int, + "isBuyingContract": leg.is_buying_asset, + } + ) + return { + "subAccountID": order.sub_account_id, + "isMarket": order.is_market or False, + "timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value, + "postOnly": order.post_only or False, + "reduceOnly": order.reduce_only or False, + "legs": legs, + "nonce": order.signature.nonce, + "expiration": order.signature.expiration, + } + + +##################### +# Sign Transfer # +##################### + +EIP712_TRANSFER_MESSAGE_TYPE = { + "Transfer": [ + {"name": "fromAccount", "type": "address"}, + {"name": "fromSubAccount", "type": "uint64"}, + {"name": "toAccount", "type": "address"}, + {"name": "toSubAccount", "type": "uint64"}, + {"name": "tokenCurrency", "type": "uint8"}, + {"name": "numTokens", "type": "uint64"}, + {"name": "nonce", "type": "uint32"}, + {"name": "expiration", "type": "int64"}, + ], +} + + +def build_EIP712_transfer_message_data(transfer: Transfer, currencyId: int): + return { + "fromAccount": transfer.from_account_id, + "fromSubAccount": transfer.from_sub_account_id, + "toAccount": transfer.to_account_id, + "toSubAccount": transfer.to_sub_account_id, + "tokenCurrency": currencyId, + "numTokens": int( + Decimal(transfer.num_tokens) * Decimal(1e6) + ), # USDT has 6 decimals + "nonce": transfer.signature.nonce, + "expiration": transfer.signature.expiration, + } + + +def sign_transfer( + transfer: Transfer, + config: GrvtApiConfig, + account: Account, + chainId: int | None = None, + currencyId: int = 3, # currencyId of USDT; refer to Get Currency API +) -> Transfer: + if config.private_key is None: + raise ValueError("Private key is not set") + + domain = get_EIP712_domain_data(config.env, chainId) + + message_data = build_EIP712_transfer_message_data(transfer, currencyId) + signable_message = encode_typed_data( + domain, EIP712_TRANSFER_MESSAGE_TYPE, message_data + ) + signed_message = account.sign_message(signable_message) + + transfer.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex() + transfer.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex() + transfer.signature.v = signed_message.v + transfer.signature.signer = str(account.address) + + return transfer + + +##################### +# Sign Withdrawal # +##################### + +EIP712_WITHDRAWAL_MESSAGE_TYPE = { + "Withdrawal": [ + {"name": "fromAccount", "type": "address"}, + {"name": "toEthAddress", "type": "address"}, + {"name": "tokenCurrency", "type": "uint8"}, + {"name": "numTokens", "type": "uint64"}, + {"name": "nonce", "type": "uint32"}, + {"name": "expiration", "type": "int64"}, + ], +} + + +def build_EIP712_withdrawal_message_data(withdrawal: Withdrawal, currencyId: int): + return { + "fromAccount": withdrawal.from_account_id, + "toEthAddress": withdrawal.to_eth_address, + "tokenCurrency": currencyId, + "numTokens": int( + Decimal(withdrawal.num_tokens) * Decimal(1e6) + ), # USDT has 6 decimals + "nonce": withdrawal.signature.nonce, + "expiration": withdrawal.signature.expiration, + } + + +def sign_withdrawal( + withdrawal: Withdrawal, + config: GrvtApiConfig, + account: Account, + chainId: int | None = None, + currencyId: int = 3, # currencyId of USDT; refer to Get Currency API +) -> Withdrawal: + if config.private_key is None: + raise ValueError("Private key is not set") + + domain = get_EIP712_domain_data(config.env, chainId) + + message_data = build_EIP712_withdrawal_message_data(withdrawal, currencyId) + signable_message = encode_typed_data( + domain, EIP712_WITHDRAWAL_MESSAGE_TYPE, message_data + ) + signed_message = account.sign_message(signable_message) + + withdrawal.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex() + withdrawal.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex() + withdrawal.signature.v = signed_message.v + withdrawal.signature.signer = str(account.address) + + return withdrawal diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_sync.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_sync.py new file mode 100644 index 0000000..451ebdb --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_sync.py @@ -0,0 +1,351 @@ +from enum import Enum + +from dacite import Config, from_dict + +from . import grvt_raw_types as types +from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawSyncBase + +# mypy: disable-error-code="no-any-return" + + +class GrvtRawSync(GrvtRawSyncBase): + def __init__(self, config: GrvtApiConfig): + super().__init__(config) + self.md_rpc = self.env.market_data.rpc_endpoint + self.td_rpc = self.env.trade_data.rpc_endpoint + + def get_instrument_v1( + self, req: types.ApiGetInstrumentRequest + ) -> types.ApiGetInstrumentResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/instrument", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum])) + + def get_all_instruments_v1( + self, req: types.ApiGetAllInstrumentsRequest + ) -> types.ApiGetAllInstrumentsResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/all_instruments", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum])) + + def get_filtered_instruments_v1( + self, req: types.ApiGetFilteredInstrumentsRequest + ) -> types.ApiGetFilteredInstrumentsResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/instruments", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum]) + ) + + def get_currency_v1( + self, req: types.ApiGetCurrencyRequest + ) -> types.ApiGetCurrencyResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/currency", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum])) + + def mini_ticker_v1( + self, req: types.ApiMiniTickerRequest + ) -> types.ApiMiniTickerResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/mini", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum])) + + def ticker_v1( + self, req: types.ApiTickerRequest + ) -> types.ApiTickerResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/ticker", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum])) + + def orderbook_levels_v1( + self, req: types.ApiOrderbookLevelsRequest + ) -> types.ApiOrderbookLevelsResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/book", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum])) + + def trade_v1(self, req: types.ApiTradeRequest) -> types.ApiTradeResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/trade", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum])) + + def trade_history_v1( + self, req: types.ApiTradeHistoryRequest + ) -> types.ApiTradeHistoryResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/trade_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum])) + + def candlestick_v1( + self, req: types.ApiCandlestickRequest + ) -> types.ApiCandlestickResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/kline", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum])) + + def funding_rate_v1( + self, req: types.ApiFundingRateRequest + ) -> types.ApiFundingRateResponse | GrvtError: + resp = self._post(False, self.md_rpc + "/full/v1/funding", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum])) + + def create_order_v1( + self, req: types.ApiCreateOrderRequest + ) -> types.ApiCreateOrderResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/create_order", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum])) + + def cancel_order_v1( + self, req: types.ApiCancelOrderRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/cancel_order", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def cancel_all_orders_v1( + self, req: types.ApiCancelAllOrdersRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def get_order_v1( + self, req: types.ApiGetOrderRequest + ) -> types.ApiGetOrderResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/order", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum])) + + def open_orders_v1( + self, req: types.ApiOpenOrdersRequest + ) -> types.ApiOpenOrdersResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/open_orders", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum])) + + def order_history_v1( + self, req: types.ApiOrderHistoryRequest + ) -> types.ApiOrderHistoryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/order_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum])) + + def cancel_on_disconnect_v1( + self, req: types.ApiCancelOnDisconnectRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def fill_history_v1( + self, req: types.ApiFillHistoryRequest + ) -> types.ApiFillHistoryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/fill_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum])) + + def positions_v1( + self, req: types.ApiPositionsRequest + ) -> types.ApiPositionsResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/positions", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum])) + + def funding_payment_history_v1( + self, req: types.ApiFundingPaymentHistoryRequest + ) -> types.ApiFundingPaymentHistoryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/funding_payment_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum]) + ) + + def deposit_history_v1( + self, req: types.ApiDepositHistoryRequest + ) -> types.ApiDepositHistoryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/deposit_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum])) + + def transfer_v1( + self, req: types.ApiTransferRequest + ) -> types.ApiTransferResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/transfer", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum])) + + def transfer_history_v1( + self, req: types.ApiTransferHistoryRequest + ) -> types.ApiTransferHistoryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/transfer_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum])) + + def withdrawal_v1( + self, req: types.ApiWithdrawalRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/withdrawal", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def withdrawal_history_v1( + self, req: types.ApiWithdrawalHistoryRequest + ) -> types.ApiWithdrawalHistoryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum])) + + def sub_account_summary_v1( + self, req: types.ApiSubAccountSummaryRequest + ) -> types.ApiSubAccountSummaryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/account_summary", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum])) + + def sub_account_history_v1( + self, req: types.ApiSubAccountHistoryRequest + ) -> types.ApiSubAccountHistoryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/account_history", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum])) + + def aggregated_account_summary_v1( + self, req: types.EmptyRequest + ) -> types.ApiAggregatedAccountSummaryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/aggregated_account_summary", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum]) + ) + + def funding_account_summary_v1( + self, req: types.EmptyRequest + ) -> types.ApiFundingAccountSummaryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/funding_account_summary", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum]) + ) + + def set_derisk_mm_ratio_v1( + self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest + ) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum]) + ) + + def get_all_initial_leverage_v1( + self, req: types.ApiGetAllInitialLeverageRequest + ) -> types.ApiGetAllInitialLeverageResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/get_all_initial_leverage", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum]) + ) + + def set_initial_leverage_v1( + self, req: types.ApiSetInitialLeverageRequest + ) -> types.ApiSetInitialLeverageResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum])) + + def vault_burn_tokens_v1( + self, req: types.ApiVaultBurnTokensRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def vault_invest_v1( + self, req: types.ApiVaultInvestRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/vault_invest", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def vault_investor_summary_v1( + self, req: types.ApiVaultInvestorSummaryRequest + ) -> types.ApiVaultInvestorSummaryResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/vault_investor_summary", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum])) + + def vault_redeem_v1( + self, req: types.ApiVaultRedeemRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def vault_redeem_cancel_v1( + self, req: types.ApiVaultRedeemCancelRequest + ) -> types.AckResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict(types.AckResponse, resp, Config(cast=[Enum])) + + def vault_redemption_queue_v1( + self, req: types.ApiVaultViewRedemptionQueueRequest + ) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError: + resp = self._post(True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum]) + ) + + def query_vault_manager_investor_history_v1( + self, req: types.ApiQueryVaultManagerInvestorHistoryRequest + ) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError: + resp = self._post( + True, self.td_rpc + "/full/v1/vault_manager_investor_history", req + ) + if resp.get("code"): + return GrvtError(**resp) + return from_dict( + types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum]) + ) diff --git a/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_types.py b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_types.py new file mode 100644 index 0000000..d803543 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/src/pysdk/grvt_raw_types.py @@ -0,0 +1,2697 @@ +# ruff: noqa: D200 +# ruff: noqa: D204 +# ruff: noqa: D205 +# ruff: noqa: D404 +# ruff: noqa: W291 +# ruff: noqa: D400 +# ruff: noqa: E501 +from dataclasses import dataclass +from enum import Enum +from typing import Any + + +class BrokerTag(Enum): + UNSPECIFIED = "UNSPECIFIED" + # CoinRoutes + COIN_ROUTES = "COIN_ROUTES" + # Alertatron + ALERTATRON = "ALERTATRON" + # Origami + ORIGAMI = "ORIGAMI" + + +class CancelStatus(Enum): + # Cancellation has expired because corresponding order had not arrived within the defined time-to-live window. + EXPIRED = "EXPIRED" + # This cancellation request was dropped because its TTL window overlaps with another cancellation request for the same order. + DROPPED_DUPLICATE = "DROPPED_DUPLICATE" + + +class CandlestickInterval(Enum): + # 1 minute + CI_1_M = "CI_1_M" + # 3 minutes + CI_3_M = "CI_3_M" + # 5 minutes + CI_5_M = "CI_5_M" + # 15 minutes + CI_15_M = "CI_15_M" + # 30 minutes + CI_30_M = "CI_30_M" + # 1 hour + CI_1_H = "CI_1_H" + # 2 hour + CI_2_H = "CI_2_H" + # 4 hour + CI_4_H = "CI_4_H" + # 6 hour + CI_6_H = "CI_6_H" + # 8 hour + CI_8_H = "CI_8_H" + # 12 hour + CI_12_H = "CI_12_H" + # 1 day + CI_1_D = "CI_1_D" + # 3 days + CI_3_D = "CI_3_D" + # 5 days + CI_5_D = "CI_5_D" + # 1 week + CI_1_W = "CI_1_W" + # 2 weeks + CI_2_W = "CI_2_W" + # 3 weeks + CI_3_W = "CI_3_W" + # 4 weeks + CI_4_W = "CI_4_W" + + +class CandlestickType(Enum): + # Tracks traded prices + TRADE = "TRADE" + # Tracks mark prices + MARK = "MARK" + # Tracks index prices + INDEX = "INDEX" + # Tracks book mid prices + MID = "MID" + + +class InstrumentSettlementPeriod(Enum): + # Instrument settles through perpetual funding cycles + PERPETUAL = "PERPETUAL" + # Instrument settles at an expiry date, marked as a daily instrument + DAILY = "DAILY" + # Instrument settles at an expiry date, marked as a weekly instrument + WEEKLY = "WEEKLY" + # Instrument settles at an expiry date, marked as a monthly instrument + MONTHLY = "MONTHLY" + # Instrument settles at an expiry date, marked as a quarterly instrument + QUARTERLY = "QUARTERLY" + + +class Kind(Enum): + # the perpetual asset kind + PERPETUAL = "PERPETUAL" + # the future asset kind + FUTURE = "FUTURE" + # the call option asset kind + CALL = "CALL" + # the put option asset kind + PUT = "PUT" + + +class MarginType(Enum): + # Simple Cross Margin Mode: all assets have a predictable margin impact, the whole subaccount shares a single margin + SIMPLE_CROSS_MARGIN = "SIMPLE_CROSS_MARGIN" + # Portfolio Cross Margin Mode: asset margin impact is analysed on portfolio level, the whole subaccount shares a single margin + PORTFOLIO_CROSS_MARGIN = "PORTFOLIO_CROSS_MARGIN" + + +class OrderRejectReason(Enum): + # order is not cancelled or rejected + UNSPECIFIED = "UNSPECIFIED" + # client called a Cancel API + CLIENT_CANCEL = "CLIENT_CANCEL" + # client called a Bulk Cancel API + CLIENT_BULK_CANCEL = "CLIENT_BULK_CANCEL" + # client called a Session Cancel API, or set the WebSocket connection to 'cancelOrdersOnTerminate' + CLIENT_SESSION_END = "CLIENT_SESSION_END" + # the market order was cancelled after no/partial fill. Lower precedence than other TimeInForce cancel reasons + MARKET_CANCEL = "MARKET_CANCEL" + # the IOC order was cancelled after no/partial fill + IOC_CANCEL = "IOC_CANCEL" + # the AON order was cancelled as it could not be fully matched + AON_CANCEL = "AON_CANCEL" + # the FOK order was cancelled as it could not be fully matched + FOK_CANCEL = "FOK_CANCEL" + # the order was cancelled as it has expired + EXPIRED = "EXPIRED" + # the post-only order could not be posted into the orderbook + FAIL_POST_ONLY = "FAIL_POST_ONLY" + # the reduce-only order would have caused position size to increase + FAIL_REDUCE_ONLY = "FAIL_REDUCE_ONLY" + # the order was cancelled due to market maker protection trigger + MM_PROTECTION = "MM_PROTECTION" + # the order was cancelled due to self-trade protection trigger + SELF_TRADE_PROTECTION = "SELF_TRADE_PROTECTION" + # the order matched with another order from the same sub account + SELF_MATCHED_SUBACCOUNT = "SELF_MATCHED_SUBACCOUNT" + # an active order on your sub account shares the same clientOrderId + OVERLAPPING_CLIENT_ORDER_ID = "OVERLAPPING_CLIENT_ORDER_ID" + # the order will bring the sub account below initial margin requirement + BELOW_MARGIN = "BELOW_MARGIN" + # the sub account is liquidated (and all open orders are cancelled by Gravity) + LIQUIDATION = "LIQUIDATION" + # instrument is invalid or not found on Gravity + INSTRUMENT_INVALID = "INSTRUMENT_INVALID" + # instrument is no longer tradable on Gravity. (typically due to a market halt, or instrument expiry) + INSTRUMENT_DEACTIVATED = "INSTRUMENT_DEACTIVATED" + # system failover resulting in loss of order state + SYSTEM_FAILOVER = "SYSTEM_FAILOVER" + # the credentials used (userSession/apiKeySession/walletSignature) is not authorised to perform the action + UNAUTHORISED = "UNAUTHORISED" + # the session key used to sign the order expired + SESSION_KEY_EXPIRED = "SESSION_KEY_EXPIRED" + # the subaccount does not exist + SUB_ACCOUNT_NOT_FOUND = "SUB_ACCOUNT_NOT_FOUND" + # the signature used to sign the order has no trade permission + NO_TRADE_PERMISSION = "NO_TRADE_PERMISSION" + # the order payload does not contain a supported TimeInForce value + UNSUPPORTED_TIME_IN_FORCE = "UNSUPPORTED_TIME_IN_FORCE" + # the order has multiple legs, but multiple legs are not supported by this venue + MULTI_LEGGED_ORDER = "MULTI_LEGGED_ORDER" + # the order would have caused the subaccount to exceed the max position size + EXCEED_MAX_POSITION_SIZE = "EXCEED_MAX_POSITION_SIZE" + # the signature supplied is more than 30 days in the future + EXCEED_MAX_SIGNATURE_EXPIRATION = "EXCEED_MAX_SIGNATURE_EXPIRATION" + # the market order has a limit price set + MARKET_ORDER_WITH_LIMIT_PRICE = "MARKET_ORDER_WITH_LIMIT_PRICE" + # client cancel on disconnect triggered + CLIENT_CANCEL_ON_DISCONNECT_TRIGGERED = "CLIENT_CANCEL_ON_DISCONNECT_TRIGGERED" + # the OCO counter part order was triggered + OCO_COUNTER_PART_TRIGGERED = "OCO_COUNTER_PART_TRIGGERED" + # the remaining order size was cancelled because it exceeded current position size + REDUCE_ONLY_LIMIT = "REDUCE_ONLY_LIMIT" + # the order was replaced by a client replace request + CLIENT_REPLACE = "CLIENT_REPLACE" + # the derisk order must be an IOC order + DERISK_MUST_BE_IOC = "DERISK_MUST_BE_IOC" + # the derisk order must be a reduce-only order + DERISK_MUST_BE_REDUCE_ONLY = "DERISK_MUST_BE_REDUCE_ONLY" + # derisk is not supported + DERISK_NOT_SUPPORTED = "DERISK_NOT_SUPPORTED" + # the order type is invalid + INVALID_ORDER_TYPE = "INVALID_ORDER_TYPE" + # the currency is not defined + CURRENCY_NOT_DEFINED = "CURRENCY_NOT_DEFINED" + + +class OrderStatus(Enum): + # Order has been sent to the matching engine and is pending a transition to open/filled/rejected. + PENDING = "PENDING" + # Order is actively matching on the matching engine, could be unfilled or partially filled. + OPEN = "OPEN" + # Order is fully filled and hence closed. Taker Orders can transition directly from pending to filled, without going through open. + FILLED = "FILLED" + # Order is rejected by matching engine since if fails a particular check (See OrderRejectReason). Once an order is open, it cannot be rejected. + REJECTED = "REJECTED" + # Order is cancelled by the user using one of the supported APIs (See OrderRejectReason). Before an order is open, it cannot be cancelled. + CANCELLED = "CANCELLED" + + +class TimeInForce(Enum): + """ + | | Must Fill All | Can Fill Partial | + | - | - | - | + | Must Fill Immediately | FOK | IOC | + | Can Fill Till Time | AON | GTC | + + """ + + # GTT - Remains open until it is cancelled, or expired + GOOD_TILL_TIME = "GOOD_TILL_TIME" + # AON - Either fill the whole order or none of it (Block Trades Only) + ALL_OR_NONE = "ALL_OR_NONE" + # IOC - Fill the order as much as possible, when hitting the orderbook. Then cancel it + IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL" + # FOK - Both AoN and IoC. Either fill the full order when hitting the orderbook, or cancel it + FILL_OR_KILL = "FILL_OR_KILL" + # RPI - A GTT + PostOnly maker order, that can only be taken by non-algorithmic UI users. + RETAIL_PRICE_IMPROVEMENT = "RETAIL_PRICE_IMPROVEMENT" + + +class TransferType(Enum): + # Default transfer that has nothing to do with bridging + UNSPECIFIED = "UNSPECIFIED" + # Standard transfer that has nothing to do with bridging + STANDARD = "STANDARD" + # Fast Arb Deposit Metadata type + FAST_ARB_DEPOSIT = "FAST_ARB_DEPOSIT" + # Fast Arb Withdrawal Metadata type + FAST_ARB_WITHDRAWAL = "FAST_ARB_WITHDRAWAL" + # Transfer type for non native bridging deposit + NON_NATIVE_BRIDGE_DEPOSIT = "NON_NATIVE_BRIDGE_DEPOSIT" + # Transfer type for non native bridging withdrawal + NON_NATIVE_BRIDGE_WITHDRAWAL = "NON_NATIVE_BRIDGE_WITHDRAWAL" + + +class TriggerBy(Enum): + """ + Defines the price type that activates a Take Profit (TP) or Stop Loss (SL) order. + + Trigger orders are executed when the selected price type reaches the specified trigger price.Different price types ensure flexibility in executing strategies based on market conditions. + + + """ + + # no trigger condition + UNSPECIFIED = "UNSPECIFIED" + # INDEX - Order is activated when the index price reaches the trigger price + INDEX = "INDEX" + # LAST - Order is activated when the last trade price reaches the trigger price + LAST = "LAST" + # MID - Order is activated when the mid price reaches the trigger price + MID = "MID" + # MARK - Order is activated when the mark price reaches the trigger price + MARK = "MARK" + + +class TriggerType(Enum): + """ + Defines the type of trigger order used in trading, such as Take Profit or Stop Loss. + + Trigger orders allow execution based on pre-defined price conditions rather than immediate market conditions. + + + """ + + # Not a trigger order. The order executes normally without any trigger conditions. + UNSPECIFIED = "UNSPECIFIED" + # Take Profit Order - Executes when the price reaches a specified level to secure profits. + TAKE_PROFIT = "TAKE_PROFIT" + # Stop Loss Order - Executes when the price reaches a specified level to limit losses. + STOP_LOSS = "STOP_LOSS" + + +class VaultInvestorAction(Enum): + UNSPECIFIED = "UNSPECIFIED" + VAULT_INVEST = "VAULT_INVEST" + VAULT_BURN_LP_TOKEN = "VAULT_BURN_LP_TOKEN" + VAULT_REDEEM = "VAULT_REDEEM" + + +class VaultRedemptionReqAgeCategory(Enum): + """ + Denotes the age category of a given redemption request. + + + """ + + # This request is at least as old as the minimum redemption period, and is eligible for automated redemption. + NORMAL = "NORMAL" + # This request is nearing the maxmimum redemption period and will be factored into pre-order check margin requirements. + URGENT = "URGENT" + # This request has exceeded the maximum redemption period and will be considered for forced redemptions. + OVERDUE = "OVERDUE" + # This request has yet to exceed the minimum redemption period, and is not yet eligible for automated redemption. + PRE_MIN = "PRE_MIN" + + +class Venue(Enum): + # the trade is cleared on the orderbook venue + ORDERBOOK = "ORDERBOOK" + # the trade is cleared on the RFQ venue + RFQ = "RFQ" + + +@dataclass +class ApiPositionsRequest: + # The sub account ID to request for + sub_account_id: str + # The kind filter to apply. If nil, this defaults to all kinds. Otherwise, only entries matching the filter will be returned + kind: list[Kind] | None = None + # The base filter to apply. If nil, this defaults to all bases. Otherwise, only entries matching the filter will be returned + base: list[str] | None = None + # The quote filter to apply. If nil, this defaults to all quotes. Otherwise, only entries matching the filter will be returned + quote: list[str] | None = None + + +@dataclass +class Positions: + # Time at which the event was emitted in unix nanoseconds + event_time: str + # The sub account ID that participated in the trade + sub_account_id: str + # The instrument being represented + instrument: str + # The size of the position, expressed in base asset decimal units. Negative for short positions + size: str + # The notional value of the position, negative for short assets, expressed in quote asset decimal units + notional: str + """ + The entry price of the position, expressed in `9` decimals + Whenever increasing the size of a position, the entry price is updated to the new average entry price + `new_entry_price = (old_entry_price * old_size + trade_price * trade_size) / (old_size + trade_size)` + """ + entry_price: str + """ + The exit price of the position, expressed in `9` decimals + Whenever decreasing the size of a position, the exit price is updated to the new average exit price + `new_exit_price = (old_exit_price * old_exit_trade_size + trade_price * trade_size) / (old_exit_trade_size + trade_size)` + """ + exit_price: str + # The mark price of the position, expressed in `9` decimals + mark_price: str + """ + The unrealized PnL of the position, expressed in quote asset decimal units + `unrealized_pnl = (mark_price - entry_price) * size` + """ + unrealized_pnl: str + """ + The realized PnL of the position, expressed in quote asset decimal units + `realized_pnl = (exit_price - entry_price) * exit_trade_size` + """ + realized_pnl: str + """ + The total PnL of the position, expressed in quote asset decimal units + `total_pnl = realized_pnl + unrealized_pnl` + """ + total_pnl: str + """ + The ROI of the position, expressed as a percentage + `roi = (total_pnl / (entry_price * abs(size))) * 100^` + """ + roi: str + # The index price of the quote currency. (reported in `USD`) + quote_index_price: str + # The estimated liquidation price + est_liquidation_price: str + # The current leverage value for this position + leverage: str + + +@dataclass +class ApiPositionsResponse: + # The positions matching the request filter + result: list[Positions] + + +@dataclass +class ApiFillHistoryRequest: + """ + Query for all historical fills made by a single account. A single order can be matched multiple times, hence there is no real way to uniquely identify a trade. + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The sub account ID to request for + sub_account_id: str + # The kind filter to apply. If nil, this defaults to all kinds. Otherwise, only entries matching the filter will be returned + kind: list[Kind] | None = None + # The base filter to apply. If nil, this defaults to all bases. Otherwise, only entries matching the filter will be returned + base: list[str] | None = None + # The quote filter to apply. If nil, this defaults to all quotes. Otherwise, only entries matching the filter will be returned + quote: list[str] | None = None + # The start time to apply in unix nanoseconds. If nil, this defaults to all start times. Otherwise, only entries matching the filter will be returned + start_time: str | None = None + # The end time to apply in unix nanoseconds. If nil, this defaults to all end times. Otherwise, only entries matching the filter will be returned + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the query from + cursor: str | None = None + + +@dataclass +class Fill: + # Time at which the event was emitted in unix nanoseconds + event_time: str + # The sub account ID that participated in the trade + sub_account_id: str + # The instrument being represented + instrument: str + # The side that the subaccount took on the trade + is_buyer: bool + # The role that the subaccount took on the trade + is_taker: bool + # The number of assets being traded, expressed in base asset decimal units + size: str + # The traded price, expressed in `9` decimals + price: str + # The mark price of the instrument at point of trade, expressed in `9` decimals + mark_price: str + # The index price of the instrument at point of trade, expressed in `9` decimals + index_price: str + # The interest rate of the underlying at point of trade, expressed in centibeeps (1/100th of a basis point) + interest_rate: str + # [Options] The forward price of the option at point of trade, expressed in `9` decimals + forward_price: str + # The realized PnL of the trade, expressed in quote asset decimal units (0 if increasing position size) + realized_pnl: str + # The fees paid on the trade, expressed in quote asset decimal unit (negative if maker rebate applied) + fee: str + # The fee rate paid on the trade + fee_rate: str + """ + A trade identifier, globally unique, and monotonically increasing (not by `1`). + All trades sharing a single taker execution share the same first component (before `-`), and `event_time`. + `trade_id` is guaranteed to be consistent across MarketData `Trade` and Trading `Fill`. + """ + trade_id: str + # An order identifier + order_id: str + # The venue where the trade occurred + venue: Venue + """ + A unique identifier for the active order within a subaccount, specified by the client + This is used to identify the order in the client's system + This field can be used for order amendment/cancellation, but has no bearing on the smart contract layer + This field will not be propagated to the smart contract, and should not be signed by the client + This value must be unique for all active orders in a subaccount, or amendment/cancellation will not work as expected + Gravity UI will generate a random clientOrderID for each order in the range [0, 2^63 - 1] + To prevent any conflicts, client machines should generate a random clientOrderID in the range [2^63, 2^64 - 1] + + When GRVT Backend receives an order with an overlapping clientOrderID, we will reject the order with rejectReason set to overlappingClientOrderId + """ + client_order_id: str + # The address (public key) of the wallet signing the payload + signer: str + # If the trade is a RPI trade + is_rpi: bool + # Specifies the broker who brokered the order + broker: BrokerTag | None = None + + +@dataclass +class ApiFillHistoryResponse: + # The private trades matching the request asset + result: list[Fill] + # The cursor to indicate when to start the query from + next: str + + +@dataclass +class ApiFundingPaymentHistoryRequest: + """ + Query for all historical funding payments made by a single account. + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The sub account ID to request for + sub_account_id: str + # The perpetual instrument to filter for + instrument: str | None = None + # The start time to apply in unix nanoseconds. If nil, this defaults to all start times. Otherwise, only entries matching the filter will be returned + start_time: str | None = None + # The end time to apply in unix nanoseconds. If nil, this defaults to all end times. Otherwise, only entries matching the filter will be returned + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the query from + cursor: str | None = None + + +@dataclass +class FundingPayment: + # Time at which the event was emitted in unix nanoseconds + event_time: str + # The sub account ID that made the funding payment + sub_account_id: str + # The perpetual instrument being funded + instrument: str + # The currency of the funding payment + currency: str + # The amount of the funding payment. Positive if paid, negative if received + amount: str + """ + The transaction ID of the funding payment. + Funding payments can be triggered by a trade, transfer, or liquidation. + The `tx_id` will match the corresponding `trade_id` or `tx_id`. + """ + tx_id: str + + +@dataclass +class ApiFundingPaymentHistoryResponse: + # The funding payments matching the request asset + result: list[FundingPayment] + # The cursor to indicate when to start the query from + next: str + + +@dataclass +class ApiSubAccountSummaryRequest: + # The subaccount ID to filter by + sub_account_id: str + + +@dataclass +class SpotBalance: + # The currency you hold a spot balance in + currency: str + # This currency's balance in this trading account. + balance: str + # The index price of this currency. (reported in `USD`) + index_price: str + + +@dataclass +class SubAccount: + # Time at which the event was emitted in unix nanoseconds + event_time: str + # The sub account ID this entry refers to + sub_account_id: str + # The type of margin algorithm this subaccount uses + margin_type: MarginType + """ + The settlement, margin, and reporting currency of this account. + This subaccount can only open positions quoted in this currency + + In the future, when users select a Multi-Currency Margin Type, this will be USD + All other assets are converted to this currency for the purpose of calculating margin + """ + settle_currency: str + """ + The total unrealized PnL of all positions owned by this subaccount, denominated in quote currency decimal units. + `unrealized_pnl = sum(position.unrealized_pnl * position.quote_index_price) / settle_index_price` + """ + unrealized_pnl: str + """ + The notional value of your account if all positions are closed, excluding trading fees (reported in `settle_currency`). + `total_equity = sum(spot_balance.balance * spot_balance.index_price) / settle_index_price + unrealized_pnl` + """ + total_equity: str + """ + The `total_equity` required to open positions in the account (reported in `settle_currency`). + Computation is different depending on account's `margin_type` + """ + initial_margin: str + """ + The `total_equity` required to avoid liquidation of positions in the account (reported in `settle_currency`). + Computation is different depending on account's `margin_type` + """ + maintenance_margin: str + """ + The notional value available to transfer out of the trading account into the funding account (reported in `settle_currency`). + `available_balance = total_equity - initial_margin - min(unrealized_pnl, 0)` + """ + available_balance: str + # The list of spot assets owned by this sub account, and their balances + spot_balances: list[SpotBalance] + # The list of positions owned by this sub account + positions: list[Positions] + # The index price of the settle currency. (reported in `USD`) + settle_index_price: str + # The derisk margin of this sub account + derisk_margin: str + # The derisk margin to maintenance margin ratio of this sub account + derisk_to_maintenance_margin_ratio: str + # Whether this sub account is a vault + is_vault: bool | None = None + # Total amount of IM (reported in `settle_currency`) deducted from the vault due to redemptions nearing the end of their redemption period + vault_im_additions: str | None = None + + +@dataclass +class ApiSubAccountSummaryResponse: + # The sub account matching the request sub account + result: SubAccount + + +@dataclass +class ApiSubAccountHistoryRequest: + """ + The request to get the history of a sub account + SubAccount Summary values are snapshotted once every hour + No snapshots are taken if the sub account has no activity in the hourly window + History is preserved only for the last 30 days + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The sub account ID to request for + sub_account_id: str + # Start time of sub account history in unix nanoseconds + start_time: str | None = None + # End time of sub account history in unix nanoseconds + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the next query from + cursor: str | None = None + + +@dataclass +class ApiSubAccountHistoryResponse: + # The sub account history matching the request sub account + result: list[SubAccount] + # The cursor to indicate when to start the next query from + next: str + + +@dataclass +class AggregatedAccountSummary: + # The main account ID of the account to which the summary belongs + main_account_id: str + # Total equity of the main (+ sub) account, denominated in USD + total_equity: str + # The list of spot assets owned by this main (+ sub) account, and their balances + spot_balances: list[SpotBalance] + + +@dataclass +class ApiAggregatedAccountSummaryResponse: + # The aggregated account summary + result: AggregatedAccountSummary + + +@dataclass +class FundingAccountSummary: + # The main account ID of the account to which the summary belongs + main_account_id: str + # Total equity of the main account, denominated in USD + total_equity: str + # The list of spot assets owned by this main account, and their balances + spot_balances: list[SpotBalance] + + +@dataclass +class ClientTier: + tier: int + futures_taker_fee: int + futures_maker_fee: int + options_taker_fee: int + options_maker_fee: int + + +@dataclass +class ApiFundingAccountSummaryResponse: + # The funding account summary + result: FundingAccountSummary + # Client fee tier at the time of query + tier: ClientTier + + +@dataclass +class ApiSetInitialLeverageRequest: + # The sub account ID to set the leverage for + sub_account_id: str + # The instrument to set the leverage for + instrument: str + # The leverage to set for the sub account + leverage: str + + +@dataclass +class ApiSetInitialLeverageResponse: + # Whether the leverage was set successfully + success: bool + + +@dataclass +class ApiGetAllInitialLeverageRequest: + # The sub account ID to get the leverage for + sub_account_id: str + + +@dataclass +class InitialLeverageResult: + # The instrument to get the leverage for + instrument: str + # The initial leverage of the sub account + leverage: str + # The min leverage this sub account can set + min_leverage: str + # The max leverage this sub account can set + max_leverage: str + + +@dataclass +class ApiGetAllInitialLeverageResponse: + # The initial leverage of the sub account + results: list[InitialLeverageResult] + + +@dataclass +class Signature: + # The address (public key) of the wallet signing the payload + signer: str + # Signature R + r: str + # Signature S + s: str + # Signature V + v: int + # Timestamp after which this signature expires, expressed in unix nanoseconds. Must be capped at 30 days + expiration: str + """ + Users can randomly generate this value, used as a signature deconflicting key. + ie. You can send the same exact instruction twice with different nonces. + When the same nonce is used, the same payload will generate the same signature. + Our system will consider the payload a duplicate, and ignore it. + """ + nonce: int + + +@dataclass +class ApiSetDeriskToMaintenanceMarginRatioRequest: + # The sub account ID to set the leverage for + sub_account_id: str + # The derisk margin to maintenance margin ratio of this sub account + ratio: str + # The signature of this operation + signature: Signature + + +@dataclass +class ApiSetDeriskToMaintenanceMarginRatioResponse: + # Whether the derisk margin to maintenance margin ratio was set successfully + success: bool + + +@dataclass +class JSONRPCRequest: + """ + All Websocket JSON RPC Requests are housed in this wrapper. You may specify a stream, and a list of feeds to subscribe to. + If a `request_id` is supplied in this JSON RPC request, it will be propagated back to any relevant JSON RPC responses (including error). + When subscribing to the same primary selector again, the previous secondary selector will be replaced. See `Overview` page for more details. + """ + + # The JSON RPC version to use for the request + jsonrpc: str + # The method to use for the request (eg: `subscribe` / `unsubscribe` / `v1/instrument` ) + method: str + # The parameters for the request + params: Any + """ + Optional Field which is used to match the response by the client. + If not passed, this field will not be returned + """ + id: int | None = None + + +@dataclass +class Error: + # The error code for the request + code: int + # The error message for the request + message: str + + +@dataclass +class JSONRPCResponse: + """ + All Websocket JSON RPC Responses are housed in this wrapper. It returns a confirmation of the JSON RPC subscribe request. + If a `request_id` is supplied in the JSON RPC request, it will be propagated back in this JSON RPC response. + """ + + # The JSON RPC version to use for the request + jsonrpc: str + # The method used in the request for this response (eg: `subscribe` / `unsubscribe` / `v1/instrument` ) + method: str + # The result for the request + result: Any | None = None + # The error for the request + error: Error | None = None + """ + Optional Field which is used to match the response by the client. + If not passed, this field will not be returned + """ + id: int | None = None + + +@dataclass +class WSSubscribeParams: + """ + All V1 Websocket Subscription Requests are housed in this wrapper. You may specify a stream and a list of feeds to subscribe to. + When subscribing to the same primary selector again, the previous secondary selector will be replaced. See `Overview` page for more details. + Sequence numbers can be either gateway-specific or global: + - **Gateway Unique Sequence Number**: Increments by one per stream, resets to 0 on gateway restart. + - **Global Unique Sequence Number**: A cluster-wide unique number assigned to each cluster payload, does not reset on gateway restarts, and can be used to track and identify message order across streams using `sequence_number` and `prev_sequence_number` in the feed response. + Set `useGlobalSequenceNumber = true` if you need a persistent, unique identifier across all streams or ordering across multiple feeds. + """ + + # The channel to subscribe to (eg: ticker.s / ticker.d) + stream: str + # The list of feeds to subscribe to + selectors: list[str] + # Whether to use the global sequence number for the stream + use_global_sequence_number: bool | None = None + + +@dataclass +class WSSubscribeResult: + """ + To ensure you always know if you have missed any payloads, GRVT servers apply the following heuristics to sequence numbers:
    • All snapshot payloads will have a sequence number of `0`. All delta payloads will have a sequence number of `1+`. So its easy to distinguish between snapshots, and deltas
    • Num snapshots returned in Response (per stream): You can ensure that you received the right number of snapshots
    • First sequence number returned in Response (per stream): You can ensure that you received the first stream, without gaps from snapshots
    • Sequence numbers should always monotonically increase by `1`. If it decreases, or increases by more than `1`. Please reconnect
    • Duplicate sequence numbers are possible due to network retries. If you receive a duplicate, please ignore it, or idempotently re-update it.
    + When subscribing to the same primary selector again, the previous secondary selector will be replaced. See `Overview` page for more details. + """ + + # The channel to subscribe to (eg: ticker.s / ticker.d) + stream: str + # The list of feeds subscribed to + subs: list[str] + # The list of feeds unsubscribed from + unsubs: list[str] + # The number of snapshot payloads to expect for each subscribed feed. Returned in same order as `subs` + num_snapshots: list[int] + # The first sequence number to expect for each subscribed feed. Returned in same order as `subs` + first_sequence_number: list[str] + # The sequence number of the most recent message in the stream. Next received sequence number must be larger than this one. Returned in same order as `subs` + latest_sequence_number: list[str] + + +@dataclass +class WSUnsubscribeParams: + # The channel to unsubscribe from (eg: ticker.s / ticker.d) + stream: str + # The list of feeds to unsubscribe from + selectors: list[str] + # Whether to use the global sequence number for the stream + use_global_sequence_number: bool | None = None + + +@dataclass +class WSUnsubscribeResult: + # The channel to subscribe to (eg: ticker.s / ticker.d) + stream: str + # The list of feeds unsubscribed from + unsubs: list[str] + + +@dataclass +class WSSubscribeRequestV1Legacy: + """ + All V1 Websocket Requests are housed in this wrapper. You may specify a stream, and a list of feeds to subscribe to. + If a `request_id` is supplied in this JSON RPC request, it will be propagated back to any relevant JSON RPC responses (including error). + When subscribing to the same primary selector again, the previous secondary selector will be replaced. See `Overview` page for more details. + """ + + # The channel to subscribe to (eg: ticker.s / ticker.d) + stream: str + # The list of feeds to subscribe to + feed: list[str] + # The method to use for the request (eg: subscribe / unsubscribe) + method: str + """ + Optional Field which is used to match the response by the client. + If not passed, this field will not be returned + """ + request_id: int | None = None + # Whether the request is for full data or lite data + is_full: bool | None = None + + +@dataclass +class WSSubscribeResponseV1Legacy: + """ + All V1 Websocket Responses are housed in this wrapper. It returns a confirmation of the JSON RPC subscribe request. + If a `request_id` is supplied in the JSON RPC request, it will be propagated back in this JSON RPC response. + To ensure you always know if you have missed any payloads, GRVT servers apply the following heuristics to sequence numbers:
    • All snapshot payloads will have a sequence number of `0`. All delta payloads will have a sequence number of `1+`. So its easy to distinguish between snapshots, and deltas
    • Num snapshots returned in Response (per stream): You can ensure that you received the right number of snapshots
    • First sequence number returned in Response (per stream): You can ensure that you received the first stream, without gaps from snapshots
    • Sequence numbers should always monotonically increase by `1`. If it decreases, or increases by more than `1`. Please reconnect
    • Duplicate sequence numbers are possible due to network retries. If you receive a duplicate, please ignore it, or idempotently re-update it.
    + When subscribing to the same primary selector again, the previous secondary selector will be replaced. See `Overview` page for more details. + """ + + # The channel to subscribe to (eg: ticker.s / ticker.d) + stream: str + # The list of feeds subscribed to + subs: list[str] + # The list of feeds unsubscribed from + unsubs: list[str] + # The number of snapshot payloads to expect for each subscribed feed. Returned in same order as `subs` + num_snapshots: list[int] + # The first sequence number to expect for each subscribed feed. Returned in same order as `subs` + first_sequence_number: list[str] + # The sequence number of the most recent message in the stream. Next received sequence number must be larger than this one. Returned in same order as `subs` + latest_sequence_number: list[str] + """ + Optional Field which is used to match the response by the client. + If not passed, this field will not be returned + """ + request_id: int | None = None + + +@dataclass +class WSOrderbookLevelsFeedSelectorV1: + """ + Subscribes to aggregated orderbook updates for a single instrument. The `book.s` channel offers simpler integration. To experience higher publishing rates, please use the `book.d` channel. + Unlike the `book.d` channel which publishes an initial snapshot, then only streams deltas after, the `book.s` channel publishes full snapshots at each feed. + + The Delta feed will work as follows:
    • On subscription, the server will send a full snapshot of all levels of the Orderbook.
    • After the snapshot, the server will only send levels that have changed in value.
    + + Subscription Pattern:
    • Delta - `instrument@rate`
    • Snapshot - `instrument@rate-depth`
    + + Field Semantics:
    • [DeltaOnly] If a level is not updated, level not published
    • If a level is updated, {size: '123'}
    • If a level is set to zero, {size: '0'}
    • Incoming levels will be published as soon as price moves
    • Outgoing levels will be published with `size = 0`
    + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + """ + The minimal rate at which we publish feeds (in milliseconds) + Delta (50, 100, 500, 1000) + Snapshot (500, 1000) + """ + rate: int + """ + Depth of the order book to be retrieved + Delta(0 - `unlimited`) + Snapshot(10, 50, 100, 500) + """ + depth: int | None = None + + +@dataclass +class OrderbookLevel: + # The price of the level, expressed in `9` decimals + price: str + # The number of assets offered, expressed in base asset decimal units + size: str + # The number of open orders at this level + num_orders: int + + +@dataclass +class OrderbookLevels: + # Time at which the event was emitted in unix nanoseconds + event_time: str + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The list of best bids up till query depth + bids: list[OrderbookLevel] + # The list of best asks up till query depth + asks: list[OrderbookLevel] + + +@dataclass +class WSOrderbookLevelsFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # An orderbook levels object matching the request filter + feed: OrderbookLevels + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSMiniTickerFeedSelectorV1: + """ + Subscribes to a mini ticker feed for a single instrument. The `mini.s` channel offers simpler integration. To experience higher publishing rates, please use the `mini.d` channel. + Unlike the `mini.d` channel which publishes an initial snapshot, then only streams deltas after, the `mini.s` channel publishes full snapshots at each feed. + + The Delta feed will work as follows:
    • On subscription, the server will send a full snapshot of the mini ticker.
    • After the snapshot, the server will only send deltas of the mini ticker.
    • The server will send a delta if any of the fields in the mini ticker have changed.
    + + Field Semantics:
    • [DeltaOnly] If a field is not updated, {}
    • If a field is updated, {field: '123'}
    • If a field is set to zero, {field: '0'}
    • If a field is set to null, {field: ''}
    + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + """ + The minimal rate at which we publish feeds (in milliseconds) + Delta (0 - `raw`, 50, 100, 200, 500, 1000, 5000) + Snapshot (200, 500, 1000, 5000) + """ + rate: int + + +@dataclass +class MiniTicker: + # Time at which the event was emitted in unix nanoseconds + event_time: str | None = None + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str | None = None + # The mark price of the instrument, expressed in `9` decimals + mark_price: str | None = None + # The index price of the instrument, expressed in `9` decimals + index_price: str | None = None + # The last traded price of the instrument (also close price), expressed in `9` decimals + last_price: str | None = None + # The number of assets traded in the last trade, expressed in base asset decimal units + last_size: str | None = None + # The mid price of the instrument, expressed in `9` decimals + mid_price: str | None = None + # The best bid price of the instrument, expressed in `9` decimals + best_bid_price: str | None = None + # The number of assets offered on the best bid price of the instrument, expressed in base asset decimal units + best_bid_size: str | None = None + # The best ask price of the instrument, expressed in `9` decimals + best_ask_price: str | None = None + # The number of assets offered on the best ask price of the instrument, expressed in base asset decimal units + best_ask_size: str | None = None + + +@dataclass +class WSMiniTickerFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # A mini ticker matching the request filter + feed: MiniTicker + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSTickerFeedSelectorV1: + """ + Subscribes to a ticker feed for a single instrument. The `ticker.s` channel offers simpler integration. To experience higher publishing rates, please use the `ticker.d` channel. + Unlike the `ticker.d` channel which publishes an initial snapshot, then only streams deltas after, the `ticker.s` channel publishes full snapshots at each feed. + + The Delta feed will work as follows:
    • On subscription, the server will send a full snapshot of the ticker.
    • After the snapshot, the server will only send deltas of the ticker.
    • The server will send a delta if any of the fields in the ticker have changed.
    + + Field Semantics:
    • [DeltaOnly] If a field is not updated, {}
    • If a field is updated, {field: '123'}
    • If a field is set to zero, {field: '0'}
    • If a field is set to null, {field: ''}
    + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + """ + The minimal rate at which we publish feeds (in milliseconds) + Delta (100, 200, 500, 1000, 5000) + Snapshot (500, 1000, 5000) + """ + rate: int + + +@dataclass +class Ticker: + """ + Derived data such as the below, will not be included by default: + - 24 hour volume (`buyVolume + sellVolume`) + - 24 hour taker buy/sell ratio (`buyVolume / sellVolume`) + - 24 hour average trade price (`volumeQ / volumeU`) + - 24 hour average trade volume (`volume / trades`) + - 24 hour percentage change (`24hStatChange / 24hStat`) + - 48 hour statistics (`2 * 24hStat - 24hStatChange`) + + To query for an extended ticker payload, leverage the `greeks` and the `derived` flags. + Ticker extensions are currently under design to offer you more convenience. + These flags are only supported on the `Ticker Snapshot` WS endpoint, and on the `Ticker` API endpoint. + + """ + + # Time at which the event was emitted in unix nanoseconds + event_time: str | None = None + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str | None = None + # The mark price of the instrument, expressed in `9` decimals + mark_price: str | None = None + # The index price of the instrument, expressed in `9` decimals + index_price: str | None = None + # The last traded price of the instrument (also close price), expressed in `9` decimals + last_price: str | None = None + # The number of assets traded in the last trade, expressed in base asset decimal units + last_size: str | None = None + # The mid price of the instrument, expressed in `9` decimals + mid_price: str | None = None + # The best bid price of the instrument, expressed in `9` decimals + best_bid_price: str | None = None + # The number of assets offered on the best bid price of the instrument, expressed in base asset decimal units + best_bid_size: str | None = None + # The best ask price of the instrument, expressed in `9` decimals + best_ask_price: str | None = None + # The number of assets offered on the best ask price of the instrument, expressed in base asset decimal units + best_ask_size: str | None = None + # The current funding rate of the instrument, expressed in percentage points + funding_rate_8h_curr: str | None = None + # The average funding rate of the instrument (over last 8h), expressed in percentage points + funding_rate_8h_avg: str | None = None + # The interest rate of the underlying, expressed in centibeeps (1/100th of a basis point) + interest_rate: str | None = None + # [Options] The forward price of the option, expressed in `9` decimals + forward_price: str | None = None + # The 24 hour taker buy volume of the instrument, expressed in base asset decimal units + buy_volume_24h_b: str | None = None + # The 24 hour taker sell volume of the instrument, expressed in base asset decimal units + sell_volume_24h_b: str | None = None + # The 24 hour taker buy volume of the instrument, expressed in quote asset decimal units + buy_volume_24h_q: str | None = None + # The 24 hour taker sell volume of the instrument, expressed in quote asset decimal units + sell_volume_24h_q: str | None = None + # The 24 hour highest traded price of the instrument, expressed in `9` decimals + high_price: str | None = None + # The 24 hour lowest traded price of the instrument, expressed in `9` decimals + low_price: str | None = None + # The 24 hour first traded price of the instrument, expressed in `9` decimals + open_price: str | None = None + # The open interest in the instrument, expressed in base asset decimal units + open_interest: str | None = None + # The ratio of accounts that are net long vs net short on this instrument + long_short_ratio: str | None = None + + +@dataclass +class WSTickerFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # A ticker matching the request filter + feed: Ticker + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSTradeFeedSelectorV1: + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The limit to query for. Valid values are (50, 200, 500, 1000). Default is 50 + limit: int + + +@dataclass +class Trade: + # Time at which the event was emitted in unix nanoseconds + event_time: str + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # If taker was the buyer on the trade + is_taker_buyer: bool + # The number of assets being traded, expressed in base asset decimal units + size: str + # The traded price, expressed in `9` decimals + price: str + # The mark price of the instrument at point of trade, expressed in `9` decimals + mark_price: str + # The index price of the instrument at point of trade, expressed in `9` decimals + index_price: str + # The interest rate of the underlying at point of trade, expressed in centibeeps (1/100th of a basis point) + interest_rate: str + # [Options] The forward price of the option at point of trade, expressed in `9` decimals + forward_price: str + """ + A trade identifier, globally unique, and monotonically increasing (not by `1`). + All trades sharing a single taker execution share the same first component (before `-`), and `event_time`. + `trade_id` is guaranteed to be consistent across MarketData `Trade` and Trading `Fill`. + """ + trade_id: str + # The venue where the trade occurred + venue: Venue + # If the trade is a RPI trade + is_rpi: bool + + +@dataclass +class WSTradeFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # A public trade matching the request filter + feed: Trade + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSCandlestickFeedSelectorV1: + """ + Subscribes to a stream of Kline/Candlestick updates for an instrument. A Kline is uniquely identified by its open time. + A new Kline is published every interval (if it exists). Upon subscription, the server will send the 5 most recent Kline for the requested interval. + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The interval of each candlestick + interval: CandlestickInterval + # The type of candlestick data to retrieve + type: CandlestickType + + +@dataclass +class Candlestick: + # Open time of kline bar in unix nanoseconds + open_time: str + # Close time of kline bar in unix nanosecond + close_time: str + # The open price, expressed in underlying currency resolution units + open: str + # The close price, expressed in underlying currency resolution units + close: str + # The high price, expressed in underlying currency resolution units + high: str + # The low price, expressed in underlying currency resolution units + low: str + # The underlying volume transacted, expressed in base asset decimal units + volume_b: str + # The quote volume transacted, expressed in quote asset decimal units + volume_q: str + # The number of trades transacted + trades: int + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + + +@dataclass +class WSCandlestickFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # A candlestick entry matching the request filters + feed: Candlestick + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSUnsubscribeAllParams: + pass + + +@dataclass +class StreamReference: + # The channel to subscribe to (eg: ticker.s / ticker.d) + stream: str + # The list of selectors for the stream + selectors: list[str] + + +@dataclass +class WSUnsubscribeAllResult: + # The list of stream references unsubscribed from + stream_reference: list[StreamReference] + + +@dataclass +class WSListStreamsParams: + pass + + +@dataclass +class WSListStreamsResult: + # The list of stream references the connection is connected to + stream_reference: list[StreamReference] + + +@dataclass +class ApiOrderbookLevelsRequest: + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # Depth of the order book to be retrieved (10, 50, 100, 500) + depth: int + + +@dataclass +class ApiOrderbookLevelsResponse: + # The orderbook levels objects matching the request asset + result: OrderbookLevels + + +@dataclass +class ApiMiniTickerRequest: + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + + +@dataclass +class ApiMiniTickerResponse: + # The mini ticker matching the request asset + result: MiniTicker + + +@dataclass +class ApiTickerRequest: + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + + +@dataclass +class ApiTickerResponse: + # The mini ticker matching the request asset + result: Ticker + + +@dataclass +class ApiTradeRequest: + """ + Retrieves up to 1000 of the most recent trades in any given instrument. Do not use this to poll for data -- a websocket subscription is much more performant, and useful. + This endpoint offers public trading data, use the Trading APIs instead to query for your personalized trade tape. + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The limit to query for. Defaults to 500; Max 1000 + limit: int + + +@dataclass +class ApiTradeResponse: + # The public trades matching the request asset + result: list[Trade] + + +@dataclass +class ApiTradeHistoryRequest: + """ + Perform historical lookup of public trades in any given instrument. + This endpoint offers public trading data, use the Trading APIs instead to query for your personalized trade tape. + Only data from the last three months will be retained. + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The start time to apply in nanoseconds. If nil, this defaults to all start times. Otherwise, only entries matching the filter will be returned + start_time: str | None = None + # The end time to apply in nanoseconds. If nil, this defaults to all end times. Otherwise, only entries matching the filter will be returned + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the query from + cursor: str | None = None + + +@dataclass +class ApiTradeHistoryResponse: + # The public trades matching the request asset + result: list[Trade] + # The cursor to indicate when to start the next query from + next: str | None = None + + +@dataclass +class ApiGetInstrumentRequest: + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + + +@dataclass +class Instrument: + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The asset ID used for instrument signing. + instrument_hash: str + # The base currency + base: str + # The quote currency + quote: str + # The kind of instrument + kind: Kind + # Venues that this instrument can be traded at + venues: list[Venue] + # The settlement period of the instrument + settlement_period: InstrumentSettlementPeriod + # The smallest denomination of the base asset supported by GRVT (+3 represents 0.001, -3 represents 1000, 0 represents 1) + base_decimals: int + # The smallest denomination of the quote asset supported by GRVT (+3 represents 0.001, -3 represents 1000, 0 represents 1) + quote_decimals: int + # The size of a single tick, expressed in price decimal units + tick_size: str + # The minimum contract size, expressed in base asset decimal units + min_size: str + # Creation time in unix nanoseconds + create_time: str + # The maximum position size, expressed in base asset decimal units + max_position_size: str + + +@dataclass +class ApiGetInstrumentResponse: + # The instrument matching the request asset + result: Instrument + + +@dataclass +class ApiGetFilteredInstrumentsRequest: + # The kind filter to apply. If nil, this defaults to all kinds. Otherwise, only entries matching the filter will be returned + kind: list[Kind] | None = None + # The base filter to apply. If nil, this defaults to all bases. Otherwise, only entries matching the filter will be returned + base: list[str] | None = None + # The quote filter to apply. If nil, this defaults to all quotes. Otherwise, only entries matching the filter will be returned + quote: list[str] | None = None + # Request for active instruments only + is_active: bool | None = None + # The limit to query for. Defaults to 500; Max 100000 + limit: int | None = None + + +@dataclass +class ApiGetFilteredInstrumentsResponse: + # The instruments matching the request filter + result: list[Instrument] + + +@dataclass +class ApiGetCurrencyRequest: + pass + + +@dataclass +class CurrencyDetail: + # The integer value of the currency + id: int + # The name of the currency + symbol: str + # The balance decimals of the currency + balance_decimals: int + # The quantity multiplier of the currency + quantity_multiplier: str + + +@dataclass +class ApiGetCurrencyResponse: + # The list of currencies + result: list[CurrencyDetail] + + +@dataclass +class ApiCandlestickRequest: + """ + Kline/Candlestick bars for an instrument. Klines are uniquely identified by their instrument, type, interval, and open time. + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The interval of each candlestick + interval: CandlestickInterval + # The type of candlestick data to retrieve + type: CandlestickType + # Start time of kline data in unix nanoseconds + start_time: str | None = None + # End time of kline data in unix nanoseconds + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the query from + cursor: str | None = None + + +@dataclass +class ApiCandlestickResponse: + # The candlestick result set for given interval + result: list[Candlestick] + # The cursor to indicate when to start the next query from + next: str | None = None + + +@dataclass +class ApiFundingRateRequest: + """ + Lookup the historical funding rate of a perpetual future. + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # Start time of funding rate in unix nanoseconds + start_time: str | None = None + # End time of funding rate in unix nanoseconds + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the query from + cursor: str | None = None + + +@dataclass +class FundingRate: + # The readable instrument name:
    • Perpetual: `ETH_USDT_Perp`
    • Future: `BTC_USDT_Fut_20Oct23`
    • Call: `ETH_USDT_Call_20Oct23_2800`
    • Put: `ETH_USDT_Put_20Oct23_2800`
    + instrument: str + # The funding rate of the instrument, expressed in percentage points + funding_rate: str + # The funding timestamp of the funding rate, expressed in unix nanoseconds + funding_time: str + # The mark price of the instrument at funding timestamp, expressed in `9` decimals + mark_price: str + # The 8h average funding rate of the instrument, expressed in percentage points + funding_rate_8_h_avg: str + + +@dataclass +class ApiFundingRateResponse: + # The funding rate result set for given interval + result: list[FundingRate] + # The cursor to indicate when to start the next query from + next: str | None = None + + +@dataclass +class ApiGetAllInstrumentsRequest: + # Fetch only active instruments + is_active: bool | None = None + + +@dataclass +class ApiGetAllInstrumentsResponse: + # List of instruments + result: list[Instrument] + + +@dataclass +class ApiQueryVaultManagerInvestorHistoryRequest: + # The unique identifier of the vault to filter by + vault_id: str + # Whether to only return investments made by the manager + only_own_investments: bool + + +@dataclass +class ApiVaultInvestorHistory: + # Time at which the event was emitted in unix nanoseconds + event_time: str + # The off chain account id of the investor, only visible to the manager + off_chain_account_id: str + # The unique identifier of the vault. + vault_id: str + # The type of transaction that occurred. List of types: vaultInvest, vaultBurnLpToken, vaultRedeem + type: VaultInvestorAction + # The price of the vault LP tokens at the time of the event. + price: str + # The amount of Vault LP tokens invested or redeemed. + size: str + # The realized PnL of the vault. + realized_pnl: str + # The performance fee of the vault. + performance_fee: str + + +@dataclass +class ApiQueryVaultManagerInvestorHistoryResponse: + # The list of vault investor history belong to the manager + result: list[ApiVaultInvestorHistory] + + +@dataclass +class OrderLeg: + # The instrument to trade in this leg + instrument: str + # The total number of assets to trade in this leg, expressed in base asset decimal units. + size: str + # Specifies if the order leg is a buy or sell + is_buying_asset: bool + """ + The limit price of the order leg, expressed in `9` decimals. + This is the number of quote currency units to pay/receive for this leg. + This should be `null/0` if the order is a market order + """ + limit_price: str | None = None + + +@dataclass +class TPSLOrderMetadata: + """ + Contains metadata for Take Profit (TP) and Stop Loss (SL) trigger orders. + + ### Fields: + - **triggerBy**: Defines the price type that activates the order (e.g., index price). + - **triggerPrice**: The price at which the order is triggered, expressed in `9` decimal precision. + + + """ + + # Defines the price type that activates a Take Profit (TP) or Stop Loss (SL) order + trigger_by: TriggerBy + # The Trigger Price of the order, expressed in `9` decimals. + trigger_price: str + # If True, the order will close the position when the trigger price is reached + close_position: bool + + +@dataclass +class TriggerOrderMetadata: + """ + Contains metadata related to trigger orders, such as Take Profit (TP) or Stop Loss (SL). + + Trigger orders are used to automatically execute an order when a predefined price condition is met, allowing traders to implement risk management strategies. + + + """ + + # Type of the trigger order. eg: Take Profit, Stop Loss, etc + trigger_type: TriggerType + """ + Contains metadata for Take Profit (TP) and Stop Loss (SL) trigger orders. + + + """ + tpsl: TPSLOrderMetadata + + +@dataclass +class OrderMetadata: + """ + Metadata fields are used to support Backend only operations. These operations are not trustless by nature. + Hence, fields in here are never signed, and is never transmitted to the smart contract. + """ + + """ + A unique identifier for the active order within a subaccount, specified by the client + This is used to identify the order in the client's system + This field can be used for order amendment/cancellation, but has no bearing on the smart contract layer + This field will not be propagated to the smart contract, and should not be signed by the client + This value must be unique for all active orders in a subaccount, or amendment/cancellation will not work as expected + Gravity UI will generate a random clientOrderID for each order in the range [0, 2^63 - 1] + To prevent any conflicts, client machines should generate a random clientOrderID in the range [2^63, 2^64 - 1] + + When GRVT Backend receives an order with an overlapping clientOrderID, we will reject the order with rejectReason set to overlappingClientOrderId + """ + client_order_id: str + # [Filled by GRVT Backend] Time at which the order was received by GRVT in unix nanoseconds + create_time: str | None = None + # Trigger fields are used to support any type of trigger order such as TP/SL + trigger: TriggerOrderMetadata | None = None + # Specifies the broker who brokered the order + broker: BrokerTag | None = None + + +@dataclass +class OrderState: + # The status of the order + status: OrderStatus + # The reason for rejection or cancellation + reject_reason: OrderRejectReason + # The number of assets available for orderbook/RFQ matching. Sorted in same order as Order.Legs + book_size: list[str] + # The total number of assets traded. Sorted in same order as Order.Legs + traded_size: list[str] + # Time at which the order was updated by GRVT, expressed in unix nanoseconds + update_time: str + # The average fill price of the order. Sorted in same order as Order.Legs + avg_fill_price: list[str] + + +@dataclass +class Order: + """ + Order is a typed payload used throughout the GRVT platform to express all orderbook, RFQ, and liquidation orders. + GRVT orders are capable of expressing both single-legged, and multi-legged orders by default. + This increases the learning curve slightly but reduces overall integration load, since the order payload is used across all GRVT trading venues. + Given GRVT's trustless settlement model, the Order payload also carries the signature, required to trade the order on our ZKSync Hyperchain. + + All fields in the Order payload (except `id`, `metadata`, and `state`) are trustlessly enforced on our Hyperchain. + This minimizes the amount of trust users have to offer to GRVT + """ + + # The subaccount initiating the order + sub_account_id: str + """ + Four supported types of orders: GTT, IOC, AON, FOK:
      +
    • PARTIAL EXECUTION = GTT / IOC - allows partial size execution on each leg
    • +
    • FULL EXECUTION = AON / FOK - only allows full size execution on all legs
    • +
    • TAKER ONLY = IOC / FOK - only allows taker orders
    • +
    • MAKER OR TAKER = GTT / AON - allows maker or taker orders
    • +
    Exchange only supports (GTT, IOC, FOK) + RFQ Maker only supports (GTT, AON), RFQ Taker only supports (FOK) + """ + time_in_force: TimeInForce + """ + The legs present in this order + The legs must be sorted by Asset.Instrument/Underlying/Quote/Expiration/StrikePrice + """ + legs: list[OrderLeg] + # The signature approving this order + signature: Signature + # Order Metadata, ignored by the smart contract, and unsigned by the client + metadata: OrderMetadata + # [Filled by GRVT Backend] A unique 128-bit identifier for the order, deterministically generated within the GRVT backend + order_id: str | None = None + """ + If the order is a market order + Market Orders do not have a limit price, and are always executed according to the maker order price. + Market Orders must always be taker orders + """ + is_market: bool | None = None + """ + If True, Order must be a maker order. It has to fill the orderbook instead of match it. + If False, Order can be either a maker or taker order. In this case, order creation is currently subject to a speedbump of 25ms to ensure orders are matched against updated orderbook quotes. + + | | Must Fill All | Can Fill Partial | + | - | - | - | + | Must Be Taker | FOK + False | IOC + False | + | Can Be Either | AON + False | GTC + False | + | Must Be Maker | AON + True | GTC + True | + + """ + post_only: bool | None = None + # If True, Order must reduce the position size, or be cancelled + reduce_only: bool | None = None + # [Filled by GRVT Backend] The current state of the order, ignored by the smart contract, and unsigned by the client + state: OrderState | None = None + + +@dataclass +class ApiCreateOrderRequest: + # The order to create + order: Order + + +@dataclass +class ApiCreateOrderResponse: + # The created order + result: Order + + +@dataclass +class ApiCancelOrderRequest: + # The subaccount ID cancelling the order + sub_account_id: str + # Cancel the order with this `order_id` + order_id: str | None = None + # Cancel the order with this `client_order_id` + client_order_id: str | None = None + """ + Specifies the time-to-live (in milliseconds) for this cancellation. + During this period, any order creation with a matching `client_order_id` will be cancelled and not be added to the GRVT matching engine. + This mechanism helps mitigate time-of-flight issues where cancellations might arrive before the corresponding orders. + Hence, cancellation by `order_id` ignores this field as the exchange can only assign `order_id`s to already-processed order creations. + The duration cannot be negative, is rounded down to the nearest 100ms (e.g., `'670'` -> `'600'`, `'30'` -> `'0'`) and capped at 5 seconds (i.e., `'5000'`). + Value of `'0'` or omission results in the default time-to-live value being applied. + If the caller requests multiple successive cancellations for a given order, such that the time-to-live windows overlap, only the first request will be considered. + + """ + time_to_live_ms: str | None = None + + +@dataclass +class ApiCancelAllOrdersRequest: + # The subaccount ID cancelling all orders + sub_account_id: str + # The kind filter to apply. If nil, this defaults to all kinds. Otherwise, only entries matching the filter will be cancelled + kind: list[Kind] | None = None + # The base filter to apply. If nil, this defaults to all bases. Otherwise, only entries matching the filter will be cancelled + base: list[str] | None = None + # The quote filter to apply. If nil, this defaults to all quotes. Otherwise, only entries matching the filter will be cancelled + quote: list[str] | None = None + + +@dataclass +class ApiOpenOrdersRequest: + # The subaccount ID to filter by + sub_account_id: str + # The kind filter to apply. If nil, this defaults to all kinds. Otherwise, only entries matching the filter will be returned + kind: list[Kind] | None = None + # The base filter to apply. If nil, this defaults to all bases. Otherwise, only entries matching the filter will be returned + base: list[str] | None = None + # The quote filter to apply. If nil, this defaults to all quotes. Otherwise, only entries matching the filter will be returned + quote: list[str] | None = None + + +@dataclass +class ApiOpenOrdersResponse: + # The Open Orders matching the request filter + result: list[Order] + + +@dataclass +class ApiOrderHistoryRequest: + """ + Retrieves the order history for the account. + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The subaccount ID to filter by + sub_account_id: str + # The kind filter to apply. If nil, this defaults to all kinds. Otherwise, only entries matching the filter will be returned + kind: list[Kind] | None = None + # The base filter to apply. If nil, this defaults to all bases. Otherwise, only entries matching the filter will be returned + base: list[str] | None = None + # The quote filter to apply. If nil, this defaults to all quotes. Otherwise, only entries matching the filter will be returned + quote: list[str] | None = None + # The start time to apply in nanoseconds. If nil, this defaults to all start times. Otherwise, only entries matching the filter will be returned + start_time: str | None = None + # The end time to apply in nanoseconds. If nil, this defaults to all end times. Otherwise, only entries matching the filter will be returned + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the query from + cursor: str | None = None + + +@dataclass +class ApiOrderHistoryResponse: + # The Open Orders matching the request filter + result: list[Order] + # The cursor to indicate when to start the query from + next: str + + +@dataclass +class EmptyRequest: + pass + + +@dataclass +class Ack: + # Gravity has acknowledged that the request has been successfully received and it will process it in the backend + ack: bool + + +@dataclass +class AckResponse: + # The Ack Object + result: Ack + + +@dataclass +class ApiGetOrderRequest: + # The subaccount ID to filter by + sub_account_id: str + # Filter for `order_id` + order_id: str | None = None + # Filter for `client_order_id` + client_order_id: str | None = None + + +@dataclass +class ApiGetOrderResponse: + # The order object for the requested filter + result: Order + + +@dataclass +class ApiCancelOnDisconnectRequest: + """ + Auto-Cancel All Open Orders when the countdown time hits zero. + + Market Maker inputs a countdown time parameter in milliseconds (e.g. 120000 for 120s) rounded down to the smallest second follows the following logic: + - Market Maker initially entered a value between 0 -> 1000, which is rounded to 0: will result in termination of their COD + - Market Maker initially entered a value between 1001 -> 300_000, which is rounded to the nearest second: will result in refresh of their COD + - Market Maker initially entered a value bigger than 300_000, which will result in error (upper bound) + Market Maker will send a heartbeat message by calling the endpoint at specific intervals (ex. every 30 seconds) to the server to refresh the count down. + + If the server does not receive a heartbeat message within the countdown time, it will cancel all open orders for the specified Sub Account ID. + """ + + # The subaccount ID cancelling the orders for + sub_account_id: str + """ + Countdown time in milliseconds (ex. 120000 for 120s). + + 0 to disable the timer. + + Does not accept negative values. + + Minimum acceptable value is 1,000. + + Maximum acceptable value is 300,000 + """ + countdown_time: str | None = None + + +@dataclass +class WSOrderFeedSelectorV1: + """ + Subscribes to a feed of order updates pertaining to orders made by your account. + Each Order can be uniquely identified by its `order_id` or `client_order_id`. + To subscribe to all orders, specify an empty `instrument` (eg. `2345123`). + Otherwise, specify the `instrument` to only receive orders for that instrument (eg. `2345123-BTC_USDT_Perp`). + """ + + # The subaccount ID to filter by + sub_account_id: str + # The instrument filter to apply. + instrument: str | None = None + + +@dataclass +class WSOrderFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # The order object being created or updated + feed: Order + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSOrderStateFeedSelectorV1: + """ + Subscribes to a feed of order updates pertaining to orders made by your account. + Unlike the Order Stream, this only streams state updates, drastically improving throughput, and latency. + Each Order can be uniquely identified by its `order_id` or `client_order_id`. + To subscribe to all orders, specify an empty `instrument` (eg. `2345123`). + Otherwise, specify the `instrument` to only receive orders for that instrument (eg. `2345123-BTC_USDT_Perp`). + """ + + # The subaccount ID to filter by + sub_account_id: str + # The instrument filter to apply. + instrument: str | None = None + + +@dataclass +class OrderStateFeed: + # A unique 128-bit identifier for the order, deterministically generated within the GRVT backend + order_id: str + # A unique identifier for the active order within a subaccount, specified by the client + client_order_id: str + # The order state object being created or updated + order_state: OrderState + + +@dataclass +class WSOrderStateFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # The Order State Feed + feed: OrderStateFeed + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSPositionsFeedSelectorV1: + """ + Subscribes to a feed of position updates. + Updates get published when a trade is executed, and when leverage configurations are changed for instruments with ongoing positions. + To subscribe to all positions, specify an empty `instrument` (eg. `2345123`). + Otherwise, specify the `instrument` to only receive positions for that instrument (eg. `2345123-BTC_USDT_Perp`). + """ + + # The subaccount ID to filter by + sub_account_id: str + # The instrument filter to apply. + instrument: str | None = None + + +@dataclass +class WSPositionsFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # A Position being created or updated matching the request filter + feed: Positions + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSFillFeedSelectorV1: + """ + Subscribes to a feed of private trade updates. This happens when a trade is executed. + To subscribe to all private trades, specify an empty `instrument` (eg. `2345123`). + Otherwise, specify the `instrument` to only receive private trades for that instrument (eg. `2345123-BTC_USDT_Perp`). + """ + + # The sub account ID to request for + sub_account_id: str + # The instrument filter to apply. + instrument: str | None = None + + +@dataclass +class WSFillFeedDataV1: + # The websocket channel to which the response is sent + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # A private trade matching the request filter + feed: Fill + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSTransferFeedSelectorV1: + """ + Subscribes to a feed of transfers. This will execute when there is any transfer to or from the selected account. + To subscribe to a main account, specify the account ID (eg. `0x9fe3758b67ce7a2875ee4b452f01a5282d84ed8a`). + To subscribe to a sub account, specify the main account and the sub account dash separated (eg. `0x9fe3758b67ce7a2875ee4b452f01a5282d84ed8a-1920109784202388`). + """ + + # The main account ID to request for + main_account_id: str + # The sub account ID to request for + sub_account_id: str | None = None + + +@dataclass +class TransferHistory: + # The transaction ID of the transfer + tx_id: str + # The account to transfer from + from_account_id: str + # The subaccount to transfer from (0 if transferring from main account) + from_sub_account_id: str + # The account to deposit into + to_account_id: str + # The subaccount to transfer to (0 if transferring to main account) + to_sub_account_id: str + # The token currency to transfer + currency: str + # The number of tokens to transfer + num_tokens: str + # The signature of the transfer + signature: Signature + # The timestamp of the transfer in unix nanoseconds + event_time: str + # The type of transfer + transfer_type: TransferType + # The metadata of the transfer + transfer_metadata: str + + +@dataclass +class WSTransferFeedDataV1: + # The websocket channel to which the response is sent + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # The transfer history matching the requested filters + feed: TransferHistory + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSDepositFeedSelectorV1: + """ + Subscribes to a feed of deposits. This will execute when there is any deposit to selected account. + To subscribe to a main account, specify the account ID (eg. `0x9fe3758b67ce7a2875ee4b452f01a5282d84ed8a`). + """ + + # The main account ID to request for + main_account_id: str + + +@dataclass +class Deposit: + # The hash of the bridgemint event producing the deposit + tx_hash: str + # The account to deposit into + to_account_id: str + # The token currency to deposit + currency: str + # The number of tokens to deposit + num_tokens: str + + +@dataclass +class WSDepositFeedDataV1: + # The websocket channel to which the response is sent + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # The Deposit object + feed: Deposit + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSWithdrawalFeedSelectorV1: + """ + Subscribes to a feed of withdrawals. This will execute when there is any withdrawal from the selected account. + To subscribe to a main account, specify the account ID (eg. `0x9fe3758b67ce7a2875ee4b452f01a5282d84ed8a`). + """ + + # The main account ID to request for + main_account_id: str + + +@dataclass +class Withdrawal: + # The subaccount to withdraw from + from_account_id: str + # The ethereum address to withdraw to + to_eth_address: str + # The token currency to withdraw + currency: str + # The number of tokens to withdraw + num_tokens: str + # The signature of the withdrawal + signature: Signature + + +@dataclass +class WSWithdrawalFeedDataV1: + # The websocket channel to which the response is sent + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # The Withdrawal object + feed: Withdrawal + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class CancelStatusFeed: + # The subaccount ID that requested the cancellation + sub_account_id: str + # A unique identifier for the active order within a subaccount, specified by the client + client_order_id: str + # A unique 128-bit identifier for the order, deterministically generated within the GRVT backend + order_id: str + # The user-provided reason for cancelling the order + reason: OrderRejectReason + # Status of the cancellation attempt + cancel_status: CancelStatus + # [Filled by GRVT Backend] Time at which the cancellation status was updated by GRVT in unix nanoseconds + update_time: str | None = None + + +@dataclass +class WSCancelFeedDataV1: + # Stream name + stream: str + # Primary selector + selector: str + """ + A sequence number used to determine message order within a stream. + - If `useGlobalSequenceNumber` is **false**, this returns the gateway sequence number, which increments by one locally within each stream and resets on gateway restarts. + - If `useGlobalSequenceNumber` is **true**, this returns the global sequence number, which uniquely identifies messages across the cluster. + - A single cluster payload can be multiplexed into multiple stream payloads. + - To distinguish each stream payload, a `dedupCounter` is included. + - The returned sequence number is computed as: `cluster_sequence_number * 10^5 + dedupCounter`. + """ + sequence_number: str + # Data relating to the status of the cancellation attempt + feed: CancelStatusFeed + # The previous sequence number that determines the message order + prev_sequence_number: str + + +@dataclass +class WSCancelFeedSelectorV1: + """ + Subscribes to a feed of time-to-live expiry events for order cancellations requested by a given subaccount. + **This stream presently only provides expiry updates for cancel-order requests set with a valid TTL value**. + Successful order cancellations will reflect as updates published to the [order-state stream](https://api-docs.grvt.io/trading_streams/#order-state). + _A future release will expand the functionality of this stream to provide more general status updates on order cancellation requests._ + Each Order can be uniquely identified by its `client_order_id`. + + """ + + # The subaccount ID to filter by + sub_account_id: str + + +@dataclass +class ApiWithdrawalRequest: + """ + Leverage this API to initialize a withdrawal from GRVT's Hyperchain onto Ethereum. + Do take note that the bridging process does take time. The GRVT UI will help you keep track of bridging progress, and notify you once its complete. + + If not withdrawing the entirety of your balance, there is a minimum withdrawal amount. Currently that amount is ~25 USDT. + Withdrawal fees also apply to cover the cost of the Ethereum transaction. + Note that your funds will always remain in self-custory throughout the withdrawal process. At no stage does GRVT gain control over your funds. + """ + + # The main account to withdraw from + from_account_id: str + # The Ethereum wallet to withdraw into + to_eth_address: str + # The token currency to withdraw + currency: str + # The number of tokens to withdraw, quoted in tokenCurrency decimal units + num_tokens: str + # The signature of the withdrawal + signature: Signature + + +@dataclass +class ApiTransferRequest: + """ + This API allows you to transfer funds in multiple different ways
      +
    • Between SubAccounts within your Main Account
    • +
    • Between your MainAccount and your SubAccounts
    • +
    • To other MainAccounts that you have previously allowlisted
    • +
    Fast Withdrawal Funding Address + For fast withdrawals, funds must be sent to the designated funding account address. Please ensure you use the correct address based on the environment: + Production Environment Address: + [To be updated, not ready yet] + This address should be specified as the to_account_id in your API requests for transferring funds using the transfer API. Ensure accurate input to avoid loss of funds or use the UI. + + """ + + # The main account to transfer from + from_account_id: str + # The subaccount to transfer from (0 if transferring from main account) + from_sub_account_id: str + # The main account to deposit into + to_account_id: str + # The subaccount to transfer to (0 if transferring to main account) + to_sub_account_id: str + # The token currency to transfer + currency: str + # The number of tokens to transfer, quoted in tokenCurrency decimal units + num_tokens: str + # The signature of the transfer + signature: Signature + # The type of transfer + transfer_type: TransferType + # The metadata of the transfer + transfer_metadata: str + + +@dataclass +class ApiTransferAck: + # Gravity has acknowledged that the transfer has been successfully processed. If true, a `tx_id` will be returned. If false, an error will be returned. + ack: bool + # The transaction ID of the transfer. This is only returned if the transfer is successful. + tx_id: str + + +@dataclass +class ApiTransferResponse: + # The Transfer response object + result: ApiTransferAck + + +@dataclass +class ApiDepositHistoryRequest: + """ + The request to get the historical deposits of an account + The history is returned in reverse chronological order + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The token currency to query for, if nil or empty, return all deposits. Otherwise, only entries matching the filter will be returned + currency: list[str] + # The start time to query for in unix nanoseconds + start_time: str | None = None + # The end time to query for in unix nanoseconds + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the next query from + cursor: str | None = None + # Main account ID being queried. By default, applies the requestor's main account ID. + main_account_id: str | None = None + + +@dataclass +class DepositHistory: + # The L1 txHash of the deposit + l_1_hash: str + # The L2 txHash of the deposit + l_2_hash: str + # The account to deposit into + to_account_id: str + # The token currency to deposit + currency: str + # The number of tokens to deposit + num_tokens: str + # The timestamp when the deposit was initiated on L1 in unix nanoseconds + initiated_time: str + # The timestamp when the deposit was confirmed on L2 in unix nanoseconds + confirmed_time: str + # The address of the sender + from_address: str + + +@dataclass +class ApiDepositHistoryResponse: + # The deposit history matching the request account + result: list[DepositHistory] + # The cursor to indicate when to start the next query from + next: str | None = None + + +@dataclass +class ApiTransferHistoryRequest: + """ + The request to get the historical transfers of an account + The history is returned in reverse chronological order + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The token currency to query for, if nil or empty, return all transfers. Otherwise, only entries matching the filter will be returned + currency: list[str] + # The start time to query for in unix nanoseconds + start_time: str | None = None + # The end time to query for in unix nanoseconds + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the next query from + cursor: str | None = None + # The transaction ID to query for + tx_id: str | None = None + # Main account ID being queried. By default, applies the requestor's main account ID. + main_account_id: str | None = None + + +@dataclass +class ApiTransferHistoryResponse: + # The transfer history matching the request account + result: list[TransferHistory] + # The cursor to indicate when to start the next query from + next: str | None = None + + +@dataclass +class ApiWithdrawalHistoryRequest: + """ + The request to get the historical withdrawals of an account + The history is returned in reverse chronological order + + Pagination works as follows:
    • We perform a reverse chronological lookup, starting from `end_time`. If `end_time` is not set, we start from the most recent data.
    • The lookup is limited to `limit` records. If more data is requested, the response will contain a `next` cursor for you to query the next page.
    • If a `cursor` is provided, it will be used to fetch results from that point onwards.
    • Pagination will continue until the `start_time` is reached. If `start_time` is not set, pagination will continue as far back as our data retention policy allows.
    + """ + + # The token currency to query for, if nil or empty, return all withdrawals. Otherwise, only entries matching the filter will be returned + currency: list[str] + # The start time to query for in unix nanoseconds + start_time: str | None = None + # The end time to query for in unix nanoseconds + end_time: str | None = None + # The limit to query for. Defaults to 500; Max 1000 + limit: int | None = None + # The cursor to indicate when to start the next query from + cursor: str | None = None + # Main account ID being queried. By default, applies the requestor's main account ID. + main_account_id: str | None = None + + +@dataclass +class WithdrawalHistory: + # The transaction ID of the withdrawal + tx_id: str + # The subaccount to withdraw from + from_account_id: str + # The ethereum address to withdraw to + to_eth_address: str + # The token currency to withdraw + currency: str + # The number of tokens to withdraw + num_tokens: str + # The signature of the withdrawal + signature: Signature + # The timestamp of the withdrawal in unix nanoseconds + event_time: str + + +@dataclass +class ApiWithdrawalHistoryResponse: + # The withdrawals history matching the request account + result: list[WithdrawalHistory] + # The cursor to indicate when to start the next query from + next: str | None = None + + +@dataclass +class ApiVaultInvestRequest: + """ + Request payload for investing in a vault. + + This API allows a client to invest a specified amount of tokens in a particular vault. + """ + + # The address of the main account initiating the investment. + main_account_id: str + # The unique identifier of the vault to invest in. + vault_id: str + # The currency used for the investment. This should be the vault's quote currency. + currency: str + # The investment sum, in terms of the token currency specified (i.e., `numTokens` of '1000' with `tokenCurrency` of 'USDT' denotes investment of 1,000 USDT). + num_tokens: str + """ + The digital signature from the investing account. + This signature must be generated by the main account ID and is used to verify the authenticity of the request. + The signature must comply with AccountPermExternalTransfer permission. + """ + signature: Signature + + +@dataclass +class ApiVaultRedeemRequest: + """ + Request payload for redeeming from a vault. + + This API allows a client to redeem a specified amount of tokens from a particular vault. + """ + + # The address of the main account initiating the redemption. + main_account_id: str + # The unique identifier of the vault to redeem from. + vault_id: str + # The currency used for the redemption. This should be the vault's quote currency. + currency: str + # The number of shares to redeem. + num_tokens: str + """ + The digital signature from the investing account. + This signature must be generated by the main account ID and is used to verify the authenticity of the request. + The signature must comply with AccountPermExternalTransfer permission. + """ + signature: Signature + + +@dataclass +class ApiVaultRedeemCancelRequest: + """ + Request payload for canceling a vault redemption. + + This API allows a client to cancel a previously initiated redemption from a vault. + """ + + # The address of the main account initiating the cancellation. + main_account_id: str + # The unique identifier of the vault to cancel the redemption from. + vault_id: str + + +@dataclass +class ApiVaultViewRedemptionQueueRequest: + """ + Request payload for a vault manager to view the redemption queue for their vault. + + Fetches the redemption queue for a vault, ordered by descending priority. + + Urgent redemption requests, defined as having been pending >90% of the manager-defined maximum redemption period, have top priority (following insertion order). + + Non-urgent redemption requests are otherwise prioritized by insertion order, unless they are >5x the size of the smallest redemption request. + + E.g., If FIFO ordering (all non-urgent) is 1k -> 50k -> 100k -> 20k -> 10k -> 25k, then priority ordering is 1k -> 10k -> 50k -> 20k -> 100k -> 25k. + + Only displays redemption requests that are eligible for automated redemption, i.e., have been pending for the manager-defined minimum redemption period. + """ + + # The address of the vault manager making the request. + main_account_id: str + # The unique identifier of the vault to fetch the redemption queue for. + vault_id: str + + +@dataclass +class VaultRedemptionRequest: + # [Filled by GRVT Backend] Time at which the redemption request was received by GRVT in unix nanoseconds + request_time: str + # The number of shares to redeem + num_lp_tokens: str + # [Filled by GRVT Backend] Time in unix nanoseconds, beyond which the request will be force-redeemed. + max_redemption_period_timestamp: str + # Age category of this redemption request. + age_category: VaultRedemptionReqAgeCategory + # `true` if this request belongs to the vault manager, omitted otherwise. + is_manager: bool | None = None + + +@dataclass +class ApiVaultViewRedemptionQueueResponse: + """ + Response payload for a vault manager to view the redemption queue for their vault, ordered by descending priority. + + Excludes requests that have not yet aged past the minmimum redemption period. + + Also includes counters for total redemption sizes pending as well as urgent (refer to API integration guide for more detail on redemption request classifications). + + + """ + + # Outstanding vault redemption requests, ordered by descending priority. Excludes requests that have not yet aged past the minmimum redemption period. + redemption_queue: list[VaultRedemptionRequest] + # Number of shares eligible for automated redemption (held in queue for at least the minimum redemption period). + pending_redemption_token_count: str + # Number of shares nearing the maximum redemption period (>= 90% of maximum redemption period). + urgent_redemption_token_count: str + # Amount available for automated redemption request servicing (in USD). + auto_redeemable_balance: str + # Current share price (in USD). + share_price: str + + +@dataclass +class ApiVaultInvestorSummaryRequest: + """ + Request payload for fetching the summary of a vault investor. + + This API allows a client to retrieve the summary of investments in a specific vault. + """ + + # The address of the main account initiating the request. + main_account_id: str + # The unique identifier of the vault to fetch the summary for. + vault_id: str + + +@dataclass +class VaultRedemption: + """ + Vault redemption information. + + This struct contains information about a pending redemption from a vault. + """ + + # The number of LP Tokens requested for redemption. + num_lp_tokens: str + # The valuation (in USD) of the redemption request. + request_valuation: str + # [Filled by GRVT Backend] Time at which the redemption request was received by GRVT in unix nanoseconds + request_time: str + # [Filled by GRVT Backend] Time in unix nanoseconds, beyond which the request will be force-redeemed. + max_redemption_period_timestamp: str + + +@dataclass +class VaultInvestorSummary: + """ + Vault investor summary information. + + This struct contains the summary of investments in a vault. + """ + + # The unique identifier of the vault sub account. + sub_account_id: str + # The number of Vault LP tokens held by the investor. + num_lp_tokens: str + # The average entry price (in USD) of the vault LP tokens. + avg_entry_price: str + # The current price (in USD) of the vault LP tokens. + current_price: str + # The current valuation (in USD) of all held vault LP tokens. + total_equity: str + # The all-time realized PnL (in USD) that the investor has received from the vault. + all_time_realized_pnl: str + # The singleton pending redemption (omitted if none). + pending_redemption: VaultRedemption | None = None + + +@dataclass +class ApiVaultInvestorSummaryResponse: + """ + Response payload for the summary of a vault investor. + + This API provides the summary of investments in a specific vault. + """ + + # The summary of investments in the vault. + vault_investor_summary: list[VaultInvestorSummary] + + +@dataclass +class ApiVaultBurnTokensRequest: + """ + Request payload for burning tokens in a vault. + + This API allows a client to burn a specified amount of tokens in a particular vault. + """ + + # The address of the main account initiating the burn. + main_account_id: str + # The unique identifier of the vault to burn tokens from. + vault_id: str + # The currency used for the burn. This should be the vault's quote currency. + currency: str + # The number of tokens to burn. + num_tokens: str + """ + The digital signature from the investing account. + This signature must be generated by the main account ID and is used to verify the authenticity of the request. + The signature must comply with AccountPermExternalTransfer permission. + """ + signature: Signature diff --git a/docs/grvt/grvt-pysdk-main/tests/__init__.py b/docs/grvt/grvt-pysdk-main/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/__init__.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt.py new file mode 100644 index 0000000..174b809 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt.py @@ -0,0 +1,316 @@ + +import os +import time +import traceback +from datetime import datetime +from decimal import Decimal + +from pysdk.grvt_ccxt import GrvtCcxt +from pysdk.grvt_ccxt_env import GrvtEnv +from pysdk.grvt_ccxt_logging_selector import logger +from pysdk.grvt_ccxt_test_utils import validate_return_values +from pysdk.grvt_ccxt_types import DURATION_SECOND_IN_NSEC, GrvtOrderSide +from pysdk.grvt_ccxt_utils import rand_uint32 + + +def get_open_orders(api: GrvtCcxt) -> list[dict]: + open_orders: list[dict] = api.fetch_open_orders( + symbol="BTC_USDT_Perp", + params={"kind": "PERPETUAL"}, + ) + logger.info(f"open_orders: {open_orders=}") + return open_orders + + +def fetch_order_history(api: GrvtCcxt) -> dict: + order_history: dict = api.fetch_order_history( + params={"kind": "PERPETUAL", "limit": 3}, + ) + logger.info(f"order_history: {order_history=}") + return order_history + +def fetch_funding_history(api: GrvtCcxt) -> dict: + start_date: datetime = datetime.strptime("2025-05-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") + funding_history: dict = api.fetch_funding_rate_history( + symbol="BTC_USDT_Perp", + since=int(start_date.timestamp() * DURATION_SECOND_IN_NSEC), # Convert to nanoseconds + limit=500, + ) + results: list = funding_history.get("result", []) + if results: + logger.info(f"funding_history: START={results[0]}") + logger.info(f"funding_history: END={results[-1]}") + else: + logger.info(f"funding_history: No results found in {funding_history=}") + return funding_history + + +def cancel_orders(api: GrvtCcxt, open_orders: list) -> int: + FN = "cancel_orders" + order_count = 0 + for order_dict in open_orders: + client_order_id = order_dict["metadata"].get("client_order_id") + if client_order_id: + # Cancel + logger.info(f"{FN} cancel order by id:{order_dict['order_id']}") + success = api.cancel_order( + id=order_dict["order_id"], params={"time_to_live_ms": "1000"} + ) + order_count += int(success) + else: + logger.warning(f"{FN} client_order_id not found in {order_dict=}") + return order_count + +def show_derisk_mm_ratios(api: GrvtCcxt, keyword: str) -> None: + """Show the current derisking market making ratios.""" + FN = "show_derisk_mm_ratios" + acc_summary = api.get_account_summary(type="sub-account") + maintenance_margin = acc_summary.get("maintenance_margin") + derisk_margin = acc_summary.get("derisk_margin") + derisk_ratio = acc_summary.get("derisk_to_maintenance_margin_ratio") + logger.info(f"{FN} {keyword} {maintenance_margin=}") + logger.info(f"{FN} {keyword} {derisk_margin=}") + logger.info(f"{FN} {keyword} {derisk_ratio=}") + logger.info(f"sub-account summary:\n{acc_summary}") + +def set_derisk_mm_ratio(api: GrvtCcxt, ratio: str = "1.4") -> None: + """Set the derisking market making ratio.""" + FN = f"set_derisk_mm_ratio {ratio=}" + logger.info(f"{FN} START") + show_derisk_mm_ratios(api, "BEFORE") + api.set_derisk_mm_ratio(ratio) + show_derisk_mm_ratios(api, "AFTER") + + +def cancel_all_orders(api: GrvtCcxt) -> bool: + FN = "cancel_all_orders" + logger.info(f"{FN} START") + cancel_response = api.cancel_all_orders() + logger.info(f"{FN} {cancel_response=}") + return cancel_response + + +def print_instruments(api: GrvtCcxt): + logger.info("print_instruments: START") + if not api.markets: + return + for market in list(api.markets.values())[:3]: + logger.info(f"{market=}") + instrument = market["instrument"] + logger.info(f"fetch_market: {instrument=}, {api.fetch_market(instrument)}") + logger.info(f"fetch_mini_ticker: {instrument=}, {api.fetch_mini_ticker(instrument)}") + logger.info(f"fetch_ticker: {instrument=}, {api.fetch_ticker(instrument)}") + logger.info(f"fetch_order_book {instrument=}, {api.fetch_order_book(instrument, limit=10)}") + logger.info( + f"fetch_recent_trades {instrument=}, {api.fetch_recent_trades(instrument, limit=5)}" + ) + logger.info(f"fetch_trades {instrument=}, {api.fetch_trades(instrument, limit=5)}") + logger.info( + f"fetch_funding_rate_history {instrument=}, " + f"{api.fetch_funding_rate_history(instrument, limit=5)}" + ) + for type in ["TRADE", "MARK", "INDEX", "MID"]: + ohlc = api.fetch_ohlcv( + instrument, timeframe="5m", limit=5, params={"candle_type": type} + ) + logger.info(f"fetch_ohlcv {type} {instrument=}, {ohlc}") + + +def send_order(api: GrvtCcxt, side: GrvtOrderSide, client_order_id: int) -> dict: + price = 94_000 if side == "buy" else 95_000 + send_order_response: dict = api.create_order( + symbol="BTC_USDT_Perp", + order_type="limit", + side=side, + amount=0.01, + price=price, + params={"client_order_id": client_order_id}, + ) + logger.info(f"send order: {send_order_response=} {client_order_id=}") + return send_order_response + + +def send_mkt_order( + api: GrvtCcxt, symbol: str, side: GrvtOrderSide, amount: Decimal, client_order_id: int +) -> dict: + send_order_response: dict = api.create_order( + symbol=symbol, + order_type="market", + side=side, + amount=amount, + params={"client_order_id": client_order_id}, + ) + logger.info(f"send mkt order: {send_order_response=} {client_order_id=}") + return send_order_response + + +# Test scenarios, called by the __main__ test routine +def send_fetch_order(api: GrvtCcxt): + client_order_id = rand_uint32() + _ = send_order(api, side="buy", client_order_id=client_order_id) + time.sleep(0.1) + order_status = api.fetch_order( + id=None, + params={"client_order_id": client_order_id}, + ) + logger.info(f"result of fetch_order: {order_status=}") + + +def check_cancel_check_orders(api: GrvtCcxt): + logger.info("check_cancel_check_orders: START") + open_orders = get_open_orders(api) + if open_orders: + cancel_orders(api, open_orders) + get_open_orders(api) + + +def fetch_my_trades(api: GrvtCcxt): + logger.info("fetch_my_trades: START") + my_trades = api.fetch_my_trades( + symbol="BTC_USDT_Perp", + limit=10, + params={}, + ) + logger.info(f"my_trades: num trades:{len(my_trades)}") + logger.info(f"my_trades: {my_trades=}") + + +def cancel_send_order(api: GrvtCcxt): + FN = "cancel_send_order" + logger.info(f"{FN}: START") + client_order_id: int = rand_uint32() + logger.info(f"{FN} cancel order by {client_order_id=}") + result = api.cancel_order( + params={"client_order_id": client_order_id, "time_to_live_ms": "1000"} + ) + logger.info(f"{FN} cancel_order: {result=}") + order_response = send_mkt_order( + api, + symbol="BTC_USDT_Perp", + side="sell", + amount=Decimal("0.01"), + client_order_id=client_order_id, + ) + if order_response: + time.sleep(0.1) + # Get status + logger.info(f"{FN} fetch_order by {client_order_id=}") + order_status = api.fetch_order(params={"client_order_id": client_order_id}) + logger.info(f"{FN} {order_status=}") + else: + logger.warning(f"{FN}: order_response is None") + + +def send_fetch_mkt_order(api: GrvtCcxt): + FN = "send_fetch_mkt_order" + logger.info(f"{FN}: START") + client_order_id: int = rand_uint32() + order_response = send_mkt_order( + api, + symbol="BTC_USDT_Perp", + side="sell", + amount=Decimal("0.01"), + client_order_id=client_order_id, + ) + if order_response: + time.sleep(0.1) + # Get status + logger.info(f"{FN} fetch_order by {client_order_id=}") + order_status = api.fetch_order(params={"client_order_id": client_order_id}) + logger.info(f"{FN} {order_status=}") + else: + logger.warning(f"{FN}: order_response is None") + + +def print_markets(api: GrvtCcxt): + logger.info("print_markets: START") + if api.markets: + logger.info(f"MARKETS:{len(api.markets)}") + for market in api.markets.values(): + logger.info(f"MARKET:{market}") + + +def fetch_all_markets(api: GrvtCcxt): + logger.info("fetch_all_markets: START") + instruments = api.fetch_all_markets() + logger.info(f"fetch_all_markets: num instruments={len(instruments)}") + + +def print_account_summary(api: GrvtCcxt): + try: + logger.info(f"sub-account summary:\n{api.get_account_summary(type='sub-account')}") + logger.info(f"funding-account summary:\n{api.get_account_summary(type='funding')}") + logger.info(f"aggregated-account summary:\n{api.get_account_summary(type='aggregated')}") + logger.info(f"fetch_balance:\n{api.fetch_balance()}") + except Exception as e: + logger.error(f"account summary failed: {e}") + + +def print_account_history(api: GrvtCcxt): + try: + hist = api.fetch_account_history(params={}) + logger.info(f"account history:\n{hist}") + except Exception as e: + logger.error(f"account history failed: {e}") + + +def print_positions(api: GrvtCcxt): + try: + logger.info(f"positions:\n{api.fetch_positions(symbols=['BTC_USDT_Perp'])}") + except Exception as e: + logger.error(f"positions failed: {e}") + + +def print_description(api: GrvtCcxt): + try: + logger.info(f"print_description: {api.describe()}") + except Exception as e: + logger.error(f"print_description failed: {e}") + + +def test_grvt_ccxt(): + params = { + "api_key": os.getenv("GRVT_API_KEY"), + "trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"), + "private_key": os.getenv("GRVT_PRIVATE_KEY"), + } + env = GrvtEnv(os.getenv("GRVT_ENV", "testnet")) + test_api = GrvtCcxt(env, logger, parameters=params, order_book_ccxt_format=True) + function_list = [ + print_description, + # -------- MARKET related + fetch_all_markets, + print_markets, + print_instruments, + print_account_summary, + print_account_history, + # print_positions, + # -------- TRADE related + # fetch_my_trades, + fetch_order_history, + fetch_funding_history, + # # -------- order related + send_fetch_order, + fetch_my_trades, + print_positions, + check_cancel_check_orders, + cancel_send_order, + send_fetch_mkt_order, + get_open_orders, + send_fetch_order, + get_open_orders, + cancel_all_orders, + get_open_orders, + # Derisk MM ratio + set_derisk_mm_ratio, + ] + for f in function_list: + try: + f(test_api) + except Exception as e: + logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}") + validate_return_values(test_api, "test_results_sync.csv") + + +if __name__ == "__main__": + test_grvt_ccxt() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_pro.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_pro.py new file mode 100644 index 0000000..3c38fec --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_pro.py @@ -0,0 +1,300 @@ +import asyncio +import os +import traceback +from datetime import datetime +from decimal import Decimal + +from pysdk.grvt_ccxt_env import GrvtEnv +from pysdk.grvt_ccxt_logging_selector import logger +from pysdk.grvt_ccxt_pro import GrvtCcxtPro +from pysdk.grvt_ccxt_test_utils import validate_return_values +from pysdk.grvt_ccxt_types import DURATION_SECOND_IN_NSEC, GrvtOrderSide +from pysdk.grvt_ccxt_utils import rand_uint32 + + +# Utility functions , not called directly by the __main__ test routine +async def get_open_orders(api: GrvtCcxtPro) -> list[dict]: + open_orders = await api.fetch_open_orders( + + symbol="BTC_USDT_Perp", + params={"kind": "PERPETUAL"}, + ) + logger.info(f"open_orders: {open_orders=}") + return open_orders + + +async def fetch_order_history(api: GrvtCcxtPro) -> dict: + order_history: dict = await api.fetch_order_history( + params={"kind": "PERPETUAL", "limit": 3}, + ) + logger.info(f"order_history: {order_history=}") + return order_history + +async def fetch_funding_history(api: GrvtCcxtPro) -> dict: + start_date: datetime = datetime.strptime("2025-05-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") + funding_history: dict = await api.fetch_funding_rate_history( + symbol="BTC_USDT_Perp", + since=int(start_date.timestamp() * DURATION_SECOND_IN_NSEC), # Convert to nanoseconds + limit=500, + ) + results: list = funding_history.get("result", []) + if results: + logger.info(f"funding_history: START={results[0]}") + logger.info(f"funding_history: END={results[-1]}") + else: + logger.info(f"funding_history: No results found in {funding_history=}") + return funding_history + + +async def cancel_orders(api: GrvtCcxtPro, open_orders: list) -> int: + FN = "cancel_orders" + logger.info(f"{FN} START") + order_count: int = 0 + for order_dict in open_orders: + client_order_id = order_dict["metadata"].get("client_order_id") + if client_order_id: + # Cancel + logger.info(f"{FN} cancel order by id:{order_dict['order_id']}") + await api.cancel_order(id=order_dict["order_id"]) + order_count += 1 + else: + logger.warning(f"{FN} client_order_id not found in {order_dict=}") + return order_count + +async def show_derisk_mm_ratios(api: GrvtCcxtPro, keyword: str) -> None: + """Show the current derisking market making ratios.""" + FN = "show_derisk_mm_ratios" + acc_summary = await api.get_account_summary(type="sub-account") + maintenance_margin = acc_summary.get("maintenance_margin") + derisk_margin = acc_summary.get("derisk_margin") + derisk_ratio = acc_summary.get("derisk_to_maintenance_margin_ratio") + logger.info(f"{FN} {keyword} {maintenance_margin=}") + logger.info(f"{FN} {keyword} {derisk_margin=}") + logger.info(f"{FN} {keyword} {derisk_ratio=}") + logger.info(f"sub-account summary:\n{acc_summary}") + +async def set_derisk_mm_ratio(api: GrvtCcxtPro, ratio: str = "1.5") -> None: + """Set the derisking market making ratio.""" + FN = f"set_derisk_mm_ratio {ratio=}" + logger.info(f"{FN} START") + await show_derisk_mm_ratios(api, "BEFORE") + await api.set_derisk_mm_ratio(ratio) + await show_derisk_mm_ratios(api, "AFTER") + +async def cancel_all_orders(api: GrvtCcxtPro) -> bool: + FN = "cancel_all_orders" + logger.info(f"{FN} START") + cancel_response = await api.cancel_all_orders() + logger.info(f"{FN} {cancel_response=}") + return cancel_response + + +async def print_instruments(api: GrvtCcxtPro): + logger.info("print_instruments: START") + if not api.markets: + return + for market in list(api.markets.values())[:3]: + logger.info(f"{market=}") + instrument = market["instrument"] + logger.info(f"fetch_mini_ticker: {instrument=}, {await api.fetch_mini_ticker(instrument)}") + logger.info(f"fetch_ticker: {instrument=}, {await api.fetch_ticker(instrument)}") + logger.info( + f"fetch_order_book {instrument=}, {await api.fetch_order_book(instrument, limit=10)}" + ) + logger.info( + f"fetch_recent_trades {instrument=}, " + f"{await api.fetch_recent_trades(instrument, limit=7)}" + ) + logger.info(f"fetch_trades {instrument=}, {await api.fetch_trades(instrument, limit=5)}") + logger.info( + f"fetch_funding_rate_history {instrument=}, " + f"{await api.fetch_funding_rate_history(instrument, limit=5)}" + ) + for type in ["TRADE", "MARK", "INDEX", "MID"]: + ohlc = await api.fetch_ohlcv( + instrument, timeframe="1m", limit=5, params={"candle_type": type} + ) + logger.info(f"fetch_ohlcv {type} {instrument=}, {ohlc}") + + +async def send_order(api: GrvtCcxtPro, side: GrvtOrderSide, client_order_id: int) -> dict: + price = 64_000 if side == "buy" else 65_000 + send_order_response = await api.create_order( + symbol="BTC_USDT_Perp", + order_type="limit", + side=side, + amount=0.01, + price=price, + params={"client_order_id": client_order_id}, + ) + logger.info(f"send order: {send_order_response=} {client_order_id=}") + return send_order_response + + +# Test scenarios, called by the __main__ test routine +async def send_fetch_order(api: GrvtCcxtPro): + client_order_id = rand_uint32() + _ = await send_order(api, side="buy", client_order_id=client_order_id) + order_status = await api.fetch_order( + id=None, + params={"client_order_id": client_order_id}, + ) + logger.info(f"result of fetch_order: {order_status=}") + + +async def send_mkt_order( + api: GrvtCcxtPro, symbol: str, side: GrvtOrderSide, amount: Decimal, client_order_id: int +) -> dict: + send_order_response = await api.create_order( + symbol=symbol, + order_type="market", + side=side, + amount=amount, + params={"client_order_id": client_order_id}, + ) + logger.info(f"send mkt order: {send_order_response=} {client_order_id=}") + return send_order_response + + +async def check_cancel_check_orders(api: GrvtCcxtPro): + logger.info("check_cancel_check_orders: START") + open_orders = await get_open_orders(api) + if open_orders: + await cancel_orders(api, open_orders) + await get_open_orders(api) + + +async def fetch_my_trades(api: GrvtCcxtPro): + logger.info("fetch_my_trades: START") + my_trades = await api.fetch_my_trades( + symbol="BTC_USDT_Perp", + limit=10, + params={}, + ) + logger.info(f"my_trades: num trades:{len(my_trades)}") + logger.info(f"my_trades: {my_trades=}") + + +async def cancel_send_order(api: GrvtCcxtPro): + FN = "cancel_send_order" + logger.info(f"{FN}: START") + client_order_id: int = rand_uint32() + logger.info(f"{FN} cancel order by {client_order_id=}") + result = await api.cancel_order( + params={"client_order_id": client_order_id, "time_to_live_ms": "1000"} + ) + logger.info(f"{FN} cancel_order: {result=}") + order_response = await send_mkt_order( + api, + symbol="BTC_USDT_Perp", + side="sell", + amount=Decimal("0.01"), + client_order_id=client_order_id, + ) + if order_response: + # Get status + logger.info(f"{FN} fetch_order by {client_order_id=}") + order_status = await api.fetch_order(params={"client_order_id": client_order_id}) + logger.info(f"{FN} {order_status=}") + + +async def print_markets(api: GrvtCcxtPro): + logger.info("print_markets: START") + if api.markets: + logger.info(f"MARKETS:{len(api.markets)}") + for market in api.markets.values(): + logger.info(f"MARKET:{market}") + + +async def fetch_all_markets(api: GrvtCcxtPro): + logger.info("fetch_all_markets: START") + instruments = await api.fetch_all_markets() + logger.info(f"fetch_all_markets: num instruments={len(instruments)}") + + +async def print_account_summary(api: GrvtCcxtPro): + try: + logger.info("print_account_summary: START") + logger.info(f"sub-account summary:\n{await api.get_account_summary(type='sub-account')}") + logger.info(f"funding-account summary:\n{await api.get_account_summary(type='funding')}") + logger.info( + f"aggregated-account summary:\n{await api.get_account_summary(type='aggregated')}" + ) + logger.info(f"fetch_balance:\n{await api.fetch_balance()}") + except Exception as e: + logger.error(f"account summary failed: {e}") + + +async def print_account_history(api: GrvtCcxtPro): + try: + hist = await api.fetch_account_history(params={}) + logger.info(f"account history:\n{hist}") + except Exception as e: + logger.error(f"account history failed: {e}") + + +async def print_positions(api: GrvtCcxtPro): + try: + logger.info(f"positions:\n{await api.fetch_positions(symbols=['BTC_USDT_Perp'])}") + except Exception as e: + logger.error(f"positions failed: {e}") + + +async def print_description(api: GrvtCcxtPro): + try: + logger.info(f"print_description: {api.describe()}") + except Exception as e: + logger.error(f"print_description failed: {e}") + + +async def grvt_ccxt_pro(): + params = { + "api_key": os.getenv("GRVT_API_KEY"), + "trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"), + "private_key": os.getenv("GRVT_PRIVATE_KEY"), + } + env = GrvtEnv(os.getenv("GRVT_ENV", "testnet")) + test_api = GrvtCcxtPro(env, logger, parameters=params, order_book_ccxt_format=True) + await test_api.load_markets() + await asyncio.sleep(2) + function_list = [ + print_description, + # -------- MARKET related + fetch_all_markets, + print_markets, + print_instruments, + print_account_summary, + print_account_history, + print_positions, + # Order / Trade history + fetch_my_trades, + fetch_order_history, + fetch_funding_history, + # Trade related + send_fetch_order, + check_cancel_check_orders, + fetch_my_trades, + fetch_order_history, + print_positions, + cancel_send_order, + get_open_orders, + send_fetch_order, + get_open_orders, + cancel_all_orders, + get_open_orders, + set_derisk_mm_ratio, + ] + for f in function_list: + try: + await f(test_api) + except Exception as e: + logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}") + validate_return_values(test_api, "test_results.csv") + + +def test_grvt_ccxt_pro() -> None: + asyncio.run(grvt_ccxt_pro()) + + +if __name__ == "__main__": + test_grvt_ccxt_pro() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault.py new file mode 100644 index 0000000..14299cf --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault.py @@ -0,0 +1,52 @@ +import os +import traceback + +from pysdk.grvt_ccxt import GrvtCcxt +from pysdk.grvt_ccxt_env import GrvtEnv +from pysdk.grvt_ccxt_logging_selector import logger + + +def call_vault_manager_investor_history(api: GrvtCcxt): + FN = "call_vault_manager_investor_history" + logger.info(f"{FN}: START") + try: + history = api.fetch_vault_manager_investor_history() + # Expect dict with result key and list of dicts with history items + # [{'event_time': '1752057360756255849', 'off_chain_account_id': 'ACC:2s**fW', + # 'vault_id': '2002239639', 'type': 'VAULT_REDEEM', 'price': '0.998912', 'size': '1900.0', + # 'realized_pnl': '-0.077452', 'performance_fee': '0.0'}, ...] + logger.info(f"{FN}: {history=}") + except Exception as e: + logger.error(f"{FN} failed: {e}") + + +def call_vault_redemption_queue(api: GrvtCcxt): + FN = "call_vault_redemption_queue" + logger.info(f"{FN}: START") + try: + redemption_queue = api.fetch_vault_redemption_queue() + logger.info(f"{FN}: {redemption_queue=}") + except Exception as e: + logger.error(f"{FN} failed: {e}") + + +def test_grvt_ccxt_vault(): + params = { + "api_key": os.getenv("GRVT_API_KEY"), + "trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"), + "private_key": os.getenv("GRVT_PRIVATE_KEY"), + } + env = GrvtEnv(os.getenv("GRVT_ENV", "testnet")) + test_api = GrvtCcxt(env, logger, parameters=params, order_book_ccxt_format=True) + function_list = [ + call_vault_manager_investor_history, + call_vault_redemption_queue, + ] + for f in function_list: + try: + f(test_api) + except Exception as e: + logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}") + +if __name__ == "__main__": + test_grvt_ccxt_vault() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault_pro.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault_pro.py new file mode 100644 index 0000000..f2e1075 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_vault_pro.py @@ -0,0 +1,60 @@ +import asyncio +import os +import traceback + +from pysdk.grvt_ccxt_env import GrvtEnv +from pysdk.grvt_ccxt_logging_selector import logger +from pysdk.grvt_ccxt_pro import GrvtCcxtPro + + +async def call_vault_manager_investor_history(api: GrvtCcxtPro): + FN = "call_vault_manager_investor_history" + logger.info(f"{FN}: START") + try: + history = await api.fetch_vault_manager_investor_history() + # Expect dict with result key and list of dicts with history items + # [{'event_time': '1752057360756255849', 'off_chain_account_id': 'ACC:2s**fW', + # 'vault_id': '2002239639', 'type': 'VAULT_REDEEM', 'price': '0.998912', 'size': '1900.0', + # 'realized_pnl': '-0.077452', 'performance_fee': '0.0'}, ...] + logger.info(f"{FN}: {history=}") + except Exception as e: + logger.error(f"{FN} failed: {e}") + + +async def call_vault_redemption_queue(api: GrvtCcxtPro): + FN = "call_vault_redemption_queue" + logger.info(f"{FN}: START") + try: + redemption_queue = await api.fetch_vault_redemption_queue() + logger.info(f"{FN}: {redemption_queue=}") + except Exception as e: + logger.error(f"{FN} failed: {e}") + + +async def grvt_ccxt_vault_pro(): + params = { + "api_key": os.getenv("GRVT_API_KEY"), + "trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"), + "private_key": os.getenv("GRVT_PRIVATE_KEY"), + } + env = GrvtEnv(os.getenv("GRVT_ENV", "testnet")) + test_api = GrvtCcxtPro(env, logger, parameters=params, order_book_ccxt_format=True) + await test_api.load_markets() + await asyncio.sleep(2) + function_list = [ + call_vault_manager_investor_history, + call_vault_redemption_queue, + ] + for f in function_list: + try: + await f(test_api) + except Exception as e: + logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}") + + +def test_grvt_ccxt_vault_pro() -> None: + asyncio.run(grvt_ccxt_vault_pro()) + + +if __name__ == "__main__": + test_grvt_ccxt_vault_pro() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_ws.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_ws.py new file mode 100644 index 0000000..a64b7d8 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_ccxt_ws.py @@ -0,0 +1,297 @@ +import asyncio +import os +import signal +import sys +import traceback + +from pysdk.grvt_ccxt_env import GrvtEnv, GrvtWSEndpointType +from pysdk.grvt_ccxt_logging_selector import logger +from pysdk.grvt_ccxt_types import GrvtOrderSide +from pysdk.grvt_ccxt_utils import rand_uint32 +from pysdk.grvt_ccxt_ws import GrvtCcxtWS + + +# Utility functions , not called directly by the __main__ test routine +async def callback_general(message: dict) -> None: + message.get("params", {}).get("channel") + logger.info(f"callback_general(): message:{message}") + + +async def grvt_ws_subscribe(api: GrvtCcxtWS, args_list: dict) -> None: + """Subscribes to Websocket channels/feeds in args list.""" + for stream, (callback, ws_endpoint_type, params) in args_list.items(): + logger.info(f"Subscribing to {stream} {params=}") + await api.subscribe( + stream=stream, + callback=callback, + ws_end_point_type=ws_endpoint_type, + params=params, + ) + await asyncio.sleep(0) + + +async def subscribe(loop) -> GrvtCcxtWS: + """Subscribe to Websocket channels and feeds.""" + params = { + "api_key": os.getenv("GRVT_API_KEY"), + "trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"), + "api_ws_version": os.getenv("GRVT_WS_STREAM_VERSION", "v1"), + } + if os.getenv("GRVT_PRIVATE_KEY"): + params["private_key"] = os.getenv("GRVT_PRIVATE_KEY") + env = GrvtEnv(os.getenv("GRVT_ENV", "testnet")) + + test_api = GrvtCcxtWS(env, loop, logger, parameters=params) + await test_api.initialize() + pub_args_dict = { + # ********* Market Data ********* + "mini.s": ( + callback_general, + None, # use deafult endpoint + {"instrument": "BTC_USDT_Perp"}, + ), + "mini.d": ( + callback_general, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + {"instrument": "BTC_USDT_Perp", "rate": 0}, + ), + "ticker.s": ( + callback_general, + None, # use deafult endpoint + {"instrument": "BTC_USDT_Perp"}, + ), + "ticker.d": ( + callback_general, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + {"instrument": "BTC_USDT_Perp"}, + ), + "book.s": ( + callback_general, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + {"instrument": "BTC_USDT_Perp"}, + ), + "book.d": ( + callback_general, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + {"instrument": "BTC_USDT_Perp"}, + ), + "trade": ( + callback_general, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + {"instrument": "BTC_USDT_Perp"}, + ), + "candle": ( + callback_general, + GrvtWSEndpointType.MARKET_DATA_RPC_FULL, + { + "instrument": "BTC_USDT_Perp", + "interval": "CI_1_M", + "type": "TRADE", + }, + ), + } + prv_args_dict = { + # ********* Trade Data ********* + "position": ( + callback_general, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + {}, + ), + "order": ( + callback_general, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + { + "instrument": "BTC_USDT_Perp", + }, + ), + "cancel": ( + callback_general, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + {}, + ), + "state": ( + callback_general, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + { + "instrument": "BTC_USDT_Perp", + }, + ), + "fill": ( + callback_general, + GrvtWSEndpointType.TRADE_DATA_RPC_FULL, + { + "instrument": "BTC_USDT_Perp", + }, + ), + "deposit": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}), + "transfer": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}), + "withdrawal": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}), + } + try: + if "private_key" in params: + await grvt_ws_subscribe(test_api, {**pub_args_dict, **prv_args_dict}) + else: # not private_key , subscribe to public feeds only + await grvt_ws_subscribe(test_api, pub_args_dict) + except Exception as e: + logger.error(f"Error in grvt_ws_subscribe: {e} {traceback.format_exc()}") + return test_api + + +async def rpc_create_order( + test_api: GrvtCcxtWS, side: GrvtOrderSide, price: str, client_order_id: str = "" +) -> str: + if test_api and test_api._private_key: + # Send order + if not client_order_id: + client_order_id = str(rand_uint32()) + payload = await test_api.rpc_create_order( + symbol="BTC_USDT_Perp", + order_type="limit", + side=side, + amount=0.001, + price=price, + params={ + "client_order_id": client_order_id, + # "time_in_force": "IMMEDIATE_OR_CANCEL", + "time_in_force": "GOOD_TILL_TIME", + }, + ) + logger.info(f"rpc_create_order: {payload=}") + return client_order_id + return "" + + +async def rpc_create_mkt_order( + test_api: GrvtCcxtWS, symbol: str, side: GrvtOrderSide, client_order_id: str = "" +) -> str: + FN = "rpc_create_mkt_order" + if test_api and test_api._private_key: + # Send order + if not client_order_id: + client_order_id = str(rand_uint32()) + payload = await test_api.rpc_create_order( + symbol=symbol, + order_type="market", + side=side, + amount=0.001, + params={ + "client_order_id": client_order_id, + "time_in_force": "GOOD_TILL_TIME", + }, + ) + logger.info(f"{FN}: {payload=}") + return client_order_id + return "" + + +async def rpc_fetch_order(test_api: GrvtCcxtWS, client_order_id: str) -> None: + if test_api and test_api._private_key: + # Send order + payload = await test_api.rpc_fetch_order( + params={ + "client_order_id": client_order_id, + }, + ) + logger.info(f"rpc_fetch_order: {payload=}") + + +async def rpc_fetch_open_orders(test_api: GrvtCcxtWS) -> None: + if test_api and test_api._private_key: + # Send order + payload: dict = await test_api.rpc_fetch_open_orders() + logger.info(f"rpc_fetch_open_orders: {payload=}") + + +async def rpc_cancel_order( + test_api: GrvtCcxtWS, client_order_id: str, time_to_live_ms: str = "" +) -> None: + if test_api and test_api._private_key: + # Send order + params: dict = {"client_order_id": client_order_id} + if time_to_live_ms: + params["time_to_live_ms"] = time_to_live_ms + payload = await test_api.rpc_cancel_order(params=params) + logger.info(f"rpc_cancel_order: {payload=}") + + +async def rpc_cancel_all_orders(test_api: GrvtCcxtWS) -> None: + if test_api and test_api._private_key: + # Send order + payload: dict = await test_api.rpc_cancel_all_orders() + logger.info(f"rpc_cancel_all_orders: {payload=}") + + +async def cancel_send_rpc_order(test_api: GrvtCcxtWS) -> None: + FN = "cancel_send_rpc_order" + """Cancels order then sends an order to be canceled.""" + if test_api and test_api._private_key: + cloid = str(rand_uint32()) + logger.info(f"{FN} {cloid=}") + await rpc_cancel_order(test_api, cloid, time_to_live_ms="0") + await asyncio.sleep(1) + await rpc_cancel_order(test_api, cloid, time_to_live_ms="5000") + await asyncio.sleep(0.01) + await rpc_create_mkt_order( + test_api, symbol="BTC_USDT_Perp", side="buy", client_order_id=cloid + ) + await asyncio.sleep(0.01) + await rpc_fetch_order(test_api, cloid) + + +async def send_check_cancel_rpc_order(test_api: GrvtCcxtWS) -> None: + if test_api and test_api._private_key: + # Send order + cloid = await rpc_create_order(test_api, side="buy", price="60000") + if cloid: + await rpc_fetch_open_orders(test_api) + await rpc_fetch_order(test_api, cloid) + await asyncio.sleep(5) + await rpc_cancel_order(test_api, cloid) + cloid = await rpc_create_order(test_api, side="sell", price="70000") + if cloid: + await rpc_fetch_open_orders(test_api) + await rpc_fetch_order(test_api, cloid) + await asyncio.sleep(5) + await rpc_cancel_all_orders(test_api) + + +async def send_rpc_messages(test_api: GrvtCcxtWS) -> None: + """Sends test RPC messages for send/fetch/cancel orders.""" + await send_check_cancel_rpc_order(test_api) + await cancel_send_rpc_order(test_api) + + +async def shutdown(loop, test_api: GrvtCcxtWS) -> None: + """Clean up resources and stop the bot gracefully.""" + import time + logger.info("Shutting down gracefully...") + if test_api: + for stream, message in test_api._last_message.items(): + logger.info(f"Last message: {stream=} {message=}") + logger.info("Delete GrvtCcxtWS...") + del test_api # Close the websocket connection and session + time.sleep(3) # Allow time for cleanup + await asyncio.sleep(5) # Allow time for cleanup + logger.info("Cancelling all tasks...") + tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task(loop)] + _ = [task.cancel() for task in tasks] + logger.info(f"Cancelling {len(tasks)=}") + await asyncio.gather(*tasks, return_exceptions=True) + logger.info("Shutdown complete.") + sys.exit(0) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + test_api = loop.run_until_complete(subscribe(loop)) + if not test_api: + logger.error("Failed to subscribe to Websocket channels.") + sys.exit(1) + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler( + sig, lambda: asyncio.create_task(shutdown(loop, test_api)) + ) + loop.run_until_complete(asyncio.sleep(5)) + loop.run_until_complete(send_rpc_messages(test_api)) + loop.run_forever() + loop.close() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_async.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_async.py new file mode 100644 index 0000000..574d4d3 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_async.py @@ -0,0 +1,137 @@ +import asyncio + +from pysdk import grvt_raw_types +from pysdk.grvt_raw_async import GrvtRawAsync +from pysdk.grvt_raw_base import GrvtError + +from .test_raw_utils import ( + get_config, + get_test_order, + get_test_transfer, + get_test_withdrawal, +) + + +async def get_all_instruments() -> None: + api = GrvtRawAsync(config=get_config()) + resp = await api.get_all_instruments_v1( + grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True) + ) + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected results to be non-null") + if len(resp.result) == 0: + raise ValueError("Expected results to be non-empty") + + +async def open_orders() -> None: + api = GrvtRawAsync(config=get_config()) + + # Skip test if trading account id is not set + if api.config.trading_account_id is None or api.config.api_key is None: + return None # Skip test if configs are not set + + resp = await api.open_orders_v1( + grvt_raw_types.ApiOpenOrdersRequest( + sub_account_id=str(api.config.trading_account_id), + kind=[grvt_raw_types.Kind.PERPETUAL], + base=["BTC", "ETH"], + quote=["USDT"], + ) + ) + if isinstance(resp, GrvtError): + api.logger.error(f"Received error: {resp}") + return None + if resp.result is None: + raise ValueError("Expected orders to be non-null") + if len(resp.result) == 0: + api.logger.info("Expected orders to be non-empty") + + +async def create_order_with_signing() -> None: + api = GrvtRawAsync(config=get_config()) + + inst_resp = await api.get_all_instruments_v1( + grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True) + ) + if isinstance(inst_resp, GrvtError): + raise ValueError(f"Received error: {inst_resp}") + + order = get_test_order(api, {inst.instrument: inst for inst in inst_resp.result}) + if order is None: + return None # Skip test if configs are not set + resp = await api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order)) + + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected order to be non-null") + + +async def transfer_with_signing_async() -> None: + api = GrvtRawAsync(config=get_config()) + transfer = get_test_transfer(api) + + if transfer is None: + return None # Skip test if configs are not set + + resp = await api.transfer_v1( + grvt_raw_types.ApiTransferRequest( + transfer.from_account_id, + transfer.from_sub_account_id, + transfer.to_account_id, + transfer.to_sub_account_id, + transfer.currency, + transfer.num_tokens, + transfer.signature, + ) + ) + + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected transfer response to be non-null") + + +async def withdrawal_with_signing_async() -> None: + api = GrvtRawAsync(config=get_config()) + withdrawal = get_test_withdrawal(api) + + if withdrawal is None: + return None # Skip test if configs are not set + + resp = await api.withdrawal_v1( + grvt_raw_types.ApiWithdrawalRequest( + withdrawal.from_account_id, + withdrawal.to_eth_address, + withdrawal.currency, + withdrawal.num_tokens, + withdrawal.signature, + ) + ) + + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected withdrawal response to be non-null") + + +def test_get_all_instruments() -> None: + asyncio.run(get_all_instruments()) + + +def test_open_orders() -> None: + asyncio.run(open_orders()) + + +def test_create_order_with_signing() -> None: + asyncio.run(create_order_with_signing()) + + +def test_transfer_with_signing_async() -> None: + asyncio.run(transfer_with_signing_async()) + + +def test_withdrawal_with_signing_async() -> None: + asyncio.run(withdrawal_with_signing_async()) diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing.py new file mode 100644 index 0000000..6482a41 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing.py @@ -0,0 +1,400 @@ +import logging +import traceback +from pprint import pprint + +from eth_account import Account + +from pysdk.grvt_fixed_types import Transfer +from pysdk.grvt_raw_base import GrvtApiConfig +from pysdk.grvt_raw_env import GrvtEnv +from pysdk.grvt_raw_signing import sign_order, sign_transfer +from pysdk.grvt_raw_types import ( + Instrument, + InstrumentSettlementPeriod, + Kind, + Order, + OrderLeg, + OrderMetadata, + Signature, + TimeInForce, + TransferType, +) + +# Setup logger +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def test_sign_order_table(): + private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d" + sub_account_id = "8289849667772468" + expiry = 1730800479321350000 + nonce = 828700936 + + test_cases = [ + { + "name": "test decimal precision 1, 3 decimals", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.013", + limit_price="68900.5", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "want_r": "0xb00512d986a718b15136a8ba23de1c1ec84bbdb9958629cbbe4909bae620bb04", + "want_s": "0x79f706de61c68cc14d7734594b5d8689df2b2a7b25951f9a3f61d799f4327ffc", + "want_v": 28, + "want_error": None, + }, + { + "name": "test decimal precision 2, 9 decimals", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.123123123", + limit_price="68900.777123479", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0", + "want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a", + "want_v": 28, + "want_error": None, + }, + { + "name": "test decimal precision 3, round down", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.1231231234", + limit_price="68900.7771234794", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0", + "want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a", + "want_v": 28, + "want_error": None, + }, + { + "name": "test decimal precision 4, round down", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.1231231239", + limit_price="68900.7771234799", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0", + "want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a", + "want_v": 28, + "want_error": None, + }, + # { + # "name": "no private key", + # "order": Order(), + # "want_error": ValueError("Private key is not set") + # }, + # { + # "name": "decimal precision test", + # "order": Order( + # sub_account_id="123", + # time_in_force=TimeInForce.GOOD_TILL_TIME, + # legs=[ + # OrderLeg( + # instrument="BTC_USDT_Perp", + # size="1.013", + # limit_price="64170.7", + # is_buying_asset=True + # ) + # ], + # signature=Signature( + # expiration=expiry, + # nonce=nonce + # ) + # ), + # "want_error": None + # } + ] + + account = Account.from_key(private_key) + + instruments = { + "BTC_USDT_Perp": Instrument( + instrument="BTC_USDT_Perp", + instrument_hash="0x030501", + base="BTC", + quote="USDT", + kind=Kind.PERPETUAL, + venues=[], + settlement_period=InstrumentSettlementPeriod.DAILY, + tick_size="0.00000001", + min_size="0.00000001", + create_time="123", + base_decimals=9, + quote_decimals=9, + max_position_size="1000000", + ) + } + + for tc in test_cases: + config = GrvtApiConfig( + env=GrvtEnv.TESTNET, + private_key=private_key, + trading_account_id=sub_account_id, + api_key="not-needed", + logger=logger, + ) + + signed_order = sign_order(tc["order"], config, account, instruments) + pprint(signed_order) + + # Verify signature fields are populated + assert signed_order.signature.signer == str(account.address) + + # Compare r, s, v values with expected values + if "want_r" in tc: + assert ( + signed_order.signature.r == tc["want_r"] + ), f"Test '{tc['name']}' failed: r value mismatch" + if "want_s" in tc: + assert ( + signed_order.signature.s == tc["want_s"] + ), f"Test '{tc['name']}' failed: s value mismatch" + if "want_v" in tc: + assert ( + signed_order.signature.v == tc["want_v"] + ), f"Test '{tc['name']}' failed: v value mismatch" + + +def test_sign_transfer_table(): + chainId = 1 + private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d" + main_account_id = "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1" + sub_account_id = "8289849667772468" + expiry = "1730800479321350000" + nonce = 828700936 + + test_cases = [ + { + "name": "Transfer $1 from main account to sub account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id="0", + to_account_id=main_account_id, + to_sub_account_id=sub_account_id, + currency="USDT", + num_tokens="1", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "want_r": "0x21c7d7a8e225cb146c80dc79bbe818f915536817f1343e974cfdbe2bfc952cf1", + "want_s": "0x6de83999555f6236e5a56c86876defe00b4776c428ab5a4d7f997d290baaea10", + "want_v": 28, + "want_error": None, + }, + { + "name": "Transfer $1.5 from main account to sub account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id="0", + to_account_id=main_account_id, + to_sub_account_id=sub_account_id, + currency="USDT", + num_tokens="1.5", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "want_r": "0xe0a9c66d8d11c3a9ae3624e150cbbdf85d542722cac5255cad4e50af5ac1ddcb", + "want_s": "0x06769e5284352ead5735b8b11f9e9510d024bbb889a828889db4cb04132b52aa", + "want_v": 28, + "want_error": None, + }, + { + "name": "Transfer $1 from sub account to main account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id=sub_account_id, + to_account_id=main_account_id, + to_sub_account_id="0", + currency="USDT", + num_tokens="1", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "want_r": "0xc1214ee17dbc14f183297b9dd3f93120b16e633691817ee26045451bc629101c", + "want_s": "0x2de27d226a3d3188742629ab222d430d7989d6ea3e6a86bc259606e371123df3", + "want_v": 27, + "want_error": None, + }, + { + "name": "Transfer $1.5 from sub account to main account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id=sub_account_id, + to_account_id=main_account_id, + to_sub_account_id="0", + currency="USDT", + num_tokens="1.5", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "want_r": "0xb9b80dfd4b0d53e64b6dd1067d7d936c79a8c3966175bcefb2021cc71d08116f", + "want_s": "0x3cbe955c9f56e41f70c658e02df07873e77d347aa5c422943b87fdfd94293ae6", + "want_v": 27, + "want_error": None, + }, + { + "name": "Transfer $1 external", + "transfer": Transfer( + from_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f", + from_sub_account_id="0", + to_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd", + to_sub_account_id="0", + currency="USDT", + num_tokens="1", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "want_r": "0x185ad129ca0de3584fcd91f6df0c25d8065411041db117c50dabd057249a1a43", + "want_s": "0x58b1979c1f8c65970578bc2756f17a0b5c7352c27007811800dcd8966351647d", + "want_v": 28, + "want_error": None, + }, + { + "name": "Transfer $1.5 external revert", + "transfer": Transfer( + from_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd", + from_sub_account_id="0", + to_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f", + to_sub_account_id="0", + currency="USDT", + num_tokens="1.5", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "want_r": "0xbbdd4726fef5cddb6eeb5fac07ed95702673293133d66481777a6a3ee82adc12", + "want_s": "0x0ad3592ea3ebf428b260c56723386383253481bc188d9aeb87d9b21b85069821", + "want_v": 28, + "want_error": None, + }, + ] + + account = Account.from_key(private_key) + config = GrvtApiConfig( + env=GrvtEnv.TESTNET, + private_key=private_key, + trading_account_id=sub_account_id, + api_key="not-needed", + logger=logger, + ) + + for tc in test_cases: + signed = sign_transfer(tc["transfer"], config, account, chainId) + pprint(signed) + + # Verify signature fields are populated + assert signed.signature.signer == str(account.address) + + # Compare r, s, v values with expected values + if "want_r" in tc: + assert ( + signed.signature.r == tc["want_r"] + ), f"Test '{tc['name']}' failed: r value mismatch" + if "want_s" in tc: + assert ( + signed.signature.s == tc["want_s"] + ), f"Test '{tc['name']}' failed: s value mismatch" + if "want_v" in tc: + assert ( + signed.signature.v == tc["want_v"] + ), f"Test '{tc['name']}' failed: v value mismatch" + + +def main(): + functions = [ + test_sign_order_table, + test_sign_transfer_table, + ] + for f in functions: + try: + f() + except Exception as e: + logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}") + + +if __name__ == "__main__": + main() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing_intermediate_steps.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing_intermediate_steps.py new file mode 100644 index 0000000..d25f300 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_signing_intermediate_steps.py @@ -0,0 +1,837 @@ +import json +import logging +import traceback + +from eth_account import Account +from eth_account.messages import encode_typed_data + +from pysdk.grvt_fixed_types import Transfer +from pysdk.grvt_raw_base import GrvtApiConfig +from pysdk.grvt_raw_env import GrvtEnv +from pysdk.grvt_raw_signing import ( + EIP712_ORDER_MESSAGE_TYPE, + EIP712_TRANSFER_MESSAGE_TYPE, + build_EIP712_order_message_data, + build_EIP712_transfer_message_data, + get_EIP712_domain_data, + sign_order, +) +from pysdk.grvt_raw_types import ( + Instrument, + InstrumentSettlementPeriod, + Kind, + Order, + OrderLeg, + OrderMetadata, + Signature, + TimeInForce, + TransferType, +) + +# Setup logger +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def test_sign_order_table(): + # Generated using https://key.tokenpocket.pro/#/?network=ETH + # NOTE: `0x` hexadecimal prefix removed + public_key = "ee2060eECaC18beC7F8F670D751801294911E445" + private_key = "c0663ca94684aead40c41a1cb3a94b68a24296e87245be3a186e882a29a15ee0" + + sub_account_id = "8289849667772468" + expiry = 1730800479321350000 + nonce = 828700936 + + test_cases = [ + { + "name": "test decimal precision 1, 3 decimals", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.013", + limit_price="68900.5", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "expected_message_data_json": """ +{ + "subAccountID": "8289849667772468", + "isMarket": false, + "timeInForce": 1, + "postOnly": false, + "reduceOnly": false, + "legs": [ + { + "assetID": "0x030501", + "contractSize": 1013000000, + "limitPrice": 68900500000000, + "isBuyingContract": false + } + ], + "nonce": 828700936, + "expiration": 1730800479321350000 +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 326 +}""", + # Per EIP-191: https://eips.ethereum.org/EIPS/eip-191 + "expected_signable_message": """ +{ + "version": "01", + "header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16", + "body": "41650c08ab6e720f899307d7c2b4381a10c1301888375cec2dfefd6a583859eb" +}""", + # EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712 + # Pre-image of the signed message's message hash + # encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19"‖ version ‖ domainSeparator ‖ hashStruct(message) + "digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f1641650c08ab6e720f899307d7c2b4381a10c1301888375cec2dfefd6a583859eb", + "expected_signed_message": """ +{ + "message_hash": "03cb7ca7b353969ab2c00ff92fd472f81f59a84d28fa1aa39128176f21062982", + "r": 14615867946748605809126568669694142791729730263440553623296112010401671344650, + "s": 41758084966111141828834491248882149351523023670747687501271185591898818786045, + "v": 28, + "signature": "205049c0db6e38fd88e4e46be81db7bc354520d52ec6268d210fa35b86c4f20a5c523d0ff8e6f12542fdcf34a6e6e2c547986b7c8fa53f86a5e656d9ab44b2fd1c" +}""", + }, + { + "name": "test decimal precision 2, 9 decimals", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.123123123", + limit_price="68900.777123479", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "expected_message_data_json": """ +{ + "subAccountID": "8289849667772468", + "isMarket": false, + "timeInForce": 1, + "postOnly": false, + "reduceOnly": false, + "legs": [ + { + "assetID": "0x030501", + "contractSize": 1123123123, + "limitPrice": 68900777123479, + "isBuyingContract": false + } + ], + "nonce": 828700936, + "expiration": 1730800479321350000 +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 326 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16", + "body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814" +}""", + "digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814", + "expected_signed_message": """ +{ + "message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69", + "r": 30067953785684815030293507105225786115862149795459199007947867478230986262090, + "s": 23299979093064779986156565292425191814752388247725004533191995996670719399433, + "v": 28, + "signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c" +}""", + }, + { + "name": "test decimal precision 3, round down", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.1231231234", + limit_price="68900.7771234794", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "expected_message_data_json": """ +{ + "subAccountID": "8289849667772468", + "isMarket": false, + "timeInForce": 1, + "postOnly": false, + "reduceOnly": false, + "legs": [ + { + "assetID": "0x030501", + "contractSize": 1123123123, + "limitPrice": 68900777123479, + "isBuyingContract": false + } + ], + "nonce": 828700936, + "expiration": 1730800479321350000 +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 326 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16", + "body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814" +}""", + "digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814", + "expected_signed_message": """ +{ + "message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69", + "r": 30067953785684815030293507105225786115862149795459199007947867478230986262090, + "s": 23299979093064779986156565292425191814752388247725004533191995996670719399433, + "v": 28, + "signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c" +}""", + }, + { + "name": "test decimal precision 4, round down", + "order": Order( + metadata=OrderMetadata( + client_order_id="1", create_time="1730800479321350000" + ), + sub_account_id=sub_account_id, + time_in_force=TimeInForce.GOOD_TILL_TIME, + post_only=False, + is_market=False, + reduce_only=False, + legs=[ + OrderLeg( + instrument="BTC_USDT_Perp", + size="1.1231231239", + limit_price="68900.7771234799", + is_buying_asset=False, + ) + ], + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + ), + "expected_message_data_json": """ +{ + "subAccountID": "8289849667772468", + "isMarket": false, + "timeInForce": 1, + "postOnly": false, + "reduceOnly": false, + "legs": [ + { + "assetID": "0x030501", + "contractSize": 1123123123, + "limitPrice": 68900777123479, + "isBuyingContract": false + } + ], + "nonce": 828700936, + "expiration": 1730800479321350000 +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 326 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16", + "body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814" +}""", + "digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814", + "expected_signed_message": """ +{ + "message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69", + "r": 30067953785684815030293507105225786115862149795459199007947867478230986262090, + "s": 23299979093064779986156565292425191814752388247725004533191995996670719399433, + "v": 28, + "signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c" +}""", + }, + ] + + account = Account.from_key(private_key) + + instruments = { + "BTC_USDT_Perp": Instrument( + instrument="BTC_USDT_Perp", + instrument_hash="0x030501", + base="BTC", + quote="USDT", + kind=Kind.PERPETUAL, + venues=[], + settlement_period=InstrumentSettlementPeriod.DAILY, + tick_size="0.00000001", + min_size="0.00000001", + create_time="123", + base_decimals=9, + quote_decimals=9, + max_position_size="1000000", + ) + } + + for tc in test_cases: + config = GrvtApiConfig( + env=GrvtEnv.TESTNET, + private_key=private_key, + trading_account_id=sub_account_id, + api_key="not-needed", + logger=logger, + ) + + # Get intermediate values + message_data = build_EIP712_order_message_data(tc["order"], instruments) + domain_data = get_EIP712_domain_data(config.env, 326) + signable_message = encode_typed_data( + domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data + ) + signed_message = account.sign_message(signable_message) + signed_order = sign_order(tc["order"], config, account, instruments) + + # Convert to comparable strings + message_data_json = json.dumps(message_data, indent=2) + domain_data_json = json.dumps(domain_data, indent=2) + signable_message_json = json.dumps( + { + "version": signable_message.version.hex(), + "header": signable_message.header.hex(), + "body": signable_message.body.hex(), + }, + indent=2, + ) + signed_message_json = json.dumps( + { + "message_hash": signed_message.message_hash.hex(), + "r": signed_message.r, + "s": signed_message.s, + "v": signed_message.v, + "signature": signed_message.signature.hex(), + }, + indent=2, + ) + # EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712 + signed_message_hash_preimage = ( + "0x19" + + signable_message.version.hex() + + signable_message.header.hex() + + signable_message.body.hex() + ) + + # Strip whitespace for comparison + assert ( + message_data_json.strip() == tc["expected_message_data_json"].strip() + ), f""" +Test '{tc['name']}' failed: message_data mismatch. +Wanted: +{tc['expected_message_data_json'].strip()} +Got: +{message_data_json.strip()} +""" + assert ( + domain_data_json.strip() == tc["expected_domain_data_json"].strip() + ), f""" +Test '{tc['name']}' failed: domain_data mismatch. +Wanted: +{tc['expected_domain_data_json'].strip()} +Got: +{domain_data_json.strip()} +""" + assert ( + signable_message_json.strip() == tc["expected_signable_message"].strip() + ), f""" +Test '{tc['name']}' failed: signable_message mismatch. +Wanted: +{tc['expected_signable_message'].strip()} +Got: +{signable_message_json.strip()} +""" + assert ( + signed_message_hash_preimage.strip() == tc["digest_input"].strip() + ), f""" +Test '{tc['name']}' failed: digest_input mismatch. +Wanted: +{tc["digest_input"].strip() +} +Got: +{signed_message_hash_preimage.strip()} +""" + assert ( + signed_message_json.strip() == tc["expected_signed_message"].strip() + ), f""" +Test '{tc['name']}' failed: signed_message mismatch. +Wanted: +{tc['expected_signed_message'].strip()} +Got: +{signed_message_json.strip()} +""" + assert ( + signed_order.signature.signer == str(account.address) == "0x" + public_key + ), f"""Test '{tc['name']}' failed: signer mismatch.""" + + +def test_sign_transfer_table(): + chainId = 1 + private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d" + main_account_id = "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1" + sub_account_id = "8289849667772468" + expiry = "1730800479321350000" + nonce = 828700936 + + test_cases = [ + { + "name": "Transfer $1 from main account to sub account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id="0", + to_account_id=main_account_id, + to_sub_account_id=sub_account_id, + currency="USDT", + num_tokens="1", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "expected_message_data_json": """ +{ + "fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "fromSubAccount": "0", + "toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "toSubAccount": "8289849667772468", + "tokenCurrency": 3, + "numTokens": 1000000, + "nonce": 828700936, + "expiration": "1730800479321350000" +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 1 +}""", + # Per EIP-191: https://eips.ethereum.org/EIPS/eip-191 + "expected_signable_message": """ +{ + "version": "01", + "header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6", + "body": "8dfbfc161ca14b60b318aec118c0f77137ab6d5c0d6f4aa283a75995ec842a9f" +}""", + # EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712 + # Pre-image of the signed message's message hash + # encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19"‖ version ‖ domainSeparator ‖ hashStruct(message) + "digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f68dfbfc161ca14b60b318aec118c0f77137ab6d5c0d6f4aa283a75995ec842a9f", + "expected_signed_message": """ +{ + "message_hash": "f237c6e8ceaf8f75c35537c26a5810f5029fe3fa0179d636cfa75dba0eeaecda", + "r": 15279414997690414521303216846501751476684522786442627331685751803272563600625, + "s": 49712406548009011982925909577001640710775887931482920893646195243522061953552, + "v": 28, + "signature": "21c7d7a8e225cb146c80dc79bbe818f915536817f1343e974cfdbe2bfc952cf16de83999555f6236e5a56c86876defe00b4776c428ab5a4d7f997d290baaea101c" +}""", + }, + { + "name": "Transfer $1.5 from main account to sub account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id="0", + to_account_id=main_account_id, + to_sub_account_id=sub_account_id, + currency="USDT", + num_tokens="1.5", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "expected_message_data_json": """ +{ + "fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "fromSubAccount": "0", + "toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "toSubAccount": "8289849667772468", + "tokenCurrency": 3, + "numTokens": 1500000, + "nonce": 828700936, + "expiration": "1730800479321350000" +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 1 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6", + "body": "efc25031927cbee99a4b8c408d427168218312cb5fcbc2ef0644a25c411e9cd6" +}""", + "digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6efc25031927cbee99a4b8c408d427168218312cb5fcbc2ef0644a25c411e9cd6", + "expected_signed_message": """ +{ + "message_hash": "e9a82979035693538d20cbd16a231379d68b2484f065a64b5ac962a0ef45ed2c", + "r": 101618044735866410136029494650850506089543609286068143785089360195209387957707, + "s": 2923457745704967739422721965786309607758766142290846761836065357300251710122, + "v": 28, + "signature": "e0a9c66d8d11c3a9ae3624e150cbbdf85d542722cac5255cad4e50af5ac1ddcb06769e5284352ead5735b8b11f9e9510d024bbb889a828889db4cb04132b52aa1c" +}""", + }, + { + "name": "Transfer $1 from sub account to main account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id=sub_account_id, + to_account_id=main_account_id, + to_sub_account_id="0", + currency="USDT", + num_tokens="1", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "expected_message_data_json": """ +{ + "fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "fromSubAccount": "8289849667772468", + "toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "toSubAccount": "0", + "tokenCurrency": 3, + "numTokens": 1000000, + "nonce": 828700936, + "expiration": "1730800479321350000" +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 1 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6", + "body": "325bbcdd0af37fd856ab19d5a34f04c98b4b9125fd924a41d149290d85cf5d1a" +}""", + "digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6325bbcdd0af37fd856ab19d5a34f04c98b4b9125fd924a41d149290d85cf5d1a", + "expected_signed_message": """ +{ + "message_hash": "05a4682aaaffda9347f7f577623dc86be873d75680250d94f30e27fb450b0597", + "r": 87355230145152558239319417798390737905701563801639010151017443441731256782876, + "s": 20754249269006714296744776682911604802815023385414018875577784419437276970483, + "v": 27, + "signature": "c1214ee17dbc14f183297b9dd3f93120b16e633691817ee26045451bc629101c2de27d226a3d3188742629ab222d430d7989d6ea3e6a86bc259606e371123df31b" +}""", + }, + { + "name": "Transfer $1.5 from sub account to main account", + "transfer": Transfer( + from_account_id=main_account_id, + from_sub_account_id=sub_account_id, + to_account_id=main_account_id, + to_sub_account_id="0", + currency="USDT", + num_tokens="1.5", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "expected_message_data_json": """ +{ + "fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "fromSubAccount": "8289849667772468", + "toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1", + "toSubAccount": "0", + "tokenCurrency": 3, + "numTokens": 1500000, + "nonce": 828700936, + "expiration": "1730800479321350000" +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 1 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6", + "body": "d20efa4f5ca3d7cd996d0a7be827a788fdea40ad9dc3f7d64f909ec3df2f0f1c" +}""", + "digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6d20efa4f5ca3d7cd996d0a7be827a788fdea40ad9dc3f7d64f909ec3df2f0f1c", + "expected_signed_message": """ +{ + "message_hash": "57de810bfd4c46796b923242eba5fba199c69ec6159fd7f0bae13d20e6b2e32f", + "r": 84003073399296424218543069615592899281416934630850361306323613156742252728687, + "s": 27475502714605040799395938380083883707063662756512201821577577919846841662182, + "v": 27, + "signature": "b9b80dfd4b0d53e64b6dd1067d7d936c79a8c3966175bcefb2021cc71d08116f3cbe955c9f56e41f70c658e02df07873e77d347aa5c422943b87fdfd94293ae61b" +}""", + }, + { + "name": "Transfer $1 external", + "transfer": Transfer( + from_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f", + from_sub_account_id="0", + to_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd", + to_sub_account_id="0", + currency="USDT", + num_tokens="1", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "expected_message_data_json": """ +{ + "fromAccount": "0x922a4874196806460fc63b5bcbff45f94c87f76f", + "fromSubAccount": "0", + "toAccount": "0x6a3434fce60ff567f60d80fb98f2f981e9b081fd", + "toSubAccount": "0", + "tokenCurrency": 3, + "numTokens": 1000000, + "nonce": 828700936, + "expiration": "1730800479321350000" +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 1 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6", + "body": "c92edbd85a6756bbc1d472694218135800c5c34cecd62dfecf8b79696fb30a5a" +}""", + "digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6c92edbd85a6756bbc1d472694218135800c5c34cecd62dfecf8b79696fb30a5a", + "expected_signed_message": """ +{ + "message_hash": "aa540bb37dc69583724239e2ee089f9863760b1f5dd3707f63254f5f9045a36b", + "r": 11015968193451536627767691276906473050663313657726362297424610772811255519811, + "s": 40117308978565698603770991327546519074883337154968913527997965101318785819773, + "v": 28, + "signature": "185ad129ca0de3584fcd91f6df0c25d8065411041db117c50dabd057249a1a4358b1979c1f8c65970578bc2756f17a0b5c7352c27007811800dcd8966351647d1c" +}""", + }, + { + "name": "Transfer $1.5 external revert", + "transfer": Transfer( + from_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd", + from_sub_account_id="0", + to_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f", + to_sub_account_id="0", + currency="USDT", + num_tokens="1.5", + signature=Signature( + signer="", r="", s="", v=0, expiration=expiry, nonce=nonce + ), + transfer_type=TransferType.STANDARD, + transfer_metadata="", + ), + "expected_message_data_json": """ +{ + "fromAccount": "0x6a3434fce60ff567f60d80fb98f2f981e9b081fd", + "fromSubAccount": "0", + "toAccount": "0x922a4874196806460fc63b5bcbff45f94c87f76f", + "toSubAccount": "0", + "tokenCurrency": 3, + "numTokens": 1500000, + "nonce": 828700936, + "expiration": "1730800479321350000" +}""", + "expected_domain_data_json": """ +{ + "name": "GRVT Exchange", + "version": "0", + "chainId": 1 +}""", + "expected_signable_message": """ +{ + "version": "01", + "header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6", + "body": "5660d6d82b43a7bd01c7bee9379478b27e2c7774d9e9aebd69ea163c523f8730" +}""", + "digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f65660d6d82b43a7bd01c7bee9379478b27e2c7774d9e9aebd69ea163c523f8730", + "expected_signed_message": """ +{ + "message_hash": "2a02bf2a7772d74f176d75f987e984dd253837aba1faed4b0a65ba5a3038ce06", + "r": 84973466961705873082056285677608514305288271637256010073726199940890275535890, + "s": 4896548729346283309439956649162331116008267310844279523516289648065258100769, + "v": 28, + "signature": "bbdd4726fef5cddb6eeb5fac07ed95702673293133d66481777a6a3ee82adc120ad3592ea3ebf428b260c56723386383253481bc188d9aeb87d9b21b850698211c" +}""", + }, + ] + + account = Account.from_key(private_key) + config = GrvtApiConfig( + env=GrvtEnv.TESTNET, + private_key=private_key, + trading_account_id=sub_account_id, + api_key="not-needed", + logger=logger, + ) + + for tc in test_cases: + message_data = build_EIP712_transfer_message_data( + tc["transfer"], 3 # assume all test cases are USDT transfers + ) + domain_data = get_EIP712_domain_data(config.env, chainId) + signable_message = encode_typed_data( + domain_data, EIP712_TRANSFER_MESSAGE_TYPE, message_data + ) + signed_message = account.sign_message(signable_message) + + # Convert to comparable strings + message_data_json = json.dumps(message_data, indent=2) + domain_data_json = json.dumps(domain_data, indent=2) + signable_message_json = json.dumps( + { + "version": signable_message.version.hex(), + "header": signable_message.header.hex(), + "body": signable_message.body.hex(), + }, + indent=2, + ) + signed_message_json = json.dumps( + { + "message_hash": signed_message.message_hash.hex(), + "r": signed_message.r, + "s": signed_message.s, + "v": signed_message.v, + "signature": signed_message.signature.hex(), + }, + indent=2, + ) + # EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712 + signed_message_hash_preimage = ( + "0x19" + + signable_message.version.hex() + + signable_message.header.hex() + + signable_message.body.hex() + ) + + # Strip whitespace for comparison + assert ( + message_data_json.strip() == tc["expected_message_data_json"].strip() + ), f""" +Test '{tc['name']}' failed: message_data mismatch. +Wanted: +{tc['expected_message_data_json'].strip()} +Got: +{message_data_json.strip()} +""" + assert ( + domain_data_json.strip() == tc["expected_domain_data_json"].strip() + ), f""" +Test '{tc['name']}' failed: domain_data mismatch. +Wanted: +{tc['expected_domain_data_json'].strip()} +Got: +{domain_data_json.strip()} +""" + assert ( + signable_message_json.strip() == tc["expected_signable_message"].strip() + ), f""" +Test '{tc['name']}' failed: signable_message mismatch. +Wanted: +{tc['expected_signable_message'].strip()} +Got: +{signable_message_json.strip()} +""" + assert ( + signed_message_hash_preimage.strip() == tc["digest_input"].strip() + ), f""" +Test '{tc['name']}' failed: digest_input mismatch. +Wanted: +{tc["digest_input"].strip() +} +Got: +{signed_message_hash_preimage.strip()} +""" + assert ( + signed_message_json.strip() == tc["expected_signed_message"].strip() + ), f""" +Test '{tc['name']}' failed: signed_message mismatch. +Wanted: +{tc['expected_signed_message'].strip()} +Got: +{signed_message_json.strip()} +""" + + +def main(): + functions = [ + test_sign_transfer_table, + ] + for f in functions: + try: + f() + except Exception as e: + logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}") + + +if __name__ == "__main__": + main() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_sync.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_sync.py new file mode 100644 index 0000000..2c8d640 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_grvt_raw_sync.py @@ -0,0 +1,141 @@ +from pysdk import grvt_raw_types +from pysdk.grvt_raw_base import GrvtError +from pysdk.grvt_raw_sync import GrvtRawSync + +from .test_raw_utils import ( + get_config, + get_test_order, + get_test_tpsl_order, + get_test_transfer, + get_test_withdrawal, +) + + +def test_get_all_instruments() -> None: + api = GrvtRawSync(config=get_config()) + resp = api.get_all_instruments_v1( + grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True) + ) + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected results to be non-null") + if len(resp.result) == 0: + raise ValueError("Expected results to be non-empty") + + +def test_open_orders() -> None: + api = GrvtRawSync(config=get_config()) + + # Skip test if trading account id is not set + if api.config.trading_account_id is None or api.config.api_key is None: + return None # Skip test if configs are not set + + resp = api.open_orders_v1( + grvt_raw_types.ApiOpenOrdersRequest( + # sub_account_id=233, Uncomment to test error path with invalid sub account id + sub_account_id=str(api.config.trading_account_id), + kind=[grvt_raw_types.Kind.PERPETUAL], + base=["BTC", "ETH"], + quote=["USDT"], + ) + ) + if isinstance(resp, GrvtError): + api.logger.error(f"Received error: {resp}") + return None + if resp.result is None: + raise ValueError("Expected orders to be non-null") + if len(resp.result) == 0: + api.logger.info("Expected orders to be non-empty") + + +def test_create_order_with_signing() -> None: + api = GrvtRawSync(config=get_config()) + + inst_resp = api.get_all_instruments_v1( + grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True) + ) + if isinstance(inst_resp, GrvtError): + raise ValueError(f"Received error: {inst_resp}") + + order = get_test_order(api, {inst.instrument: inst for inst in inst_resp.result}) + if order is None: + return None # Skip test if configs are not set + resp = api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order)) + + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected order to be non-null") + + +def test_create_tpsl_order_with_signing() -> None: + api = GrvtRawSync(config=get_config()) + + inst_resp = api.get_all_instruments_v1( + grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True) + ) + if isinstance(inst_resp, GrvtError): + raise ValueError(f"Received error: {inst_resp}") + + order = get_test_tpsl_order( + api, {inst.instrument: inst for inst in inst_resp.result} + ) + if order is None: + return None # Skip test if configs are not set + resp = api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order)) + + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected order to be non-null") + + +def test_transfer_with_signing() -> None: + api = GrvtRawSync(config=get_config()) + transfer = get_test_transfer(api) + + if transfer is None: + return None # Skip test if configs are not set + + resp = api.transfer_v1( + grvt_raw_types.ApiTransferRequest( + transfer.from_account_id, + transfer.from_sub_account_id, + transfer.to_account_id, + transfer.to_sub_account_id, + transfer.currency, + transfer.num_tokens, + transfer.signature, + grvt_raw_types.TransferType.STANDARD, + "", + ) + ) + + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected transfer response to be non-null") + + +def test_withdrawal_with_signing() -> None: + api = GrvtRawSync(config=get_config()) + withdrawal = get_test_withdrawal(api) + + if withdrawal is None: + return None # Skip test if configs are not set + + resp = api.withdrawal_v1( + grvt_raw_types.ApiWithdrawalRequest( + withdrawal.from_account_id, + withdrawal.to_eth_address, + withdrawal.currency, + withdrawal.num_tokens, + withdrawal.signature, + ) + ) + + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected withdrawal response to be non-null") diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_raw_utils.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_raw_utils.py new file mode 100644 index 0000000..1087485 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_raw_utils.py @@ -0,0 +1,158 @@ +import logging +import os +import random +import time + +from pysdk import grvt_fixed_types, grvt_raw_types +from pysdk.grvt_raw_base import GrvtApiConfig, GrvtError +from pysdk.grvt_raw_env import GrvtEnv +from pysdk.grvt_raw_signing import sign_order, sign_transfer, sign_withdrawal +from pysdk.grvt_raw_sync import GrvtRawSync + + +def get_config() -> GrvtApiConfig: + logging.basicConfig() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + conf = GrvtApiConfig( + env=GrvtEnv(os.getenv("GRVT_ENV", "testnet")), + trading_account_id=os.getenv("GRVT_SUB_ACCOUNT_ID"), + private_key=os.getenv("GRVT_PRIVATE_KEY"), + api_key=os.getenv("GRVT_API_KEY"), + logger=logger, + ) + logger.debug(conf) + return conf + + +def get_main_account_id(api: GrvtRawSync) -> str: + resp = api.funding_account_summary_v1(grvt_raw_types.EmptyRequest()) + if isinstance(resp, GrvtError): + raise ValueError(f"Received error: {resp}") + if resp.result is None: + raise ValueError("Expected funding_account_summary_v1 response to be non-null") + return resp.result.main_account_id + + +def get_test_order( + api: GrvtRawSync, instruments: dict[str, grvt_raw_types.Instrument] +) -> grvt_raw_types.Order | None: + # Skip test if configs are not set + if ( + api.config.trading_account_id is None + or api.config.private_key is None + or api.config.api_key is None + ): + return None + + order = grvt_raw_types.Order( + sub_account_id=str(api.config.trading_account_id), + time_in_force=grvt_raw_types.TimeInForce.GOOD_TILL_TIME, + legs=[ + grvt_raw_types.OrderLeg( + instrument="BTC_USDT_Perp", + size="1.2", # 1.2 BTC + limit_price="64170.7", # 80,000 USDT + is_buying_asset=True, + ) + ], + signature=grvt_raw_types.Signature( + signer="", # Populated by sign_order + r="", # Populated by sign_order + s="", # Populated by sign_order + v=0, # Populated by sign_order + expiration=str( + time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000 + ), # 20 days + nonce=random.randint(0, 2**32 - 1), + ), + metadata=grvt_raw_types.OrderMetadata( + client_order_id=str(random.randint(0, 2**32 - 1)), + ), + ) + return sign_order(order, api.config, api.account, instruments) + + +def get_test_tpsl_order( + api: GrvtRawSync, instruments: dict[str, grvt_raw_types.Instrument] +) -> grvt_raw_types.Order | None: + order = get_test_order(api, instruments) + if order: + order.metadata.trigger = grvt_raw_types.TriggerOrderMetadata( + trigger_type=grvt_raw_types.TriggerType.TAKE_PROFIT, + tpsl=grvt_raw_types.TPSLOrderMetadata( + trigger_by=grvt_raw_types.TriggerBy.LAST, + trigger_price="64000", + ), + ) + return order + + +def get_test_transfer(api: GrvtRawSync) -> grvt_fixed_types.Transfer | None: + # Skip test if configs are not set + if ( + api.config.trading_account_id is None + or api.config.private_key is None + or api.config.api_key is None + ): + return None + + funding_account_address = get_main_account_id(api) + + return sign_transfer( + grvt_fixed_types.Transfer( + from_account_id=funding_account_address, + from_sub_account_id="0", + to_account_id=funding_account_address, + to_sub_account_id=str(api.config.trading_account_id), + currency="USDT", + num_tokens="1", + signature=grvt_raw_types.Signature( + signer="", + r="", + s="", + v=0, + expiration=str( + time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000 + ), # 20 days + nonce=random.randint(0, 2**32 - 1), + ), + transfer_type=grvt_fixed_types.TransferType.STANDARD, + transfer_metadata="", + ), + api.config, + api.account, + ) + + +def get_test_withdrawal(api: GrvtRawSync) -> grvt_raw_types.Withdrawal | None: + # Skip test if configs are not set + if ( + api.config.trading_account_id is None + or api.config.private_key is None + or api.config.api_key is None + ): + return None + + funding_account_address = get_main_account_id(api) + + return sign_withdrawal( + grvt_raw_types.Withdrawal( + from_account_id=funding_account_address, + to_eth_address="0xed3FF6F4E84a64556e8F7d149dC3533f0c7D9c49", # Just a test address + currency="USDT", + num_tokens="1", + signature=grvt_raw_types.Signature( + signer="", + r="", + s="", + v=0, + expiration=str( + time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000 + ), # 20 days + nonce=random.randint(0, 2**32 - 1), + ), + ), + api.config, + api.account, + ) diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_transfer.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_transfer.py new file mode 100644 index 0000000..35980ac --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_transfer.py @@ -0,0 +1,4 @@ +from .test_grvt_raw_sync import test_transfer_with_signing + +if __name__ == "__main__": + test_transfer_with_signing() diff --git a/docs/grvt/grvt-pysdk-main/tests/pysdk/test_withdrawal.py b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_withdrawal.py new file mode 100644 index 0000000..faa002f --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/tests/pysdk/test_withdrawal.py @@ -0,0 +1,4 @@ +from .test_grvt_raw_sync import test_withdrawal_with_signing + +if __name__ == "__main__": + test_withdrawal_with_signing() diff --git a/docs/grvt/grvt-pysdk-main/uv.lock b/docs/grvt/grvt-pysdk-main/uv.lock new file mode 100644 index 0000000..e340bf1 --- /dev/null +++ b/docs/grvt/grvt-pysdk-main/uv.lock @@ -0,0 +1,1961 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/07/508f9ebba367fc3370162e53a3cfd12f5652ad79f0e0bfdf9f9847c6f159/aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0", size = 21726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/4c/03fb05f56551828ec67ceb3665e5dc51638042d204983a03b0a1541475b6/aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1", size = 14543 }, +] + +[[package]] +name = "aiohttp" +version = "3.11.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/4b/952d49c73084fb790cb5c6ead50848c8e96b4980ad806cf4d2ad341eaa03/aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0", size = 7673175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/42/3880e133590820aa7bc6d068eb7d8e0ad9fdce9b4663f92b821d3f6b5601/aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f", size = 708721 }, + { url = "https://files.pythonhosted.org/packages/d8/8c/04869803bed108b25afad75f94c651b287851843caacbec6677d8f2d572b/aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854", size = 468596 }, + { url = "https://files.pythonhosted.org/packages/4f/f4/9074011f0d1335b161c953fb32545b6667cf24465e1932b9767874995c7e/aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957", size = 455758 }, + { url = "https://files.pythonhosted.org/packages/fd/68/06298c57ef8f534065930b805e6dbd83613f0534447922782fb9920fce28/aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42", size = 1584797 }, + { url = "https://files.pythonhosted.org/packages/bd/1e/cee6b51fcb3b1c4185a7dc62b3113bc136fae07f39386c88c90b7f79f199/aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55", size = 1632535 }, + { url = "https://files.pythonhosted.org/packages/71/1f/42424462b7a09da362e1711090db9f8d68a37a33f0aab51307335517c599/aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb", size = 1668484 }, + { url = "https://files.pythonhosted.org/packages/f6/79/0e25542bbe3c2bfd7a12c7a49c7bce73b09a836f65079e4b77bc2bafc89e/aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae", size = 1589708 }, + { url = "https://files.pythonhosted.org/packages/d1/13/93ae26b75e23f7d3a613872e472fae836ca100dc5bde5936ebc93ada8890/aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7", size = 1544752 }, + { url = "https://files.pythonhosted.org/packages/cf/5e/48847fad1b014ef92ef18ea1339a3b58eb81d3bc717b94c3627f5d2a42c5/aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788", size = 1529417 }, + { url = "https://files.pythonhosted.org/packages/ae/56/fbd4ea019303f4877f0e0b8c9de92e9db24338e7545570d3f275f3c74c53/aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e", size = 1557808 }, + { url = "https://files.pythonhosted.org/packages/f1/43/112189cf6b3c482ecdd6819b420eaa0c2033426f28d741bb7f19db5dd2bb/aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5", size = 1536765 }, + { url = "https://files.pythonhosted.org/packages/30/12/59986547de8306e06c7b30e547ccda02d29636e152366caba2dd8627bfe1/aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb", size = 1607621 }, + { url = "https://files.pythonhosted.org/packages/aa/9b/af3b323b20df3318ed20d701d8242e523d59c842ca93f23134b05c9d5054/aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf", size = 1628977 }, + { url = "https://files.pythonhosted.org/packages/36/62/adf5a331a7bda475cc326dde393fa2bc5849060b1b37ac3d1bee1953f2cd/aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff", size = 1564455 }, + { url = "https://files.pythonhosted.org/packages/90/c4/4a24291f22f111a854dfdb54dc94d4e0a5229ccbb7bc7f0bed972aa50410/aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d", size = 416768 }, + { url = "https://files.pythonhosted.org/packages/51/69/5221c8006acb7bb10d9e8e2238fb216571bddc2e00a8d95bcfbe2f579c57/aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5", size = 442170 }, + { url = "https://files.pythonhosted.org/packages/9c/38/35311e70196b6a63cfa033a7f741f800aa8a93f57442991cbe51da2394e7/aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb", size = 708797 }, + { url = "https://files.pythonhosted.org/packages/44/3e/46c656e68cbfc4f3fc7cb5d2ba4da6e91607fe83428208028156688f6201/aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9", size = 468669 }, + { url = "https://files.pythonhosted.org/packages/a0/d6/2088fb4fd1e3ac2bfb24bc172223babaa7cdbb2784d33c75ec09e66f62f8/aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933", size = 455739 }, + { url = "https://files.pythonhosted.org/packages/e7/dc/c443a6954a56f4a58b5efbfdf23cc6f3f0235e3424faf5a0c56264d5c7bb/aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1", size = 1685858 }, + { url = "https://files.pythonhosted.org/packages/25/67/2d5b3aaade1d5d01c3b109aa76e3aa9630531252cda10aa02fb99b0b11a1/aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94", size = 1743829 }, + { url = "https://files.pythonhosted.org/packages/90/9b/9728fe9a3e1b8521198455d027b0b4035522be18f504b24c5d38d59e7278/aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6", size = 1785587 }, + { url = "https://files.pythonhosted.org/packages/ce/cf/28fbb43d4ebc1b4458374a3c7b6db3b556a90e358e9bbcfe6d9339c1e2b6/aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5", size = 1675319 }, + { url = "https://files.pythonhosted.org/packages/e5/d2/006c459c11218cabaa7bca401f965c9cc828efbdea7e1615d4644eaf23f7/aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204", size = 1619982 }, + { url = "https://files.pythonhosted.org/packages/9d/83/ca425891ebd37bee5d837110f7fddc4d808a7c6c126a7d1b5c3ad72fc6ba/aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58", size = 1654176 }, + { url = "https://files.pythonhosted.org/packages/25/df/047b1ce88514a1b4915d252513640184b63624e7914e41d846668b8edbda/aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef", size = 1660198 }, + { url = "https://files.pythonhosted.org/packages/d3/cc/6ecb8e343f0902528620b9dbd567028a936d5489bebd7dbb0dd0914f4fdb/aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420", size = 1650186 }, + { url = "https://files.pythonhosted.org/packages/f8/f8/453df6dd69256ca8c06c53fc8803c9056e2b0b16509b070f9a3b4bdefd6c/aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df", size = 1733063 }, + { url = "https://files.pythonhosted.org/packages/55/f8/540160787ff3000391de0e5d0d1d33be4c7972f933c21991e2ea105b2d5e/aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804", size = 1755306 }, + { url = "https://files.pythonhosted.org/packages/30/7d/49f3bfdfefd741576157f8f91caa9ff61a6f3d620ca6339268327518221b/aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b", size = 1692909 }, + { url = "https://files.pythonhosted.org/packages/40/9c/8ce00afd6f6112ce9a2309dc490fea376ae824708b94b7b5ea9cba979d1d/aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16", size = 416584 }, + { url = "https://files.pythonhosted.org/packages/35/97/4d3c5f562f15830de472eb10a7a222655d750839943e0e6d915ef7e26114/aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6", size = 442674 }, + { url = "https://files.pythonhosted.org/packages/4d/d0/94346961acb476569fca9a644cc6f9a02f97ef75961a6b8d2b35279b8d1f/aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250", size = 704837 }, + { url = "https://files.pythonhosted.org/packages/a9/af/05c503f1cc8f97621f199ef4b8db65fb88b8bc74a26ab2adb74789507ad3/aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1", size = 464218 }, + { url = "https://files.pythonhosted.org/packages/f2/48/b9949eb645b9bd699153a2ec48751b985e352ab3fed9d98c8115de305508/aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c", size = 456166 }, + { url = "https://files.pythonhosted.org/packages/14/fb/980981807baecb6f54bdd38beb1bd271d9a3a786e19a978871584d026dcf/aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df", size = 1682528 }, + { url = "https://files.pythonhosted.org/packages/90/cb/77b1445e0a716914e6197b0698b7a3640590da6c692437920c586764d05b/aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259", size = 1737154 }, + { url = "https://files.pythonhosted.org/packages/ff/24/d6fb1f4cede9ccbe98e4def6f3ed1e1efcb658871bbf29f4863ec646bf38/aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d", size = 1793435 }, + { url = "https://files.pythonhosted.org/packages/17/e2/9f744cee0861af673dc271a3351f59ebd5415928e20080ab85be25641471/aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e", size = 1692010 }, + { url = "https://files.pythonhosted.org/packages/90/c4/4a1235c1df544223eb57ba553ce03bc706bdd065e53918767f7fa1ff99e0/aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0", size = 1619481 }, + { url = "https://files.pythonhosted.org/packages/60/70/cf12d402a94a33abda86dd136eb749b14c8eb9fec1e16adc310e25b20033/aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0", size = 1641578 }, + { url = "https://files.pythonhosted.org/packages/1b/25/7211973fda1f5e833fcfd98ccb7f9ce4fbfc0074e3e70c0157a751d00db8/aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9", size = 1684463 }, + { url = "https://files.pythonhosted.org/packages/93/60/b5905b4d0693f6018b26afa9f2221fefc0dcbd3773fe2dff1a20fb5727f1/aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f", size = 1646691 }, + { url = "https://files.pythonhosted.org/packages/b4/fc/ba1b14d6fdcd38df0b7c04640794b3683e949ea10937c8a58c14d697e93f/aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9", size = 1702269 }, + { url = "https://files.pythonhosted.org/packages/5e/39/18c13c6f658b2ba9cc1e0c6fb2d02f98fd653ad2addcdf938193d51a9c53/aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef", size = 1734782 }, + { url = "https://files.pythonhosted.org/packages/9f/d2/ccc190023020e342419b265861877cd8ffb75bec37b7ddd8521dd2c6deb8/aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9", size = 1694740 }, + { url = "https://files.pythonhosted.org/packages/3f/54/186805bcada64ea90ea909311ffedcd74369bfc6e880d39d2473314daa36/aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a", size = 411530 }, + { url = "https://files.pythonhosted.org/packages/3d/63/5eca549d34d141bcd9de50d4e59b913f3641559460c739d5e215693cb54a/aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802", size = 437860 }, + { url = "https://files.pythonhosted.org/packages/c3/9b/cea185d4b543ae08ee478373e16653722c19fcda10d2d0646f300ce10791/aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9", size = 698148 }, + { url = "https://files.pythonhosted.org/packages/91/5c/80d47fe7749fde584d1404a68ade29bcd7e58db8fa11fa38e8d90d77e447/aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c", size = 460831 }, + { url = "https://files.pythonhosted.org/packages/8e/f9/de568f8a8ca6b061d157c50272620c53168d6e3eeddae78dbb0f7db981eb/aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0", size = 453122 }, + { url = "https://files.pythonhosted.org/packages/8b/fd/b775970a047543bbc1d0f66725ba72acef788028fce215dc959fd15a8200/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2", size = 1665336 }, + { url = "https://files.pythonhosted.org/packages/82/9b/aff01d4f9716245a1b2965f02044e4474fadd2bcfe63cf249ca788541886/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1", size = 1718111 }, + { url = "https://files.pythonhosted.org/packages/e0/a9/166fd2d8b2cc64f08104aa614fad30eee506b563154081bf88ce729bc665/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7", size = 1775293 }, + { url = "https://files.pythonhosted.org/packages/13/c5/0d3c89bd9e36288f10dc246f42518ce8e1c333f27636ac78df091c86bb4a/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e", size = 1677338 }, + { url = "https://files.pythonhosted.org/packages/72/b2/017db2833ef537be284f64ead78725984db8a39276c1a9a07c5c7526e238/aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed", size = 1603365 }, + { url = "https://files.pythonhosted.org/packages/fc/72/b66c96a106ec7e791e29988c222141dd1219d7793ffb01e72245399e08d2/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484", size = 1618464 }, + { url = "https://files.pythonhosted.org/packages/3f/50/e68a40f267b46a603bab569d48d57f23508801614e05b3369898c5b2910a/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65", size = 1657827 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/aafbcdb1773d0ba7c20793ebeedfaba1f3f7462f6fc251f24983ed738aa7/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb", size = 1616700 }, + { url = "https://files.pythonhosted.org/packages/b0/5e/6cd9724a2932f36e2a6b742436a36d64784322cfb3406ca773f903bb9a70/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00", size = 1685643 }, + { url = "https://files.pythonhosted.org/packages/8b/38/ea6c91d5c767fd45a18151675a07c710ca018b30aa876a9f35b32fa59761/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a", size = 1715487 }, + { url = "https://files.pythonhosted.org/packages/8e/24/e9edbcb7d1d93c02e055490348df6f955d675e85a028c33babdcaeda0853/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce", size = 1672948 }, + { url = "https://files.pythonhosted.org/packages/25/be/0b1fb737268e003198f25c3a68c2135e76e4754bf399a879b27bd508a003/aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f", size = 410396 }, + { url = "https://files.pythonhosted.org/packages/68/fd/677def96a75057b0a26446b62f8fbb084435b20a7d270c99539c26573bfd/aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287", size = 436234 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "attrs" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "backports-weakref" +version = "1.0.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/ab/cf35cf43a4a6215e3255cf2e49c77d5ba1e9c733af2aa3ec1ca9c4d02592/backports.weakref-1.0.post1.tar.gz", hash = "sha256:bc4170a29915f8b22c9e7c4939701859650f2eb84184aee80da329ac0b9825c2", size = 10574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ec/f598b633c3d5ffe267aaada57d961c94fdfa183c5c3ebda2b6d151943db6/backports.weakref-1.0.post1-py2.py3-none-any.whl", hash = "sha256:81bc9b51c0abc58edc76aefbbc68c62a787918ffe943a37947e162c3f8e19e82", size = 5237 }, +] + +[[package]] +name = "bandit" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e2/c229cdb4eefc124e5b77ac2557eb0a3cb5b9fc89bc465dd2b8dc1033dbb8/bandit-1.8.2.tar.gz", hash = "sha256:e00ad5a6bc676c0954669fe13818024d66b70e42cf5adb971480cf3b671e835f", size = 4228832 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/c1/991a7a1404626558cc7db0cc34243e13e5e336eba053bf6979e9fd6006f7/bandit-1.8.2-py3-none-any.whl", hash = "sha256:df6146ad73dd30e8cbda4e29689ddda48364e36ff655dbfc86998401fcf1721f", size = 127049 }, +] + +[[package]] +name = "bitarray" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/62/dcfac53d22ef7e904ed10a8e710a36391d2d6753c34c869b51bfc5e4ad54/bitarray-3.0.0.tar.gz", hash = "sha256:a2083dc20f0d828a7cdf7a16b20dae56aab0f43dc4f347a3b3039f6577992b03", size = 126627 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/f7/2cd02557fa9f177d54b51e6d668696266bdc1af978b5c27179449cbf5870/bitarray-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ddbf71a97ad1d6252e6e93d2d703b624d0a5b77c153b12f9ea87d83e1250e0c", size = 172224 }, + { url = "https://files.pythonhosted.org/packages/49/0a/0362089c127f2639041171803f6bf193a9e1deba72df637ebd36cb510f46/bitarray-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0e7f24a0b01e6e6a0191c50b06ca8edfdec1988d9d2b264d669d2487f4f4680", size = 123359 }, + { url = "https://files.pythonhosted.org/packages/c7/ab/a0982708b5ad92d6ec40833846ac954b57b16d9f90551a9da59e4bce79e1/bitarray-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:150b7b29c36d9f1a24779aea723fdfc73d1c1c161dc0ea14990da27d4e947092", size = 121267 }, + { url = "https://files.pythonhosted.org/packages/10/23/134ad08b9e7be3b80575fd4a50c33c79b3b360794dfc2716b6a18bf4dd60/bitarray-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8330912be6cb8e2fbfe8eb69f82dee139d605730cadf8d50882103af9ac83bb4", size = 278114 }, + { url = "https://files.pythonhosted.org/packages/c1/a1/df7d0b415207de7c6210403865a5d8a9c920209d542f552a09a02749038a/bitarray-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e56ba8be5f17dee0ffa6d6ce85251e062ded2faa3cbd2558659c671e6c3bf96d", size = 292725 }, + { url = "https://files.pythonhosted.org/packages/ec/06/03a636ac237c1860e63f037ccff35f0fec45188c94e55d9df526ee80adc3/bitarray-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd94b4803811c738e504a4b499fb2f848b2f7412d71e6b517508217c1d7929d", size = 294722 }, + { url = "https://files.pythonhosted.org/packages/17/33/c2a7cb6f0030ea94408c84c4f80f4065b54b2bf1d4080e36fcd0b4c587a2/bitarray-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0255bd05ec7165e512c115423a5255a3f301417973d20a80fc5bfc3f3640bcb", size = 278304 }, + { url = "https://files.pythonhosted.org/packages/2c/a3/a06f76dd55d5337ff55585059058761c148da6d1e9e2bc0469d881ba5eb8/bitarray-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe606e728842389943a939258809dc5db2de831b1d2e0118515059e87f7bbc1a", size = 268807 }, + { url = "https://files.pythonhosted.org/packages/29/48/e8157c422542c308d6a0f5b213b21fef5779c80c00e673e2e4d7b3854d60/bitarray-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e89ea59a3ed86a6eb150d016ed28b1bedf892802d0ed32b5659d3199440f3ced", size = 272791 }, + { url = "https://files.pythonhosted.org/packages/ad/53/219d82592b150b580fbc479a718a7c31086627ed4599c9930408f43b6de4/bitarray-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf0cc2e91dd38122dec2e6541efa99aafb0a62e118179218181eff720b4b8153", size = 264821 }, + { url = "https://files.pythonhosted.org/packages/95/08/c47b24fbb34a305531d8d0d7c15f5ab9788478384583a2614b07c2449cf8/bitarray-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2d9fe3ee51afeb909b68f97e14c6539ace3f4faa99b21012e610bbe7315c388d", size = 287871 }, + { url = "https://files.pythonhosted.org/packages/77/31/5cdf3dcf407e8fcc5ca07a06f45aaf6b0adb15f38fe6fddd03d5d1fbaf9f/bitarray-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:37be5482b9df3105bad00fdf7dc65244e449b130867c3879c9db1db7d72e508b", size = 299459 }, + { url = "https://files.pythonhosted.org/packages/b5/26/15b3630dc9bed79fc0e4a5dc92f4b1d30a872ff92f20a8b7acbb7a484bfd/bitarray-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0027b8f3bb2bba914c79115e96a59b9924aafa1a578223a7c4f0a7242d349842", size = 271131 }, + { url = "https://files.pythonhosted.org/packages/45/4b/1c8ba97a015d9cf44b54d4488a0005c2e9fb33ff1df38fdcf68d1cb76785/bitarray-3.0.0-cp310-cp310-win32.whl", hash = "sha256:628f93e9c2c23930bd1cfe21c634d6c84ec30f45f23e69aefe1fcd262186d7bb", size = 114230 }, + { url = "https://files.pythonhosted.org/packages/84/54/d883073137d5c245555f66c48f9518c855704b4c619aa92ddd74d6eb2c98/bitarray-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0b655c3110e315219e266b2732609fddb0857bc69593de29f3c2ba74b7d3f51a", size = 121439 }, + { url = "https://files.pythonhosted.org/packages/61/41/321edc0fbf7e8c88552d5ff9ee07777d58e2078f2706c6478bc6651b1945/bitarray-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:44c3e78b60070389b824d5a654afa1c893df723153c81904088d4922c3cfb6ac", size = 172452 }, + { url = "https://files.pythonhosted.org/packages/48/92/4c312d6d55ac30dae96749830c9f5007a914efcb591ee0828914078eec9f/bitarray-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:545d36332de81e4742a845a80df89530ff193213a50b4cbef937ed5a44c0e5e5", size = 123502 }, + { url = "https://files.pythonhosted.org/packages/75/2c/9f3ed70ffac8e6d2b0880e132d9e5024e4ef9404a24220deca8dbd702f15/bitarray-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a9eb510cde3fa78c2e302bece510bf5ed494ec40e6b082dec753d6e22d5d1b1", size = 121363 }, + { url = "https://files.pythonhosted.org/packages/48/e0/8ec59416aaa7ca1461a0268c0fe2fbdc8d574ac41e307980f555b773d5f6/bitarray-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e3727ab63dfb6bde00b281934e2212bb7529ea3006c0031a556a84d2268bea5", size = 285792 }, + { url = "https://files.pythonhosted.org/packages/b9/8a/fb9d76ecb44a79f02188240278574376e851d0ca81437f433c9e6481d2e5/bitarray-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2055206ed653bee0b56628f6a4d248d53e5660228d355bbec0014bdfa27050ae", size = 300848 }, + { url = "https://files.pythonhosted.org/packages/63/c5/067b688553b23e99d61ecf930abf1ad5cb5f80c2ebe6f0e2fe8ecab00b3f/bitarray-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:147542299f458bdb177f798726e5f7d39ab8491de4182c3c6d9885ed275a3c2b", size = 303027 }, + { url = "https://files.pythonhosted.org/packages/dc/46/25ebc667907736b2c5c84f4bd8260d9bece8b69719a33db5c3f3dcb281a5/bitarray-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f761184b93092077c7f6b7dad7bd4e671c1620404a76620da7872ceb576a94", size = 286125 }, + { url = "https://files.pythonhosted.org/packages/16/dd/f9a1d84965a992ff42cae5b61536e68fc944f3e31a349b690347d98fc5e0/bitarray-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e008b7b4ce6c7f7a54b250c45c28d4243cc2a3bbfd5298fa7dac92afda229842", size = 277111 }, + { url = "https://files.pythonhosted.org/packages/16/5b/44f298586a09beb62ec553f9efa06c8a5356d2e230e4080c72cb2800a48f/bitarray-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfea514e665af278b2e1d4deb542de1cd4f77413bee83dd15ae16175976ea8d5", size = 280941 }, + { url = "https://files.pythonhosted.org/packages/28/7c/c6e157332227862727959057ba2987e6710985992b196a81f61995f21e19/bitarray-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:66d6134b7bb737b88f1d16478ad0927c571387f6054f4afa5557825a4c1b78e2", size = 272817 }, + { url = "https://files.pythonhosted.org/packages/b7/5d/9f7aaaaf85b5247b4a69b93af60ac7dcfff5545bf544a35517618c4244a0/bitarray-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3cd565253889940b4ec4768d24f101d9fe111cad4606fdb203ea16f9797cf9ed", size = 295830 }, + { url = "https://files.pythonhosted.org/packages/14/1b/86dd50edd2e0612b092fe4caec3001a24298c9acab5e89a503f002ed3bef/bitarray-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4800c91a14656789d2e67d9513359e23e8a534c8ee1482bb9b517a4cfc845200", size = 307592 }, + { url = "https://files.pythonhosted.org/packages/d6/8f/45a1f1bcce5fd88d2f0bb2e1ebe8bbb55247edcb8e7a8ef06e4437e2b5e3/bitarray-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c2945e0390d1329c585c584c6b6d78be017d9c6a1288f9c92006fe907f69cc28", size = 278971 }, + { url = "https://files.pythonhosted.org/packages/43/2d/948c5718fe901aa58c98cef52b8898a6bea865bea7528cff6c2bc703f9f3/bitarray-3.0.0-cp311-cp311-win32.whl", hash = "sha256:c23286abba0cb509733c6ce8f4013cd951672c332b2e184dbefbd7331cd234c8", size = 114242 }, + { url = "https://files.pythonhosted.org/packages/8c/75/e921ada57bb0bcece5eb515927c031f0bc828f702b8f213639358d9df396/bitarray-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca79f02a98cbda1472449d440592a2fe2ad96fe55515a0447fa8864a38017cf8", size = 121524 }, + { url = "https://files.pythonhosted.org/packages/4e/2e/2e4beb2b714dc83a9e90ac0e4bacb1a191c71125734f72962ee2a20b9cfb/bitarray-3.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:184972c96e1c7e691be60c3792ca1a51dd22b7f25d96ebea502fe3c9b554f25d", size = 172152 }, + { url = "https://files.pythonhosted.org/packages/e0/1f/9ec96408c060ffc3df5ba64d2b520fd0484cb3393a96691df8f660a43b17/bitarray-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:787db8da5e9e29be712f7a6bce153c7bc8697ccc2c38633e347bb9c82475d5c9", size = 123319 }, + { url = "https://files.pythonhosted.org/packages/80/9f/4dd05086308bfcc84ad88c663460a8ad9f5f638f9f96eb5fa08381054db6/bitarray-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2da91ab3633c66999c2a352f0ca9ae064f553e5fc0eca231d28e7e305b83e942", size = 121242 }, + { url = "https://files.pythonhosted.org/packages/55/bb/8865b7380e9d20445bc775079f24f2279a8c0d9ee11d57c49b118d39beaf/bitarray-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7edb83089acbf2c86c8002b96599071931dc4ea5e1513e08306f6f7df879a48b", size = 287463 }, + { url = "https://files.pythonhosted.org/packages/db/8b/779119ee438090a80cbfaa49f96e783651183ab4c25b9760fe360aa7cb31/bitarray-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996d1b83eb904589f40974538223eaed1ab0f62be8a5105c280b9bd849e685c4", size = 301599 }, + { url = "https://files.pythonhosted.org/packages/41/25/78f7ba7fa8ab428767dfb722fc1ea9aac4a9813e348023d8047d8fd32253/bitarray-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4817d73d995bd2b977d9cde6050be8d407791cf1f84c8047fa0bea88c1b815bc", size = 304837 }, + { url = "https://files.pythonhosted.org/packages/f7/8d/30a448d3157b4239e635c92fc3b3789a5b87784875ca2776f65bd543d136/bitarray-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d47bc4ff9b0e1624d613563c6fa7b80aebe7863c56c3df5ab238bb7134e8755", size = 288588 }, + { url = "https://files.pythonhosted.org/packages/86/e0/c1f1b595682244f55119d55f280b5a996bcd462b702ec220d976a7566d27/bitarray-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aca0a9cd376beaccd9f504961de83e776dd209c2de5a4c78dc87a78edf61839b", size = 279002 }, + { url = "https://files.pythonhosted.org/packages/5c/4d/a17626923ad2c9d20ed1625fc5b27a8dfe2d1a3e877083e9422455ec302d/bitarray-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:572a61fba7e3a710a8324771322fba8488d134034d349dcd036a7aef74723a80", size = 281898 }, + { url = "https://files.pythonhosted.org/packages/50/d8/5c410580a510e669d9a28bf17675e58843236c55c60fc6dc8f8747808757/bitarray-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a817ad70c1aff217530576b4f037dd9b539eb2926603354fcac605d824082ad1", size = 274622 }, + { url = "https://files.pythonhosted.org/packages/e7/21/de2e8eda85c5f6a05bda75a00c22c94aee71ef09db0d5cbf22446de74312/bitarray-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2ac67b658fa5426503e9581a3fb44a26a3b346c1abd17105735f07db572195b3", size = 296930 }, + { url = "https://files.pythonhosted.org/packages/13/7b/7cfad12d77db2932fb745fa281693b0031c3dfd7f2ecf5803be688cc3798/bitarray-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:12f19ede03e685c5c588ab5ed63167999295ffab5e1126c5fe97d12c0718c18f", size = 309836 }, + { url = "https://files.pythonhosted.org/packages/53/e1/5120fbb8438a0d718e063f70168a2975e03f00ce6b86e74b8eec079cb492/bitarray-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcef31b062f756ba7eebcd7890c5d5de84b9d64ee877325257bcc9782288564a", size = 281535 }, + { url = "https://files.pythonhosted.org/packages/73/75/8acebbbb4f85dcca73b8e91dde5d3e1e3e2317b36fae4f5b133c60720834/bitarray-3.0.0-cp312-cp312-win32.whl", hash = "sha256:656db7bdf1d81ec3b57b3cad7ec7276765964bcfd0eb81c5d1331f385298169c", size = 114423 }, + { url = "https://files.pythonhosted.org/packages/ca/56/dadae4d4351b337de6e0269001fb40f3ebe9f72222190456713d2c1be53d/bitarray-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f785af6b7cb07a9b1e5db0dea9ef9e3e8bb3d74874a0a61303eab9c16acc1999", size = 121680 }, + { url = "https://files.pythonhosted.org/packages/4f/30/07d7be4624981537d32b261dc48a16b03757cc9d88f66012d93acaf11663/bitarray-3.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7cb885c043000924554fe2124d13084c8fdae03aec52c4086915cd4cb87fe8be", size = 172147 }, + { url = "https://files.pythonhosted.org/packages/f0/e9/be1fa2828bad9cb32e1309e6dbd05adcc41679297d9e96bbb372be928e38/bitarray-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7814c9924a0b30ecd401f02f082d8697fc5a5be3f8d407efa6e34531ff3c306a", size = 123319 }, + { url = "https://files.pythonhosted.org/packages/22/28/33601d276a6eb76e40fe8a61c61f59cc9ff6d9ecf0b676235c02689475b8/bitarray-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcf524a087b143ba736aebbb054bb399d49e77cf7c04ed24c728e411adc82bfa", size = 121236 }, + { url = "https://files.pythonhosted.org/packages/85/d3/f36b213ffae8f9c8e4c6f12a91e18c06570a04f42d5a1bda4303380f2639/bitarray-3.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1d5abf1d6d910599ac16afdd9a0ed3e24f3b46af57f3070cf2792f236f36e0b", size = 287395 }, + { url = "https://files.pythonhosted.org/packages/b7/1a/2da3b00d876883b05ffd3be9b1311858b48d4a26579f8647860e271c5385/bitarray-3.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9929051feeaf8d948cc0b1c9ce57748079a941a1a15c89f6014edf18adaade84", size = 301501 }, + { url = "https://files.pythonhosted.org/packages/88/b9/c1b5af8d1c918f1ee98748f7f7270f932f531c2259dd578c0edcf16ec73e/bitarray-3.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96cf0898f8060b2d3ae491762ae871b071212ded97ff9e1e3a5229e9fefe544c", size = 304804 }, + { url = "https://files.pythonhosted.org/packages/92/24/81a10862856419638c0db13e04de7cbf19938353517a67e4848c691f0b7c/bitarray-3.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab37da66a8736ad5a75a58034180e92c41e864da0152b84e71fcc253a2f69cd4", size = 288507 }, + { url = "https://files.pythonhosted.org/packages/da/70/a093af92ef7b207a59087e3b5819e03767fbdda9dd56aada3a4ee25a1fbd/bitarray-3.0.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeb79e476d19b91fd6a3439853e4e5ba1b3b475920fa40d62bde719c8af786f", size = 278905 }, + { url = "https://files.pythonhosted.org/packages/fb/40/0925c6079c4b282b16eb9085f82df0cdf1f787fb4c67fd4baca3e37acf7f/bitarray-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f75fc0198c955d840b836059bd43e0993edbf119923029ca60c4fc017cefa54a", size = 281909 }, + { url = "https://files.pythonhosted.org/packages/61/4b/e11754a5d34cb997250d8019b1fe555d4c06fe2d2a68b0bf7c5580537046/bitarray-3.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f12cc7c7638074918cdcc7491aff897df921b092ffd877227892d2686e98f876", size = 274711 }, + { url = "https://files.pythonhosted.org/packages/5b/78/39513f75423959ee2d82a82e10296b6a7bc7d880b16d714980a6752ef33b/bitarray-3.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dbe1084935b942fab206e609fa1ed3f46ad1f2612fb4833e177e9b2a5e006c96", size = 297038 }, + { url = "https://files.pythonhosted.org/packages/af/a2/5cb81f8773a479de7c06cc1ada36d5cc5a8ebcd8715013e1c4e01a76e84a/bitarray-3.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ac06dd72ee1e1b6e312504d06f75220b5894af1fb58f0c20643698f5122aea76", size = 309814 }, + { url = "https://files.pythonhosted.org/packages/03/3e/795b57c6f6eea61c47d0716e1d60219218028b1f260f7328802eac684964/bitarray-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00f9a88c56e373009ac3c73c55205cfbd9683fbd247e2f9a64bae3da78795252", size = 281564 }, + { url = "https://files.pythonhosted.org/packages/f6/31/5914002ae4dd0e0079f8bccfd0647119cff364280d106108a19bd2511933/bitarray-3.0.0-cp313-cp313-win32.whl", hash = "sha256:9c6e52005e91803eb4e08c0a08a481fb55ddce97f926bae1f6fa61b3396b5b61", size = 114404 }, + { url = "https://files.pythonhosted.org/packages/76/0a/184f85a1739db841ae8fbb1d9ec028240d5a351e36abec9cd020de889dab/bitarray-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:cb98d5b6eac4b2cf2a5a69f60a9c499844b8bea207059e9fc45c752436e6bb49", size = 121672 }, + { url = "https://files.pythonhosted.org/packages/01/6b/405d04ed3d0e46dcc52b9f9ca98b342de5930ed87adcacb86afc830e188b/bitarray-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fef4e3b3f2084b4dae3e5316b44cda72587dcc81f68b4eb2dbda1b8d15261b61", size = 119755 }, + { url = "https://files.pythonhosted.org/packages/90/d8/cdfd2d41a836479db66c1d33f2615c37529458427586c8d585fec4c39c5c/bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e9eee03f187cef1e54a4545124109ee0afc84398628b4b32ebb4852b4a66393", size = 124105 }, + { url = "https://files.pythonhosted.org/packages/12/5d/4214bb7103fa9601332b49fc2fcef73005750581aabe7e13163ad66013cc/bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb5702dd667f4bb10fed056ffdc4ddaae8193a52cd74cb2cdb54e71f4ef2dd1", size = 124669 }, + { url = "https://files.pythonhosted.org/packages/fc/9b/ecfe49cf03047c8415d71ee931352b11b747525cbff9bc5db9c3592d21da/bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:666e44b0458bb2894b64264a29f2cc7b5b2cbcc4c5e9cedfe1fdbde37a8e329a", size = 126520 }, + { url = "https://files.pythonhosted.org/packages/e7/79/190bcac2a23fb5f726d0305b372f73e0bf496a43da0ace4e285e9927fcdb/bitarray-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c756a92cf1c1abf01e56a4cc40cb89f0ff9147f2a0be5b557ec436a23ff464d8", size = 122035 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "ckzg" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/80/4b6219a65634915efc4694fa606f38d4b893dcdc1e50b9bcf69b38ec82b0/ckzg-2.0.1.tar.gz", hash = "sha256:62c5adc381637affa7e1df465c57750b356a761b8a3164c3106589b02532b9c9", size = 1113747 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/3b/6c9e4a1b84bd4a8cab3277754a05816707d4fb873e7c2ee56a793fdb76dc/ckzg-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b7f9ba6d215f8981c5545f952aac84875bd564a63da02fb22a3d1321662ecdc0", size = 114745 }, + { url = "https://files.pythonhosted.org/packages/b5/19/9f8b9c35a00040ddbb9c33a4b9c03e4f3e18987fd098ab45b3c4ce67eb44/ckzg-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8fdec3ff96399acba9baeef9e1b0b5258c08f73245780e6c69f7b73def5e8d0a", size = 98693 }, + { url = "https://files.pythonhosted.org/packages/c8/88/706c97167ebf4a5acfcb32766bf55760db10d110757ca4776e80fe3a9063/ckzg-2.0.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1644369af9900a9f109d417d6760693edf134118f3100d0c68f56667de775b80", size = 174085 }, + { url = "https://files.pythonhosted.org/packages/02/85/ed6d01f8b54aba83d029208a5bca395401263d24e55ed33ce7d42b68cb1c/ckzg-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a2146f122d489ac7e67ae0c0743f8d0db1718e6aeed8f05717340594fe07dd", size = 160084 }, + { url = "https://files.pythonhosted.org/packages/e7/73/2883fc3a2e63a9b1b69b3abfba6fa6af00b02777f0f5c580840723316570/ckzg-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979841be50f2782b447762db38e9bc927ae251f6ca86c54a26561a52068ee779", size = 168864 }, + { url = "https://files.pythonhosted.org/packages/55/b4/e377569c018c90f0028f2e73c8791e776ac526ecf09a35e0a1ab4d7a74b0/ckzg-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4516d86647ee4e8ea9470f4adf68fbebb6dc1bdedff7d9592c2504fe53145908", size = 170524 }, + { url = "https://files.pythonhosted.org/packages/cd/56/145b81f4650e7ca4eba58c24ca8bbdfbdc4cf5427c61895cbc0cd2eb17e4/ckzg-2.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:91866fc58a29b4829201efd9ffadfac3ffeca6359254a54a360ff6a189c34bf5", size = 185023 }, + { url = "https://files.pythonhosted.org/packages/d7/39/663dcd97cf4dac9baa59f3143187349c7dbd43628640a34cf1e04b97d8e0/ckzg-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed35508dac059b2c0a7994383bc7a92eaf35d0b9ce790016819e2619e0f4b8a9", size = 179076 }, + { url = "https://files.pythonhosted.org/packages/fe/05/c250fc209f30a4dea8eb92a12d720391a3bd657b55a5b6ab7ba16c56e092/ckzg-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:449c4fe38017351eca362106420eeb2d28d50b7e54aa8668b3af29a8ab780132", size = 98229 }, + { url = "https://files.pythonhosted.org/packages/56/eb/00e5978a32facf9203e2069d7905f8a5c262cafd6795adf9933531e7427f/ckzg-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:260608a22e2f2cadcd31f4495832d45d6460438c38faba9761b92df885a99d88", size = 114743 }, + { url = "https://files.pythonhosted.org/packages/b7/99/65015b5c6447293a3321112cb33d303ae07a5000302fa08c976819fcad64/ckzg-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1015f99c50215098751b07d7e459ba9a2790d3692ca81552eed29996128e90d", size = 98687 }, + { url = "https://files.pythonhosted.org/packages/92/ec/5d324b490b30e581888d8f7243c3259aad6a25913653ea6d8d673c84ec0d/ckzg-2.0.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dd350d97554c161dc5b8c7b32c2dc8e659632c374f60e2669fb3c9b5b294827", size = 174848 }, + { url = "https://files.pythonhosted.org/packages/71/50/40a6c567c15eb65c73cbc45c1ed9341a0f1ea77bf489078f56e40e0e4836/ckzg-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec7724fa8dc4ae95757efe4a87e7b2d4b880cb348c72ce7355fc0c4f64bc298", size = 160846 }, + { url = "https://files.pythonhosted.org/packages/03/c9/044fbe6083fc0e8477b9545b193b82bf349a03f4b561f87870f0b2280f36/ckzg-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3fa0f4398fa67fb71f0a2b34a652cc89e6e0e6af1340b0dc771db1a5f3e089c", size = 169632 }, + { url = "https://files.pythonhosted.org/packages/ae/f5/54e736a969813c72c745c2f0c2906e973d63f89744d947c4e3429c81761d/ckzg-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f865a0297aabeeb638187a46f7df445763360417b9df4dea60560d512c2cda09", size = 171214 }, + { url = "https://files.pythonhosted.org/packages/f4/2f/0896c133021479c5fb57ffbddd584774bdebd7d8c966be732380aa1d62ac/ckzg-2.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b6ec738350771dbf5974fb70cc8bbb20a4df784af770f7e655922adc08a2171", size = 185708 }, + { url = "https://files.pythonhosted.org/packages/02/a8/513876410e9634f10ef933f6aae6e71afd3e0786817773c9d40908b6abd4/ckzg-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b4b669fc77edeb16adc182efc32b3737b36f741a2e33a170d40619e8b171a94", size = 179787 }, + { url = "https://files.pythonhosted.org/packages/c4/b6/5fe04bdbcf881bf23626ff82145e0c077ee3bdb35c86d177bf37fa57832e/ckzg-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:decb97f4a17c7338b2130dcc4b045df4cc0e7785ece872c764b554c7c73a99ff", size = 98228 }, + { url = "https://files.pythonhosted.org/packages/6c/87/dcc62fc2f6651127b6306a37db492998c291ad1a09a6a0d18895882fec51/ckzg-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:285cf3121b8a8c5609c5b706314f68d2ba2784ab02c5bb7487c6ae1714ecb27f", size = 114776 }, + { url = "https://files.pythonhosted.org/packages/fd/99/2d3aa09ebf692c26e03d17b9e7426a34fd71fe4d9b2ff1acf482736cc8da/ckzg-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f927bc41c2551b0ef0056a649a7ebed29d9665680a10795f4cee5002c69ddb7", size = 98711 }, + { url = "https://files.pythonhosted.org/packages/50/b3/44a533895aa4257d0dcb2818f7dd9b1321664784cac2d381022ed8c40113/ckzg-2.0.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd9fb690c88919f30c9f3ab7cc46a7ecd734d5ff4c9ccea383c119b9b7cc4da", size = 175026 }, + { url = "https://files.pythonhosted.org/packages/54/a2/c594861665851f91ae81ec29cf90e38999de042aa95604737d4b779a8609/ckzg-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fabc3bd41b306d1c7025d561c3281a007c2aca8ceaf998582dc3894904d9c73e", size = 161039 }, + { url = "https://files.pythonhosted.org/packages/59/a0/96bb77fb8bf4cd4d51d8bd1d67d59d13f51fa2477b11b915ab6465aa92ce/ckzg-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb50c53efdb9c34f762bd0c8006cf79bc92a9daf47aa6b541e496988484124f", size = 169889 }, + { url = "https://files.pythonhosted.org/packages/68/c4/77d54a7e5f85d833e9664935f6278fbea7de30f4fde213d121f7fdbc27a0/ckzg-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7960cc62f959403293fb53a3c2404778369ae7cefc6d7f202e5e00567cf98c4b", size = 171378 }, + { url = "https://files.pythonhosted.org/packages/02/54/6520ab37c06680910f8ff99afdc473c945c37ab1016662288d98a028d775/ckzg-2.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d721bcd492294c70eca39da0b0a433c29b6a571dbac2f7084bab06334904af06", size = 185969 }, + { url = "https://files.pythonhosted.org/packages/d6/fa/16c3a4fd8353a3a9f95728f4141b2800b08e588522f7b5644c91308f6fe1/ckzg-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dde2391d025b5033ef0eeacf62b11ecfe446aea25682b5f547a907766ad0a8cb", size = 180093 }, + { url = "https://files.pythonhosted.org/packages/d5/ae/91d36445c247a8832bbb7a71bd75293c4c006731d03a2ccaa13e5506ac8a/ckzg-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fab8859d9420f6f7df4e094ee3639bc49d18c8dab0df81bee825e2363dd67a09", size = 98280 }, + { url = "https://files.pythonhosted.org/packages/79/92/4910b9131eb637c6f72e655c1535b9a9c72a5fb2bdf52742f50066cb9e6b/ckzg-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9747d92883199d4f8f3a3d7018134745fddcf692dfe67115434e4b32609ea785", size = 114793 }, + { url = "https://files.pythonhosted.org/packages/09/95/cb52623cbc4573b2e65bd924524f479e6a8611002c4634dfd6e9d490b403/ckzg-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b2cf58fb9e165da97f0ffe9f4a6efb73992645fac8e0fa223a6cc7ec486a434a", size = 98715 }, + { url = "https://files.pythonhosted.org/packages/27/05/3f246149a728b5d829dd8e0b75379fd6bce0d420de4042b8ca692083f96d/ckzg-2.0.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d25d006899d76bb8c9d3e8b27981dd6b66a78f9826e33c1bf981af6577a69a19", size = 175008 }, + { url = "https://files.pythonhosted.org/packages/5f/76/df568b24de6bdbb99fe2c48519744d01f1b9e152fa791ed84b43a2752e78/ckzg-2.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04bf0b32f04f5ea5e4b8518e292d3321bc05596fde95f9c3b4f504e5e4bc780", size = 161009 }, + { url = "https://files.pythonhosted.org/packages/7f/b5/8bbde8acb339a018c0456fb9af714fcca86ed9bf96114ece9556415afbac/ckzg-2.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0cf3dccd72376bff10e1833641cc9d642f34f60ca63972626d9dfcfdc8e77f", size = 169873 }, + { url = "https://files.pythonhosted.org/packages/5f/8f/9b9492f807acbfe791c4c447bbeb96e19160fda272328a9dc6700a2fcb08/ckzg-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:770809c7e93087470cc524724419b0f85590edb033c7c73ba94aef70b36ca18b", size = 171438 }, + { url = "https://files.pythonhosted.org/packages/f1/58/47d4ed23e338dbe1b06cca99e55ae49c3a539d88576c5893e8b589bf3ac6/ckzg-2.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e31b59b8124148d5e21f7e41b35532d7af98260c44a77c3917958adece84296d", size = 186020 }, + { url = "https://files.pythonhosted.org/packages/a5/f0/dc6f961b325d186af1a469e7119a0f48cdc36240f2aca6e10a5e5f91b8b8/ckzg-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:174f0c356df644d6e349ce03b7284d83dbec859e11ca5d1b1b3bace8b8fbc65d", size = 180132 }, + { url = "https://files.pythonhosted.org/packages/72/ca/44676731ca52e6d2289f7e9c74d836f59dc986e9b4182ddd2c7d0b14d88f/ckzg-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:30e375cd45142e56b5dbfdec05ce4deb2368d7f7dedfc7408ba37d5639af05ff", size = 98284 }, + { url = "https://files.pythonhosted.org/packages/d0/e7/9530e51d23d2ef3840aa8c18c3e7844a1c1465f0226befaef14b4270a9fa/ckzg-2.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a12e96f20dce35e5222f898a5c8355054ef7c5ee038eeb97dbb694640b57577b", size = 112463 }, + { url = "https://files.pythonhosted.org/packages/c5/31/463330808421319784a41667c8141a1912ed71abb6b55192186142502aaf/ckzg-2.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4e0ebc55253addaa24dd2cd871bbe3b8f57855f32b5f74e70bf2cb76b6f7da54", size = 95936 }, + { url = "https://files.pythonhosted.org/packages/fe/6e/84dc599aefcd4533298349bd468e3f56790b1dbd086ebd624184e0d8c6fb/ckzg-2.0.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f917a7bf363a3735db30559e1ed63cf1ccf414234433ba687fa72c007abd756", size = 125768 }, + { url = "https://files.pythonhosted.org/packages/51/2d/bdc7dd9d902bceb523d77e90f9d62a4692999dc8715a5795fb2817b1737f/ckzg-2.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30f08c984286853271d4adae219e9ba87275a15047dbaa262ab8dd6c01be97b0", size = 102410 }, + { url = "https://files.pythonhosted.org/packages/f1/f8/c2a06ad4daf189057652ed2ec6ebb1cb29190f57bf67fd4f4309d5614947/ckzg-2.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fa1ea4888417e1f109fd5e57965788fb7f53b674329b937a65604a3c1ca1d03", size = 110686 }, + { url = "https://files.pythonhosted.org/packages/99/a9/ce3d5fd9df58da092c90ddc7b214fd313b54d5bc325770c488c8864ad17a/ckzg-2.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0b249914aeaf05cabc71c5c3797e3d6c126cb2c64192b7eb6755ef6aa5ab2f11", size = 98327 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 }, + { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 }, + { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 }, + { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 }, + { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 }, + { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 }, + { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 }, + { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 }, + { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 }, + { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 }, + { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, + { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, + { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, + { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, + { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, + { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, + { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, + { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, + { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, + { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, + { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, + { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, + { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, + { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, + { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, + { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, + { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, + { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, + { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, + { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, + { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, + { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, + { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, + { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, + { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, + { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, + { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, + { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, + { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, + { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, + { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, + { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, + { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "44.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, + { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, + { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, + { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, + { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, + { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, + { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, + { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, + { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, + { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, + { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, + { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, + { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, + { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, + { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, + { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, + { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, + { url = "https://files.pythonhosted.org/packages/e0/f1/7fb4982d59aa86e1a116c812b545e7fc045352be07738ae3fb278835a9a4/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", size = 3888155 }, + { url = "https://files.pythonhosted.org/packages/60/7b/cbc203838d3092203493d18b923fbbb1de64e0530b332a713ba376905b0b/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", size = 4106417 }, + { url = "https://files.pythonhosted.org/packages/12/c7/2fe59fb085ab418acc82e91e040a6acaa7b1696fcc1c1055317537fbf0d3/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", size = 3887540 }, + { url = "https://files.pythonhosted.org/packages/48/89/09fc7b115f60f5bd970b80e32244f8e9aeeb9244bf870b63420cec3b5cd5/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", size = 4106040 }, +] + +[[package]] +name = "cytoolz" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/f9/3243eed3a6545c2a33a21f74f655e3fcb5d2192613cd3db81a93369eb339/cytoolz-1.0.1.tar.gz", hash = "sha256:89cc3161b89e1bb3ed7636f74ed2e55984fd35516904fc878cae216e42b2c7d6", size = 626652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d9/f13d66c16cff1fa1cb6c234698029877c456f35f577ef274aba3b86e7c51/cytoolz-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cec9af61f71fc3853eb5dca3d42eb07d1f48a4599fa502cbe92adde85f74b042", size = 403515 }, + { url = "https://files.pythonhosted.org/packages/4b/2d/4cdf848a69300c7d44984f2ebbebb3b8576e5449c8dea157298f3bdc4da3/cytoolz-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:140bbd649dbda01e91add7642149a5987a7c3ccc251f2263de894b89f50b6608", size = 383936 }, + { url = "https://files.pythonhosted.org/packages/72/a4/ccfdd3f0ed9cc818f734b424261f6018fc61e3ec833bf85225a9aca0d994/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90124bdc42ff58b88cdea1d24a6bc5f776414a314cc4d94f25c88badb3a16d1", size = 1934569 }, + { url = "https://files.pythonhosted.org/packages/50/fc/38d5344fa595683ad10dc819cfc1d8b9d2b3391ccf3e8cb7bab4899a01f5/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e74801b751e28f7c5cc3ad264c123954a051f546f2fdfe089f5aa7a12ccfa6da", size = 2015129 }, + { url = "https://files.pythonhosted.org/packages/28/29/75261748dc54a20a927f33641f4e9aac674cfc6d3fbd4f332e10d0b37639/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:582dad4545ddfb5127494ef23f3fa4855f1673a35d50c66f7638e9fb49805089", size = 2000506 }, + { url = "https://files.pythonhosted.org/packages/00/ae/e4ead004cc2698281d153c4a5388638d67cdb5544d6d6cc1e5b3db2bd2a3/cytoolz-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7bd0618e16efe03bd12f19c2a26a27e6e6b75d7105adb7be1cd2a53fa755d8", size = 1957537 }, + { url = "https://files.pythonhosted.org/packages/4a/ff/4f3aa07f4f47701f7f63df60ce0a5669fa09c256c3d4a33503a9414ea5cc/cytoolz-1.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d74cca6acf1c4af58b2e4a89cc565ed61c5e201de2e434748c93e5a0f5c541a5", size = 1863331 }, + { url = "https://files.pythonhosted.org/packages/a2/29/654f57f2a9b8e9765a4ab876765f64f94530b61fc6471a07feea42ece6d4/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:823a3763828d8d457f542b2a45d75d6b4ced5e470b5c7cf2ed66a02f508ed442", size = 1849938 }, + { url = "https://files.pythonhosted.org/packages/bc/7b/11f457db6b291060a98315ab2c7198077d8bddeeebe5f7126d9dad98cc54/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:51633a14e6844c61db1d68c1ffd077cf949f5c99c60ed5f1e265b9e2966f1b52", size = 1852345 }, + { url = "https://files.pythonhosted.org/packages/6b/92/0dccc96ce0323be236d404f5084479b79b747fa0e74e43a270e95868b5f9/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3ec9b01c45348f1d0d712507d54c2bfd69c62fbd7c9ef555c9d8298693c2432", size = 1989877 }, + { url = "https://files.pythonhosted.org/packages/a3/c8/1c5203a81200bae51aa8f7b5fad613f695bf1afa03f16251ca23ecb2ef9f/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1855022b712a9c7a5bce354517ab4727a38095f81e2d23d3eabaf1daeb6a3b3c", size = 1994492 }, + { url = "https://files.pythonhosted.org/packages/e2/8a/04bc193c4d7ced8ef6bb62cdcd0bf40b5e5eb26586ed2cfb4433ec7dfd0a/cytoolz-1.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9930f7288c4866a1dc1cc87174f0c6ff4cad1671eb1f6306808aa6c445857d78", size = 1896077 }, + { url = "https://files.pythonhosted.org/packages/21/a5/bee63a58f51d2c74856db66e6119a014464ff8cb1c9387fa4bd2d94e49b0/cytoolz-1.0.1-cp310-cp310-win32.whl", hash = "sha256:a9baad795d72fadc3445ccd0f122abfdbdf94269157e6d6d4835636dad318804", size = 322135 }, + { url = "https://files.pythonhosted.org/packages/e8/16/7abfb1685e8b7f2838264551ee33651748994813f566ac4c3d737dfe90e5/cytoolz-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:ad95b386a84e18e1f6136f6d343d2509d4c3aae9f5a536f3dc96808fcc56a8cf", size = 363599 }, + { url = "https://files.pythonhosted.org/packages/dc/ea/8131ae39119820b8867cddc23716fa9f681f2b3bbce6f693e68dfb36b55b/cytoolz-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d958d4f04d9d7018e5c1850790d9d8e68b31c9a2deebca74b903706fdddd2b6", size = 406162 }, + { url = "https://files.pythonhosted.org/packages/26/18/3d9bd4c146f6ea6e51300c242b20cb416966b21d481dac230e1304f1e54b/cytoolz-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0f445b8b731fc0ecb1865b8e68a070084eb95d735d04f5b6c851db2daf3048ab", size = 384961 }, + { url = "https://files.pythonhosted.org/packages/e4/73/9034827907c7f85c7c484c9494e905d022fb8174526004e9ef332570349e/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f546a96460a7e28eb2ec439f4664fa646c9b3e51c6ebad9a59d3922bbe65e30", size = 2091698 }, + { url = "https://files.pythonhosted.org/packages/74/af/d5c2733b0fde1a08254ff1a8a8d567874040c9eb1606363cfebc0713c73f/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0317681dd065532d21836f860b0563b199ee716f55d0c1f10de3ce7100c78a3b", size = 2188452 }, + { url = "https://files.pythonhosted.org/packages/6a/bb/77c71fa9c217260b4056a732d754748903423c2cdd82a673d6064741e375/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c0ef52febd5a7821a3fd8d10f21d460d1a3d2992f724ba9c91fbd7a96745d41", size = 2174203 }, + { url = "https://files.pythonhosted.org/packages/fc/a9/a5b4a3ff5d22faa1b60293bfe97362e2caf4a830c26d37ab5557f60d04b2/cytoolz-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebaf419acf2de73b643cf96108702b8aef8e825cf4f63209ceb078d5fbbbfd", size = 2099831 }, + { url = "https://files.pythonhosted.org/packages/35/08/7f6869ea1ff31ce5289a7d58d0e7090acfe7058baa2764473048ff61ea3c/cytoolz-1.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f7f04eeb4088947585c92d6185a618b25ad4a0f8f66ea30c8db83cf94a425e3", size = 1996744 }, + { url = "https://files.pythonhosted.org/packages/46/b4/9ac424c994b51763fd1bbed62d95f8fba8fa0e45c8c3c583904fdaf8f51d/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f61928803bb501c17914b82d457c6f50fe838b173fb40d39c38d5961185bd6c7", size = 2013733 }, + { url = "https://files.pythonhosted.org/packages/3e/99/03009765c4b87d742d5b5a8670abb56a8c7ede033c2cdaa4be8662d3b001/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d2960cb4fa01ccb985ad1280db41f90dc97a80b397af970a15d5a5de403c8c61", size = 1994850 }, + { url = "https://files.pythonhosted.org/packages/40/9a/8458af9a5557e177ea42f8cf7e477bede518b0bbef564e28c4151feaa52c/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2b407cc3e9defa8df5eb46644f6f136586f70ba49eba96f43de67b9a0984fd3", size = 2155352 }, + { url = "https://files.pythonhosted.org/packages/5e/5c/2a701423e001fcbec288b4f3fc2bf67557d114c2388237fc1ae67e1e2686/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8245f929144d4d3bd7b972c9593300195c6cea246b81b4c46053c48b3f044580", size = 2163515 }, + { url = "https://files.pythonhosted.org/packages/36/16/ee2e06e65d9d533bc05cd52a0b355ba9072fc8f60d77289e529c6d2e3750/cytoolz-1.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e37385db03af65763933befe89fa70faf25301effc3b0485fec1c15d4ce4f052", size = 2054431 }, + { url = "https://files.pythonhosted.org/packages/d8/d5/2fac8315f210fa1bc7106e27c19e1211580aa25bb7fa17dfd79505e5baf2/cytoolz-1.0.1-cp311-cp311-win32.whl", hash = "sha256:50f9c530f83e3e574fc95c264c3350adde8145f4f8fc8099f65f00cc595e5ead", size = 322004 }, + { url = "https://files.pythonhosted.org/packages/a9/9e/0b70b641850a95f9ff90adde9d094a4b1d81ec54dadfd97fec0a2aaf440e/cytoolz-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b7f6b617454b4326af7bd3c7c49b0fc80767f134eb9fd6449917a058d17a0e3c", size = 365358 }, + { url = "https://files.pythonhosted.org/packages/d8/e8/218098344ed2cb5f8441fade9b2428e435e7073962374a9c71e59ac141a7/cytoolz-1.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fcb8f7d0d65db1269022e7e0428471edee8c937bc288ebdcb72f13eaa67c2fe4", size = 414121 }, + { url = "https://files.pythonhosted.org/packages/de/27/4d729a5653718109262b758fec1a959aa9facb74c15460d9074dc76d6635/cytoolz-1.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:207d4e4b445e087e65556196ff472ff134370d9a275d591724142e255f384662", size = 390904 }, + { url = "https://files.pythonhosted.org/packages/72/c0/cbabfa788bab9c6038953bf9478adaec06e88903a726946ea7c88092f5c4/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21cdf6bac6fd843f3b20280a66fd8df20dea4c58eb7214a2cd8957ec176f0bb3", size = 2090734 }, + { url = "https://files.pythonhosted.org/packages/c3/66/369262c60f9423c2da82a60864a259c852f1aa122aced4acd2c679af58c0/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a55ec098036c0dea9f3bdc021f8acd9d105a945227d0811589f0573f21c9ce1", size = 2155933 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/ee55186802f8d24b5fbf9a11405ccd1203b30eded07cc17750618219b94e/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a13ab79ff4ce202e03ab646a2134696988b554b6dc4b71451e948403db1331d8", size = 2171903 }, + { url = "https://files.pythonhosted.org/packages/a1/96/bd1a9f3396e9b7f618db8cd08d15630769ce3c8b7d0534f92cd639c977ae/cytoolz-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2d944799026e1ff08a83241f1027a2d9276c41f7a74224cd98b7df6e03957d", size = 2125270 }, + { url = "https://files.pythonhosted.org/packages/28/48/2a3762873091c88a69e161111cfbc6c222ff145d57ff011a642b169f04f1/cytoolz-1.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88ba85834cd523b91fdf10325e1e6d71c798de36ea9bdc187ca7bd146420de6f", size = 1973967 }, + { url = "https://files.pythonhosted.org/packages/e4/50/500bd69774bdc49a4d78ec8779eb6ac7c1a9d706bfd91cf2a1dba604373a/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a750b1af7e8bf6727f588940b690d69e25dc47cce5ce467925a76561317eaf7", size = 2021695 }, + { url = "https://files.pythonhosted.org/packages/e4/4e/ba5a0ce34869495eb50653de8d676847490cf13a2cac1760fc4d313e78de/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44a71870f7eae31d263d08b87da7c2bf1176f78892ed8bdade2c2850478cb126", size = 2010177 }, + { url = "https://files.pythonhosted.org/packages/87/57/615c630b3089a13adb15351d958d227430cf624f03b1dd39eb52c34c1f59/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8231b9abbd8e368e036f4cc2e16902c9482d4cf9e02a6147ed0e9a3cd4a9ab0", size = 2154321 }, + { url = "https://files.pythonhosted.org/packages/7f/0f/fe1aa2d931e3b35ecc05215bd75da945ea7346095b3b6f6027164e602d5a/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa87599ccc755de5a096a4d6c34984de6cd9dc928a0c5eaa7607457317aeaf9b", size = 2188374 }, + { url = "https://files.pythonhosted.org/packages/de/fa/fd363d97a641b6d0e2fd1d5c35b8fd41d9ccaeb4df56302f53bf23a58e3a/cytoolz-1.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67cd16537df51baabde3baa770ab7b8d16839c4d21219d5b96ac59fb012ebd2d", size = 2077911 }, + { url = "https://files.pythonhosted.org/packages/d9/68/0a22946b98ae5201b54ccb4e651295285c0fb79406022b6ee8b2f791940c/cytoolz-1.0.1-cp312-cp312-win32.whl", hash = "sha256:fb988c333f05ee30ad4693fe4da55d95ec0bb05775d2b60191236493ea2e01f9", size = 321903 }, + { url = "https://files.pythonhosted.org/packages/62/1a/f3903197956055032f8cb297342e2dff07e50f83991aebfe5b4c4fcb55e4/cytoolz-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8f89c48d8e5aec55ffd566a8ec858706d70ed0c6a50228eca30986bfa5b4da8b", size = 364490 }, + { url = "https://files.pythonhosted.org/packages/aa/2e/a9f069db0107749e9e72baf6c21abe3f006841a3bcfdc9b8420e22ef31eb/cytoolz-1.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6944bb93b287032a4c5ca6879b69bcd07df46f3079cf8393958cf0b0454f50c0", size = 407365 }, + { url = "https://files.pythonhosted.org/packages/a9/9b/5e87dd0e31f54c778b4f9f34cc14c1162d3096c8d746b0f8be97d70dd73c/cytoolz-1.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e027260fd2fc5cb041277158ac294fc13dca640714527219f702fb459a59823a", size = 385233 }, + { url = "https://files.pythonhosted.org/packages/63/00/2fd32b16284cdb97cfe092822179bc0c3bcdd5e927dd39f986169a517642/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88662c0e07250d26f5af9bc95911e6137e124a5c1ec2ce4a5d74de96718ab242", size = 2062903 }, + { url = "https://files.pythonhosted.org/packages/85/39/b3cbb5a9847ba59584a263772ad4f8ca2dbfd2a0e11efd09211d1219804c/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309dffa78b0961b4c0cf55674b828fbbc793cf2d816277a5c8293c0c16155296", size = 2139517 }, + { url = "https://files.pythonhosted.org/packages/ea/39/bfcab4a46d50c467e36fe704f19d8904efead417787806ee210327f68390/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edb34246e6eb40343c5860fc51b24937698e4fa1ee415917a73ad772a9a1746b", size = 2154849 }, + { url = "https://files.pythonhosted.org/packages/fd/42/3bc6ee61b0aa47e1cb40819adc1a456d7efa809f0dea9faddacb43fdde8f/cytoolz-1.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54da7a8e4348a18d45d4d5bc84af6c716d7f131113a4f1cc45569d37edff1b", size = 2102302 }, + { url = "https://files.pythonhosted.org/packages/00/66/3f636c6ddea7b18026b90a8c238af472e423b86e427b11df02213689b012/cytoolz-1.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:241c679c3b1913c0f7259cf1d9639bed5084c86d0051641d537a0980548aa266", size = 1960872 }, + { url = "https://files.pythonhosted.org/packages/40/36/cb3b7cdd651007b69f9c48e9d104cec7cb8dc53afa1d6a720e5ad08022fa/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5bfc860251a8f280ac79696fc3343cfc3a7c30b94199e0240b6c9e5b6b01a2a5", size = 2014430 }, + { url = "https://files.pythonhosted.org/packages/88/3f/2e9bd2a16cfd269808922147551dcb2d8b68ba54a2c4deca2fa6a6cd0d5f/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8edd1547014050c1bdad3ff85d25c82bd1c2a3c96830c6181521eb78b9a42b3", size = 2003127 }, + { url = "https://files.pythonhosted.org/packages/c4/7d/08604ff940aa784df8343c387fdf2489b948b714a6afb587775ae94da912/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b349bf6162e8de215403d7f35f8a9b4b1853dc2a48e6e1a609a5b1a16868b296", size = 2142369 }, + { url = "https://files.pythonhosted.org/packages/d2/c6/39919a0645bdbdf720e97cae107f959ea9d1267fbc3b0d94fc6e1d12ac8f/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1b18b35256219b6c3dd0fa037741b85d0bea39c552eab0775816e85a52834140", size = 2180427 }, + { url = "https://files.pythonhosted.org/packages/d8/03/dbb9d47556ee54337e7e0ac209d17ceff2d2a197c34de08005abc7a7449b/cytoolz-1.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:738b2350f340ff8af883eb301054eb724997f795d20d90daec7911c389d61581", size = 2069785 }, + { url = "https://files.pythonhosted.org/packages/ea/f8/11bb7b8947002231faae3ec2342df5896afbc19eb783a332cce6d219ff79/cytoolz-1.0.1-cp313-cp313-win32.whl", hash = "sha256:9cbd9c103df54fcca42be55ef40e7baea624ac30ee0b8bf1149f21146d1078d9", size = 320685 }, + { url = "https://files.pythonhosted.org/packages/40/eb/dde173cf2357084ca9423950be1f2f11ab11d65d8bd30165bfb8fd4213e9/cytoolz-1.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:90e577e08d3a4308186d9e1ec06876d4756b1e8164b92971c69739ea17e15297", size = 362898 }, + { url = "https://files.pythonhosted.org/packages/d9/f7/ef2a10daaec5c0f7d781d50758c6187eee484256e356ae8ef178d6c48497/cytoolz-1.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:83d19d55738ad9c60763b94f3f6d3c6e4de979aeb8d76841c1401081e0e58d96", size = 345702 }, + { url = "https://files.pythonhosted.org/packages/c8/14/53c84adddedb67ff1546abb86fea04d26e24298c3ceab8436d20122ed0b9/cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f112a71fad6ea824578e6393765ce5c054603afe1471a5c753ff6c67fd872d10", size = 385695 }, + { url = "https://files.pythonhosted.org/packages/bd/80/3ae356c5e7b8d7dc7d1adb52f6932fee85cd748ed4e1217c269d2dfd610f/cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a515df8f8aa6e1eaaf397761a6e4aff2eef73b5f920aedf271416d5471ae5ee", size = 406261 }, + { url = "https://files.pythonhosted.org/packages/0c/31/8e43761ffc82d90bf9cab7e0959712eedcd1e33c211397e143dd42d7af57/cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c398e7b7023460bea2edffe5fcd0a76029580f06c3f6938ac3d198b47156f3", size = 397207 }, + { url = "https://files.pythonhosted.org/packages/d1/b9/fe9da37090b6444c65f848a83e390f87d8cb43d6a4df46de1556ad7e5ceb/cytoolz-1.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3237e56211e03b13df47435b2369f5df281e02b04ad80a948ebd199b7bc10a47", size = 343358 }, +] + +[[package]] +name = "dacite" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/a0/7ca79796e799a3e782045d29bf052b5cde7439a2bbb17f15ff44f7aacc63/dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09", size = 22420 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600 }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686 }, +] + +[[package]] +name = "docformatter" +version = "1.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "untokenize" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/44/aba2c40cf796121b35835ea8c00bc5d93f2f70730eca53b36b8bbbfaefe1/docformatter-1.7.5.tar.gz", hash = "sha256:ffed3da0daffa2e77f80ccba4f0e50bfa2755e1c10e130102571c890a61b246e", size = 25893 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/95/568a2fca29df365b82012b09b64964a05f4f20ac83c2137b262f3fa3188f/docformatter-1.7.5-py3-none-any.whl", hash = "sha256:a24f5545ed1f30af00d106f5d85dc2fce4959295687c24c8f39f5263afaf9186", size = 32798 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "eth-abi" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, + { name = "parsimonious" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/71/d9e1380bd77fd22f98b534699af564f189b56d539cc2b9dab908d4e4c242/eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0", size = 49797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/b4/2f3982c4cbcbf5eeb6aec62df1533c0e63c653b3021ff338d44944405676/eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877", size = 28511 }, +] + +[[package]] +name = "eth-account" +version = "0.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bitarray" }, + { name = "ckzg" }, + { name = "eth-abi" }, + { name = "eth-keyfile" }, + { name = "eth-keys" }, + { name = "eth-rlp" }, + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "pydantic" }, + { name = "rlp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/0d/016e90c67a9c14cd38f4e7b46ef362795fe0f9be1ea4b26026846bdae68e/eth_account-0.13.5.tar.gz", hash = "sha256:010c9ce5f3d2688106cf9bfeb711bb8eaf0154ea6f85325f54fecea85c2b3759", size = 7793978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/b9/d20fc50a5c04b23bd3e2f3d7864b529e0888f928d62b69209c778268e08e/eth_account-0.13.5-py3-none-any.whl", hash = "sha256:e43fd30c9a7fabb882b50e8c4c41d4486d2f3478ad97c66bb18cfcc872fdbec8", size = 580746 }, +] + +[[package]] +name = "eth-hash" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/38/577b7bc9380ef9dff0f1dffefe0c9a1ded2385e7a06c306fd95afb6f9451/eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5", size = 12227 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/db/f8775490669d28aca24871c67dd56b3e72105cb3bcae9a4ec65dd70859b3/eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a", size = 8028 }, +] + +[[package]] +name = "eth-keyfile" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-keys" }, + { name = "eth-utils" }, + { name = "pycryptodome" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/66/dd823b1537befefbbff602e2ada88f1477c5b40ec3731e3d9bc676c5f716/eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1", size = 12267 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/fc/48a586175f847dd9e05e5b8994d2fe8336098781ec2e9836a2ad94280281/eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64", size = 7510 }, +] + +[[package]] +name = "eth-keys" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/72/96db2e3d27c64d3e4a6bf1397447d029e4268fd70b0f1ee4192d6e8d75cd/eth_keys-0.6.1.tar.gz", hash = "sha256:a43e263cbcabfd62fa769168efc6c27b1f5603040e4de22bb84d12567e4fd962", size = 30090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/67/c241c85e9cb9c4d8c14440dd82f7fbe39536592bf59c6b643004ac63eab2/eth_keys-0.6.1-py3-none-any.whl", hash = "sha256:7deae4cd56e862e099ec58b78176232b931c4ea5ecded2f50c7b1ccbc10c24cf", size = 21292 }, +] + +[[package]] +name = "eth-rlp" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "rlp" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ea/ad39d001fa9fed07fad66edb00af701e29b48be0ed44a3bcf58cb3adf130/eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d", size = 7720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/3b/57efe2bc2df0980680d57c01a36516cd3171d2319ceb30e675de19fc2cc5/eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47", size = 4446 }, +] + +[[package]] +name = "eth-typing" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/6f/ecd98de0b67eefc68e17f6979433534a63e11aac88adaae7dede0b694567/eth_typing-5.1.0.tar.gz", hash = "sha256:8581f212ee6252aaa285377a77620f6e5f6e16ac3f144c61f098fafd47967b1a", size = 21727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/2e/40e7577866f4378fb9737e0cb08e3e96e5a25b53821b0139dbfbd77dd66e/eth_typing-5.1.0-py3-none-any.whl", hash = "sha256:c0d6b93f5385aa84efc4b47ae2bd478da069bc0ffda8b67e0ccb573f43defd29", size = 19116 }, +] + +[[package]] +name = "eth-utils" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cytoolz", marker = "implementation_name == 'cpython'" }, + { name = "eth-hash" }, + { name = "eth-typing" }, + { name = "toolz", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e9/f4210d19fa45a23b239017cd75b535d594d8945ef6afc4bb3ab9ee4ff269/eth_utils-5.2.0.tar.gz", hash = "sha256:17e474eb654df6e18f20797b22c6caabb77415a996b3ba0f3cc8df3437463134", size = 120366 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/88/e4e2cc869eaab9a830ac69f213d0609a4f5c5377cde10698cdef6ad2874e/eth_utils-5.2.0-py3-none-any.whl", hash = "sha256:4d43eeb6720e89a042ad5b28d4b2111630ae764f444b85cbafb708d7f076da10", size = 100516 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/79/29d44c4af36b2b240725dce566b20f63f9b36ef267aaaa64ee7466f4f2f8/frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", size = 94451 }, + { url = "https://files.pythonhosted.org/packages/47/47/0c999aeace6ead8a44441b4f4173e2261b18219e4ad1fe9a479871ca02fc/frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", size = 54301 }, + { url = "https://files.pythonhosted.org/packages/8d/60/107a38c1e54176d12e06e9d4b5d755b677d71d1219217cee063911b1384f/frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", size = 52213 }, + { url = "https://files.pythonhosted.org/packages/17/62/594a6829ac5679c25755362a9dc93486a8a45241394564309641425d3ff6/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", size = 240946 }, + { url = "https://files.pythonhosted.org/packages/7e/75/6c8419d8f92c80dd0ee3f63bdde2702ce6398b0ac8410ff459f9b6f2f9cb/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", size = 264608 }, + { url = "https://files.pythonhosted.org/packages/88/3e/82a6f0b84bc6fb7e0be240e52863c6d4ab6098cd62e4f5b972cd31e002e8/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", size = 261361 }, + { url = "https://files.pythonhosted.org/packages/fd/85/14e5f9ccac1b64ff2f10c927b3ffdf88772aea875882406f9ba0cec8ad84/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", size = 231649 }, + { url = "https://files.pythonhosted.org/packages/ee/59/928322800306f6529d1852323014ee9008551e9bb027cc38d276cbc0b0e7/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", size = 241853 }, + { url = "https://files.pythonhosted.org/packages/7d/bd/e01fa4f146a6f6c18c5d34cab8abdc4013774a26c4ff851128cd1bd3008e/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", size = 243652 }, + { url = "https://files.pythonhosted.org/packages/a5/bd/e4771fd18a8ec6757033f0fa903e447aecc3fbba54e3630397b61596acf0/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", size = 241734 }, + { url = "https://files.pythonhosted.org/packages/21/13/c83821fa5544af4f60c5d3a65d054af3213c26b14d3f5f48e43e5fb48556/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", size = 260959 }, + { url = "https://files.pythonhosted.org/packages/71/f3/1f91c9a9bf7ed0e8edcf52698d23f3c211d8d00291a53c9f115ceb977ab1/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", size = 262706 }, + { url = "https://files.pythonhosted.org/packages/4c/22/4a256fdf5d9bcb3ae32622c796ee5ff9451b3a13a68cfe3f68e2c95588ce/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", size = 250401 }, + { url = "https://files.pythonhosted.org/packages/af/89/c48ebe1f7991bd2be6d5f4ed202d94960c01b3017a03d6954dd5fa9ea1e8/frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", size = 45498 }, + { url = "https://files.pythonhosted.org/packages/28/2f/cc27d5f43e023d21fe5c19538e08894db3d7e081cbf582ad5ed366c24446/frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", size = 51622 }, + { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, + { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, + { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, + { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, + { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, + { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, + { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, + { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, + { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, + { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, + { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "grvt-pysdk" +version = "0.2.1" +source = { editable = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "backports-weakref" }, + { name = "dacite" }, + { name = "dataclasses-json" }, + { name = "eth-account" }, + { name = "inflection" }, + { name = "requests" }, + { name = "websockets" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "docformatter" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "twine" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.10.11" }, + { name = "backports-weakref", specifier = ">=1.0.post1" }, + { name = "dacite", specifier = ">=1.8.1" }, + { name = "dataclasses-json", specifier = ">=0.6.7" }, + { name = "eth-account", specifier = ">=0.13.4" }, + { name = "inflection", specifier = ">=0.5.1" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "websockets", specifier = "==13.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.7.9" }, + { name = "docformatter", specifier = ">=1.7.5" }, + { name = "mypy", specifier = ">=1.11.2" }, + { name = "pytest", specifier = ">=8.3.2" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "ruff", specifier = ">=0.6.2" }, + { name = "twine", specifier = ">=5.1.1" }, +] + +[[package]] +name = "hexbytes" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/71/1a3f2439cf138b555c182fffeffbf67c090837e4570370af85ee8e57013f/hexbytes-1.3.0.tar.gz", hash = "sha256:4a61840c24b0909a6534350e2d28ee50159ca1c9e89ce275fd31c110312cf684", size = 8200 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/96/035871b535a728700d3cc5b94cf883706f345c5a088253f26f0bee0b7939/hexbytes-1.3.0-py3-none-any.whl", hash = "sha256:83720b529c6e15ed21627962938dc2dec9bb1010f17bbbd66bf1e6a8287d522c", size = 4902 }, +] + +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, +] + +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "marshmallow" +version = "3.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/3b/7fa1fe835e2e93fd6d7b52b2f95ae810cf5ba133e1845f726f5a992d62c2/more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", size = 125009 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/62/0fe302c6d1be1c777cab0616e6302478251dfbf9055ad426f5d0def75c89/more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89", size = 63038 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, + { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, + { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, + { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, + { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, + { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, + { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, + { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, + { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, + { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, + { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, + { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, + { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, + { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, + { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, + { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, + { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, + { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, + { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, + { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, + { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, + { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, + { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, + { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, + { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, + { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nh3" +version = "0.2.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/f2/eb781d94c7855e9129cbbdd3ab09a470441e4176a82a396ae1df270a7333/nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5", size = 17489 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/65/d31d93b6d1e5fe80d0cc18f0b96eaa561edfa0a15a6ef6b0fce50202a931/nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db", size = 1202187 }, + { url = "https://files.pythonhosted.org/packages/b4/ae/5b03bf198e06921454012e4b9a51e676d26fd37d9fdc1f29371a0b380487/nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a", size = 737822 }, + { url = "https://files.pythonhosted.org/packages/0a/53/a12dffb6ee3772deba82eb5997667fc835afd2e813d1f4080d8738f29eec/nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c", size = 756643 }, + { url = "https://files.pythonhosted.org/packages/d0/0c/6cd2c5ac3e6e31f2a28721e8e2a924cb6b05ad054bf787bd1816ffd40b96/nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2", size = 923415 }, + { url = "https://files.pythonhosted.org/packages/64/f0/229a6c8b81b86ba22d8e7f27ade62cb2fcfb987e570f49944fdd8490a76a/nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5", size = 994959 }, + { url = "https://files.pythonhosted.org/packages/75/e3/62ae3d3b658739ee15b129356fe6d4c4bc8ab235d7bf2e0d2794d64f7bc6/nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0", size = 915777 }, + { url = "https://files.pythonhosted.org/packages/45/bd/8405d03371e335f02eb72e09dcf73307f8fd3095e4165cec6836346fe3db/nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208", size = 908614 }, + { url = "https://files.pythonhosted.org/packages/ee/f8/5d977f09cf82c1f22a864375f471db111530fc79c88efdf0659fe6d3d6bc/nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886", size = 540482 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/e34afe5fd8bed1920eac2974c9c853f548b4b65c139444285ffd2a68495d/nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150", size = 541302 }, + { url = "https://files.pythonhosted.org/packages/92/08/5e3b61eed1bc0efeb330ddc5cf5194f28a0b7be7943aa20bd44cfe14650b/nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776", size = 1202141 }, + { url = "https://files.pythonhosted.org/packages/29/d2/3377f8006c71e95e007b07b5bfcac22c9de4744ca3efb23b396d3deb9581/nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38", size = 760699 }, + { url = "https://files.pythonhosted.org/packages/37/d7/7077f925d7d680d53dcb6e18a4af13d1a7da59761c06c193bfa249a7470a/nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7", size = 747353 }, + { url = "https://files.pythonhosted.org/packages/cb/59/6b2f32af477aae81f1454a7f6ef490ebc3c22dd9e1370e73fcfe243dc07a/nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace", size = 854125 }, + { url = "https://files.pythonhosted.org/packages/5b/f2/c3d2f7b801477b8b387b51fbefd16dc7ade888aeac547f18ba0558fd6f48/nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6", size = 817453 }, + { url = "https://files.pythonhosted.org/packages/42/4d/f7e3a35506a0eba6eedafc21ad52773985511eb838812e9f96354831ad3c/nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b", size = 891694 }, + { url = "https://files.pythonhosted.org/packages/e6/0e/c499453c296fb40366e3069cd68fde77a10f0a30a17b9d3b491eb3ebc5bf/nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b", size = 744388 }, + { url = "https://files.pythonhosted.org/packages/18/67/c3de8022ba2719bdbbdd3704d1e32dbc7d3f8ac8646247711645fc90d051/nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d", size = 764831 }, + { url = "https://files.pythonhosted.org/packages/f0/14/a4ea40e2439717d11c3104fc2dc0ac412301b7aeb81d6a3d0e6505c77e7d/nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a", size = 923334 }, + { url = "https://files.pythonhosted.org/packages/ed/ae/e8ee8afaf67903dd304f390056d1ea620327524e2ad66127a331b14d5d98/nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397", size = 994873 }, + { url = "https://files.pythonhosted.org/packages/20/b5/02122cfe3b36cf0ba0fcd73a04fd462e1f7a9d91b456f6e0b70e46df21c7/nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec", size = 915707 }, + { url = "https://files.pythonhosted.org/packages/47/d3/5df43cc3570cdc9eb1dc79a39191f89fedf8bcefd8d30a161ff1dffb146c/nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330", size = 908539 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/aa000f6c76a832c488eac26f20d2e8a221ba2b965efce692f14ebc4290bf/nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784", size = 540439 }, + { url = "https://files.pythonhosted.org/packages/19/31/d65594efd3b42b1de2335d576eb77525691fc320dbf8617948ee05c008e5/nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b", size = 541249 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "parsimonious" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/91/abdc50c4ef06fdf8d047f60ee777ca9b2a7885e1a9cea81343fbecda52d7/parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c", size = 52172 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0f/c8b64d9b54ea631fcad4e9e3c8dbe8c11bb32a623be94f22974c88e71eaf/parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f", size = 48427 }, +] + +[[package]] +name = "pbr" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a5/0ea64c9426959ef145a938e38c832fc551843481d356713ececa9a8a64e8/propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", size = 79296 }, + { url = "https://files.pythonhosted.org/packages/76/5a/916db1aba735f55e5eca4733eea4d1973845cf77dfe67c2381a2ca3ce52d/propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", size = 45622 }, + { url = "https://files.pythonhosted.org/packages/2d/62/685d3cf268b8401ec12b250b925b21d152b9d193b7bffa5fdc4815c392c2/propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", size = 45133 }, + { url = "https://files.pythonhosted.org/packages/4d/3d/31c9c29ee7192defc05aa4d01624fd85a41cf98e5922aaed206017329944/propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212", size = 204809 }, + { url = "https://files.pythonhosted.org/packages/10/a1/e4050776f4797fc86140ac9a480d5dc069fbfa9d499fe5c5d2fa1ae71f07/propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", size = 219109 }, + { url = "https://files.pythonhosted.org/packages/c9/c0/e7ae0df76343d5e107d81e59acc085cea5fd36a48aa53ef09add7503e888/propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", size = 217368 }, + { url = "https://files.pythonhosted.org/packages/fc/e1/e0a2ed6394b5772508868a977d3238f4afb2eebaf9976f0b44a8d347ad63/propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", size = 205124 }, + { url = "https://files.pythonhosted.org/packages/50/c1/e388c232d15ca10f233c778bbdc1034ba53ede14c207a72008de45b2db2e/propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", size = 195463 }, + { url = "https://files.pythonhosted.org/packages/0a/fd/71b349b9def426cc73813dbd0f33e266de77305e337c8c12bfb0a2a82bfb/propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", size = 198358 }, + { url = "https://files.pythonhosted.org/packages/02/f2/d7c497cd148ebfc5b0ae32808e6c1af5922215fe38c7a06e4e722fe937c8/propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", size = 195560 }, + { url = "https://files.pythonhosted.org/packages/bb/57/f37041bbe5e0dfed80a3f6be2612a3a75b9cfe2652abf2c99bef3455bbad/propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", size = 196895 }, + { url = "https://files.pythonhosted.org/packages/83/36/ae3cc3e4f310bff2f064e3d2ed5558935cc7778d6f827dce74dcfa125304/propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", size = 207124 }, + { url = "https://files.pythonhosted.org/packages/8c/c4/811b9f311f10ce9d31a32ff14ce58500458443627e4df4ae9c264defba7f/propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", size = 210442 }, + { url = "https://files.pythonhosted.org/packages/18/dd/a1670d483a61ecac0d7fc4305d91caaac7a8fc1b200ea3965a01cf03bced/propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", size = 203219 }, + { url = "https://files.pythonhosted.org/packages/f9/2d/30ced5afde41b099b2dc0c6573b66b45d16d73090e85655f1a30c5a24e07/propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", size = 40313 }, + { url = "https://files.pythonhosted.org/packages/23/84/bd9b207ac80da237af77aa6e153b08ffa83264b1c7882495984fcbfcf85c/propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", size = 44428 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, + { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, + { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, + { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, + { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, + { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, + { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, + { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, + { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, + { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, + { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, + { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, + { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, + { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, + { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pycryptodome" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, + { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, + { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, + { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, + { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, + { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, + { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, + { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, + { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, + { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, + { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, + { url = "https://files.pythonhosted.org/packages/08/16/ae464d4ac338c1dd41f89c41f9488e54f7d2a3acf93bb920bb193b99f8e3/pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", size = 1615855 }, + { url = "https://files.pythonhosted.org/packages/1e/8c/b0cee957eee1950ce7655006b26a8894cee1dc4b8747ae913684352786eb/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", size = 1650018 }, + { url = "https://files.pythonhosted.org/packages/93/4d/d7138068089b99f6b0368622e60f97a577c936d75f533552a82613060c58/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", size = 1687977 }, + { url = "https://files.pythonhosted.org/packages/96/02/90ae1ac9f28be4df0ed88c127bf4acc1b102b40053e172759d4d1c54d937/pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6", size = 1788273 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rlp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2d/439b0728a92964a04d9c88ea1ca9ebb128893fbbd5834faa31f987f2fd4c/rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9", size = 33429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973 }, +] + +[[package]] +name = "ruff" +version = "0.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, + { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, + { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, + { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, + { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, + { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, + { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, + { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, + { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, + { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, + { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, + { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, + { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, + { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, + { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, + { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + +[[package]] +name = "setuptools" +version = "75.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, +] + +[[package]] +name = "stevedore" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pbr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/e9/4eedccff8332cc40cc60ddd3b28d4c3e255ee7e9c65679fa4533ab98f598/stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d", size = 513899 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/73/d0091d22a65b55e8fb6aca7b3b6713b5a261dd01cec4cfd28ed127ac0cfc/stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857", size = 49534 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383 }, +] + +[[package]] +name = "twine" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 }, +] + +[[package]] +name = "untokenize" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz", hash = "sha256:3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2", size = 3099 } + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "websockets" +version = "13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815 }, + { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466 }, + { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716 }, + { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806 }, + { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810 }, + { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125 }, + { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532 }, + { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948 }, + { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898 }, + { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706 }, + { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141 }, + { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813 }, + { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469 }, + { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717 }, + { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379 }, + { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376 }, + { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753 }, + { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051 }, + { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489 }, + { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438 }, + { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710 }, + { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137 }, + { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821 }, + { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480 }, + { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715 }, + { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647 }, + { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592 }, + { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012 }, + { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311 }, + { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692 }, + { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686 }, + { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712 }, + { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145 }, + { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828 }, + { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487 }, + { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721 }, + { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609 }, + { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556 }, + { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993 }, + { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360 }, + { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745 }, + { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732 }, + { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709 }, + { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144 }, + { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499 }, + { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737 }, + { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095 }, + { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701 }, + { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654 }, + { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192 }, + { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, + { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, + { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, + { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, + { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, + { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, + { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, + { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, + { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, + { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, + { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, + { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, + { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, + { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, + { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] diff --git a/docs/grvt/sdk-readme.md b/docs/grvt/sdk-readme.md index f59556b..3f5b109 100644 --- a/docs/grvt/sdk-readme.md +++ b/docs/grvt/sdk-readme.md @@ -1,231 +1,62 @@ -# GRVT TypeScript SDK +# GRVT -This SDK provides a TypeScript interface to interact with the GRVT API. It supports both REST API and WebSocket connections. +Node.js & JavaScript client for GRVT REST APIs & WebSockets -## Installation +## Installing + +Using npm: ```bash -npm install @grvt/sdk +npm install @grvt/client ``` -## Usage - -### REST API Client - -```typescript -import { - ECurrency, - ETransferType, - ITransferMetadata, - ETransferProvider, - ETransferDirection, - EGrvtEnvironment, - EChain, - ISigningOption -} from '@grvt/sdk'; - -// Initialize the client -const client = new GrvtClient({ - apiKey: 'your-api-key', - apiSecret: 'your-api-secret', - env: EGrvtEnvironment.DEV, -}); - -// Get funding account summary -const accountSummary = await client.getFundingAccountSummary(); - -// Get sub account summary -const subAccountSummary = await client.getSubAccountSummary({ - sub_account_id: 'your-sub-account-id', -}); - -// Transfer examples -// Note: the signature field is optional. If not provided, the SDK will automatically compute it using the apiSecret and provided signing options - -// Standard transfer -const transfer1 = await client.transfer({ - from_account_id: 'from-account-id', - from_sub_account_id: 'from-sub-account-id', - to_account_id: 'to-account-id', - to_sub_account_id: 'to-sub-account-id', - currency: ECurrency.USDT, - num_tokens: '100', - transfer_type: ETransferType.STANDARD, -}); - - -// Metadata for transfer, you can pass it as the second argument for the transfer API -const metadata: ITransferMetadata = { - provider: ETransferProvider.RHINO; - direction: ETransferDirection.DEPOSIT; // Use ETransferDirection.WITHDRAWAL for withdraw flow - chainid: Echain.TRON, - endpoint, - provider_tx_id: tx_hash, - provider_ref_id: commit_id, -}; - -// Signing options for generating the signature as the third argument for the transfer API -// Note: nonce must be non-negative and expiration must be within 30 days -const signingOptions: ISigningOption = { - nonce: 12345, - expiration: '1746093221289693252' -}; - -const transfer2 = await client.transfer( - { - from_account_id: 'from-account-id', - to_account_id: 'to-account-id', - currency: ECurrency.USDT, - num_tokens: '100', - transfer_type: ETransferType.NON_NATIVE_BRIDGE_DEPOSIT, // Use NON_NATIVE_BRIDGE_WITHDRAW for withdraw flow - }, - metadata, - signingOptions -); - -// Request deposit approval -// This API is used to get signature for a deposit before executing it -const depositApproval = await client.requestDepositApproval({ - l1Sender: 'your-l1-address', // L1 address of the sending wallet - l2Receiver: 'your-l2-address', // Your L2 address to receive the funds - l1Token: 'token-contract-address', // L1 token contract address - amount: '100' // Amount to deposit -}); - - -// Withdraw funds from your account -// Note: the signature field is optional. If not provided, the SDK will automatically compute it using the apiSecret and provided signing options -const withdrawResult = await client.withdraw({ - from_account_id: 'your-account-id', - to_eth_address: 'destination-eth-address', - currency: ECurrency.USDT, - num_tokens: '100' -}); - -// Query transfer history -const transferHistory = await client.getTransferHistory({ - start_time: '1745600642000785050' // timestamp in nanosecond, use this to filter transfers with event time >= start_time - end_time: '17588917787741000000' // timestamp in nanoseconds, use this to filter transfers with event time <= end_time -}); -// You can filter more & do pagination with this query if needed, please take a look at the request interface to get more details - -// Query deposit history -const depositHistory = await client.getDepositHistory({ - start_time: '1745600642000785050' // timestamp in nanosecond, use this to filter deposits with event time >= start_time - end_time: '17588917787741000000' // timestamp in nanoseconds, use this to filter deposits with event time <= end_time -}); -// You can filter more & do pagination with this query if needed, please take a look at the request interface to get more details - - -// Get current server time, in milliseconds since epoch -const currentTime = await client.getCurrentTime() -// Example result: 1747397398409 - -// Convert Rhino chain to Gravity Echain -// Result will depend on the environment, specifically -// - DEV, STAGING - Rhino DEV -// - TESTNET - Rhino STG -// - PRODUCTIOn - Rhino PROD -// This will return null if the chain ID is not found or not supported -import { SupportedChains } from "@rhino.fi/sdk" - -const chainID = await client.getGravityChainIDFromRhinoChain(SupportedChains.BNB_SMART_CHAIN) -// Result: -// - On DEV/STAGING/TESTNET: 97 -// - On PRODUCTION: 56 - -``` - -### WebSocket Client - -The WebSocket client supports real-time data streaming and follows the same authentication mehanism as the REST API client. - -```typescript -import { GrvtWsClient, EGrvtEnvironment } from '@grvt/sdk'; - -// Initialize the WebSocket client -const client = new GrvtWsClient({ - apiKey: 'your-api-key', - env: EGrvtEnvironment.DEV, -}); - -// Connect to WebSocket -await client.connect(); - -// Subscribe to transfer history -client.subscribeTransferHistory( - 'main-account-id', - (data) => { - console.log('Received transfer:', data); - }, - 'sub-account-id' // optional -); - -// Disconnect when done -client.disconnect(); -``` - -#### WebSocket Features - -1. **Authentication**: - - - Uses the same cookie-based authentication as the REST API - - Automatically refreshes cookies when needed - -2. **Connection Management**: - - - Automatic reconnection with exponential backoff - - Connection monitoring with 5-second timeout - - Reconnects if no messages are received within the timeout period - -3. **Subscription Handling**: - - - Unique subscription IDs for each subscription - - Subscriptions are not automatically restored after reconnection - - Users need to manually resubscribe after reconnection - -4. **Error Handling**: - - Automatic error logging - - Graceful disconnection handling - - Reconnection attempts with configurable maximum retries - -## Development - -### Building +Using yarn: ```bash -npm run build +yarn add @grvt/client ``` -### Formatting +Using pnpm: ```bash -npm run format +pnpm add @grvt/client ``` -### Testing +Once the package is installed, you can import the library using `import` or `require` approach: -```bash -# Run all tests -npm test - -# Run SDK tests -npm run test:sdk - -# Run WebSocket tests -npm run test:ws +```js +import GRVT from '@grvt/client' ``` -### Linting and Formating +You can also use the default export, since the named export is just a re-export from the GRVT factory: -```bash -npm run lint +```js +import GRVT from '@grvt/client' +console.log( + new GRVT.MDG({ + host: 'https://market-data.dev.gravitymarkets.io', + version: 'v1' + }) +) ``` -```bash -npm run format +If you use `require` for importing: + +```js +const GRVT = require('@grvt/client') +console.log( + new GRVT.MDG({ + host: 'https://market-data.dev.gravitymarkets.io', + version: 'v1' + }) +) ``` -## License +## To use WebSocket (available only in browsers/platforms that support WebSocket) -This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +[Browsers supported](https://caniuse.com/websockets) + +```js +import { EStreamEndpoints, WS } from '@grvt/client/ws' +console.log(new WS('wss://market-data.dev.gravitymarkets.io/ws')) +``` \ No newline at end of file diff --git a/package.json b/package.json index e715c1f..8e038b9 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typescript": "^5" }, "dependencies": { - "@grvt/sdk": "^0.0.1-beta.17", + "@grvt/client": "^1.6.4", "axios": "^1.12.2", "ccxt": "^4.5.5", "dotenv": "^17.2.2", diff --git a/src/exchanges/create-adapter.ts b/src/exchanges/create-adapter.ts new file mode 100644 index 0000000..cb86e98 --- /dev/null +++ b/src/exchanges/create-adapter.ts @@ -0,0 +1,29 @@ +import type { ExchangeAdapter } from "./adapter"; +import { AsterExchangeAdapter, type AsterCredentials } from "./aster-adapter"; +import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter"; + +export interface ExchangeFactoryOptions { + symbol: string; + exchange?: string; + aster?: AsterCredentials; + grvt?: GrvtCredentials; +} + +export type SupportedExchangeId = "aster" | "grvt"; + +export function resolveExchangeId(value?: string | null): SupportedExchangeId { + const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster") + .toString() + .trim() + .toLowerCase(); + if (fallback === "grvt") return "grvt"; + return "aster"; +} + +export function createExchangeAdapter(options: ExchangeFactoryOptions): ExchangeAdapter { + const id = resolveExchangeId(options.exchange); + if (id === "grvt") { + return new GrvtExchangeAdapter({ ...options.grvt, symbol: options.symbol }); + } + return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol }); +} diff --git a/src/exchanges/grvt/adapter.ts b/src/exchanges/grvt/adapter.ts new file mode 100644 index 0000000..a7c6be5 --- /dev/null +++ b/src/exchanges/grvt/adapter.ts @@ -0,0 +1,245 @@ +import { setTimeout, clearTimeout } from "timers"; +import path from "path"; +import { createRequire } from "module"; +import type { + AccountListener, + DepthListener, + ExchangeAdapter, + KlineListener, + OrderListener, + TickerListener, +} from "../adapter"; +import type { AsterOrder, CreateOrderParams } from "../types"; +import { extractMessage } from "../../utils/errors"; +import { + GrvtGateway, + type GrvtEnvironment, + type GrvtGatewayOptions, + type GrvtHostsOverride, + type GrvtSignatureProvider, +} from "./gateway"; + +export interface GrvtCredentials { + cookie?: string; + accountId?: string; + apiKey?: string; + apiSecret?: string; + subAccountId?: string; + instrument?: string; + symbol?: string; + env?: GrvtEnvironment; + hosts?: GrvtHostsOverride; + signatureProvider?: GrvtSignatureProvider; + pollIntervals?: GrvtGatewayOptions["pollIntervals"]; + logger?: GrvtGatewayOptions["logger"]; +} + +export class GrvtExchangeAdapter implements ExchangeAdapter { + readonly id = "grvt"; + + private readonly gateway: GrvtGateway; + private readonly symbol: string; + private readonly instrument: string; + private initPromise: Promise | null = null; + private readonly initContexts = new Set(); + private retryTimer: ReturnType | null = null; + private retryDelayMs = 3000; + private lastInitErrorAt = 0; + private klineInterval = "1m"; + + constructor(credentials: GrvtCredentials = {}) { + const apiKey = credentials.apiKey ?? process.env.GRVT_API_KEY; + const apiSecret = credentials.apiSecret ?? process.env.GRVT_API_SECRET; + const cookie = credentials.cookie ?? process.env.GRVT_COOKIE; + const accountId = credentials.accountId ?? process.env.GRVT_ACCOUNT_ID; + if (!cookie || !accountId) { + if (!apiKey) { + throw new Error("Missing GRVT_API_KEY environment variable for authentication"); + } + } + + const subAccountId = requireValue( + credentials.subAccountId ?? process.env.GRVT_SUB_ACCOUNT_ID, + "GRVT_SUB_ACCOUNT_ID" + ); + const instrument = requireValue( + credentials.instrument ?? process.env.GRVT_INSTRUMENT, + "GRVT_INSTRUMENT" + ); + const symbol = normalizeSymbol(credentials.symbol ?? process.env.GRVT_SYMBOL, instrument); + this.symbol = symbol; + this.instrument = instrument; + const signatureProvider = + credentials.signatureProvider ?? loadSignatureProviderFromEnv(credentials.logger); + if (!signatureProvider && !apiSecret) { + throw new Error( + "GRVT_API_SECRET is required when no external signature provider is configured" + ); + } + + this.gateway = new GrvtGateway({ + apiKey: apiKey ?? undefined, + apiSecret: apiSecret ?? undefined, + cookie: cookie ?? undefined, + accountId: accountId ?? undefined, + subAccountId, + instrument, + symbol, + env: (credentials.env ?? process.env.GRVT_ENV) as GrvtEnvironment | undefined, + hosts: credentials.hosts, + signatureProvider, + pollIntervals: credentials.pollIntervals, + logger: credentials.logger, + }); + } + + watchAccount(cb: AccountListener): void { + void this.ensureInitialized("watchAccount"); + this.gateway.onAccount(this.safeInvoke("watchAccount", cb)); + } + + watchOrders(cb: OrderListener): void { + void this.ensureInitialized("watchOrders"); + this.gateway.onOrders(this.safeInvoke("watchOrders", cb)); + } + + watchDepth(_symbol: string, cb: DepthListener): void { + void this.ensureInitialized("watchDepth"); + this.gateway.onDepth(this.safeInvoke("watchDepth", cb)); + } + + watchTicker(_symbol: string, cb: TickerListener): void { + void this.ensureInitialized("watchTicker"); + this.gateway.onTicker(this.safeInvoke("watchTicker", cb)); + } + + watchKlines(_symbol: string, interval: string, cb: KlineListener): void { + this.klineInterval = interval ?? this.klineInterval; + void this.ensureInitialized("watchKlines", this.klineInterval); + this.gateway.onKlines(this.safeInvoke("watchKlines", cb)); + } + + async createOrder(params: CreateOrderParams): Promise { + await this.ensureInitialized("createOrder"); + return this.gateway.createOrder(params); + } + + async cancelOrder(params: { symbol: string; orderId: number | string }): Promise { + await this.ensureInitialized("cancelOrder"); + await this.gateway.cancelOrder(params); + } + + async cancelOrders(params: { symbol: string; orderIdList: Array }): Promise { + await this.ensureInitialized("cancelOrders"); + await this.gateway.cancelOrders(params); + } + + async cancelAllOrders(_params: { symbol: string }): Promise { + await this.ensureInitialized("cancelAllOrders"); + await this.gateway.cancelAllOrders(); + } + + private safeInvoke void>(context: string, cb: T): T { + const wrapped = ((...args: any[]) => { + try { + cb(...args); + } catch (error) { + console.error(`[GrvtExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`); + } + }) as T; + return wrapped; + } + + private ensureInitialized(context?: string, interval?: string): Promise { + if (interval) { + this.klineInterval = interval; + } + if (!this.initPromise) { + this.initContexts.clear(); + this.initPromise = this.gateway + .ensureInitialized(this.klineInterval) + .then((value) => { + this.clearRetry(); + return value; + }) + .catch((error) => { + this.handleInitError("initialize", error); + this.initPromise = null; + this.scheduleRetry(); + throw error; + }); + } + if (context && !this.initContexts.has(context)) { + this.initContexts.add(context); + this.initPromise.catch((error) => { + this.handleInitError(context, error); + this.scheduleRetry(); + }); + } + return this.initPromise; + } + + private scheduleRetry(): void { + if (this.retryTimer) return; + this.retryTimer = setTimeout(() => { + this.retryTimer = null; + if (this.initPromise) return; + this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000); + void this.ensureInitialized("retry"); + }, this.retryDelayMs); + } + + private clearRetry(): void { + if (this.retryTimer) { + clearTimeout(this.retryTimer); + this.retryTimer = null; + } + this.retryDelayMs = 3000; + } + + private handleInitError(context: string, error: unknown): void { + const now = Date.now(); + if (now - this.lastInitErrorAt < 5000) return; + this.lastInitErrorAt = now; + console.error(`[GrvtExchangeAdapter] ${context} failed`, error); + } +} + +function requireValue(value: T | undefined | null, key: string): T { + if (value == null || value === "") { + throw new Error(`Missing required environment variable ${key}`); + } + return value; +} + +function normalizeSymbol(symbol: string | undefined, instrument: string): string { + if (symbol) return symbol.toUpperCase(); + return instrument.replace(/[_-]/g, "").toUpperCase(); +} + +function loadSignatureProviderFromEnv( + logger?: GrvtGatewayOptions["logger"] +): GrvtSignatureProvider | undefined { + const signerModule = process.env.GRVT_SIGNER_PATH; + if (!signerModule) return undefined; + try { + const require = createRequire(import.meta.url); + const resolved = signerModule.startsWith(".") || signerModule.startsWith("/") + ? path.resolve(process.cwd(), signerModule) + : signerModule; + const loaded = require(resolved); + if (typeof loaded === "function") { + return loaded as GrvtSignatureProvider; + } + if (loaded && typeof loaded.default === "function") { + return loaded.default as GrvtSignatureProvider; + } + console.warn( + `[GrvtExchangeAdapter] 模块 ${resolved} 未导出签名函数 (function default export)` + ); + } catch (error) { + const log = logger ?? ((ctx, err) => console.error(`[GrvtExchangeAdapter] ${ctx}`, err)); + log("loadSignatureProvider", error); + } + return undefined; +} diff --git a/src/exchanges/grvt/client.ts b/src/exchanges/grvt/client.ts deleted file mode 100644 index f9514cb..0000000 --- a/src/exchanges/grvt/client.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GrvtClient, GrvtWsClient, EGrvtEnvironment } from "@grvt/sdk"; - -export interface GrvtCredentials { - apiKey: string; - apiSecret: string; - env: EGrvtEnvironment; - subAccountId: string; - instrument: string; -} - -export class GrvtGateway { - private readonly grvtClient: GrvtClient; - private readonly wsClient: GrvtWsClient; - - constructor(private readonly credentials: GrvtCredentials) { - this.grvtClient = new GrvtClient({ - apiKey: credentials.apiKey, - apiSecret: credentials.apiSecret, - env: credentials.env, - }); - this.wsClient = new GrvtWsClient({ apiKey: credentials.apiKey, env: credentials.env }); - } - - async initialize(): Promise { - await this.wsClient.connect(); - } -} - diff --git a/src/exchanges/grvt/gateway.ts b/src/exchanges/grvt/gateway.ts index eb2806b..31c92f0 100644 --- a/src/exchanges/grvt/gateway.ts +++ b/src/exchanges/grvt/gateway.ts @@ -1,269 +1,1359 @@ -import { GrvtClient, GrvtWsClient, EGrvtEnvironment } from "@grvt/sdk"; -import { TDG } from "@grvt/client"; - +import { setInterval, clearInterval } from "timers"; +import { randomInt } from "crypto"; +import axios from "axios"; +import { TDG, MDG } from "@grvt/client"; +import { keccak256 } from "ethereum-cryptography/keccak"; +import { secp256k1 } from "ethereum-cryptography/secp256k1"; +import { bytesToHex, hexToBytes, utf8ToBytes, concatBytes } from "ethereum-cryptography/utils"; +import { extractMessage } from "../../utils/errors"; import type { - GrvtAccountSnapshot, + IApiSubAccountSummaryResponse, + IApiPositionsResponse, + IApiOpenOrdersResponse, + IApiOrderbookLevelsResponse, + IApiTickerResponse, + IApiCandlestickResponse, + IApiCreateOrderResponse, +} from "@grvt/client/interfaces"; +import type { + AsterAccountSnapshot, + AsterAccountPosition, + AsterDepth, + AsterKline, + AsterOrder, + AsterTicker, + CreateOrderParams, + OrderSide, GrvtOrder, - GrvtOrderUpdateFeed, - GrvtPosition, - GrvtPositionUpdateFeed, + GrvtSignedOrder, + GrvtSignature, + GrvtUnsignedOrder, } from "../types"; -export interface GrvtGatewayOptions { - apiKey: string; - apiSecret: string; +const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000; +const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500; +const DEFAULT_DEPTH_POLL_INTERVAL_MS = 750; +const DEFAULT_TICKER_POLL_INTERVAL_MS = 1000; +const DEFAULT_KLINE_POLL_INTERVAL_MS = 15000; + +const ENVIRONMENT_HOSTS = { + prod: { + trades: "https://trades.grvt.io", + market: "https://market-data.grvt.io", + edge: "https://edge.grvt.io", + }, + testnet: { + trades: "https://trades.testnet.grvt.io", + market: "https://market-data.testnet.grvt.io", + edge: "https://edge.testnet.grvt.io", + }, + staging: { + trades: "https://trades.staging.gravitymarkets.io", + market: "https://market-data.staging.gravitymarkets.io", + edge: "https://edge.staging.gravitymarkets.io", + }, + dev: { + trades: "https://trades.dev.gravitymarkets.io", + market: "https://market-data.dev.gravitymarkets.io", + edge: "https://edge.dev.gravitymarkets.io", + }, +} as const; + +const ENVIRONMENT_ALIASES: Record = { + mainnet: "prod", + production: "prod", +}; + +const DEFAULT_MARK_PRICE_TRIGGER = "MARK"; +const DEFAULT_TIME_IN_FORCE = "GOOD_TILL_TIME"; +const TRAILING_NOT_SUPPORTED_ERROR = + "GRVT exchange adapter does not yet support trailing stop orders"; + +const CHAIN_IDS: Record = { + prod: 325, + testnet: 326, + staging: 327, + dev: 327, +}; + +const DEFAULT_ORDER_EXPIRY_NS = BigInt(5 * 60) * 1_000_000_000n; + +type EIP712Types = Record>; + +const EIP712_ORDER_TYPES = { + Order: [ + { name: "subAccountID", type: "uint64" }, + { name: "isMarket", type: "bool" }, + { name: "timeInForce", type: "uint8" }, + { name: "postOnly", type: "bool" }, + { name: "reduceOnly", type: "bool" }, + { name: "legs", type: "OrderLeg[]" }, + { name: "nonce", type: "uint32" }, + { name: "expiration", type: "int64" }, + ], + OrderLeg: [ + { name: "assetID", type: "uint256" }, + { name: "contractSize", type: "uint64" }, + { name: "limitPrice", type: "uint64" }, + { name: "isBuyingContract", type: "bool" }, + ], +} as const; + +const EIP712_DOMAIN_FIELDS = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, +] as const; + +type BaseEnvironment = keyof typeof ENVIRONMENT_HOSTS; +export type GrvtEnvironment = BaseEnvironment | keyof typeof ENVIRONMENT_ALIASES; + +interface GrvtSignContext { + order: GrvtUnsignedOrder; + quantity: number; + price?: number; + side: OrderSide; + isMarket: boolean; + timeInForce: string; + postOnly: boolean; + reduceOnly: boolean; + nonce: number; + expirationNs: bigint; + instrument: InstrumentInfo; + chainId: number; subAccountId: string; - instrument: string; - env?: EGrvtEnvironment; } +export interface GrvtSignatureProvider { + (context: GrvtSignContext): Promise; +} + +export interface GrvtHostsOverride { + trades?: string; + market?: string; + edge?: string; +} + +export interface GrvtGatewayOptions { + cookie?: string; + accountId?: string; + apiKey?: string; + apiSecret?: string; + subAccountId: string; + instrument: string; + symbol: string; + env?: GrvtEnvironment; + hosts?: GrvtHostsOverride; + signatureProvider?: GrvtSignatureProvider; + pollIntervals?: Partial<{ + account: number; + orders: number; + depth: number; + ticker: number; + klines: number; + }>; + logger?: (context: string, error: unknown) => void; +} + +interface HostsConfig { + trades: string; + market: string; + edge: string; +} + +interface KlineCache { + interval: string; + values: AsterKline[]; +} + +interface InstrumentInfo { + instrument: string; + instrumentHash: string; + baseDecimals: number; + quoteDecimals: number; +} + +interface SessionInfo { + cookie: string; + accountId: string; + expiresAt?: number; +} + +const ONE_MILLISECOND_IN_NANOSECONDS = 1_000_000; +const ONE_SECOND_IN_NANOSECONDS = 1_000_000_000; + export class GrvtGateway { - private readonly client: GrvtClient; - private readonly ws: GrvtWsClient; private readonly tdg: TDG; - - private accountSnapshot: GrvtAccountSnapshot | null = null; - private openOrders: GrvtOrder[] = []; - private positions: GrvtPosition[] = []; + private readonly mdg: MDG; + private readonly signatureProvider?: GrvtSignatureProvider; + private readonly logger: (context: string, error: unknown) => void; private readonly instrument: string; - private snapshotListeners = new Set<(snapshot: GrvtAccountSnapshot) => void>(); - private ordersListeners = new Set<(orders: GrvtOrder[]) => void>(); - private positionsListeners = new Set<(positions: GrvtPosition[]) => void>(); + private readonly symbol: string; + private readonly subAccountId: string; + private readonly pollIntervals: Required; + private readonly hosts: HostsConfig; + private readonly chainId: number; + private headers: Record = {}; + private apiKey?: string; + private apiSecret?: string; + private sessionInfo: SessionInfo | null = null; + private instrumentInfo: InstrumentInfo | null = null; + private sessionPromise: Promise | null = null; - constructor(private readonly options: GrvtGatewayOptions) { - const environment = options.env ?? (process.env.GRVT_ENV as EGrvtEnvironment) ?? EGrvtEnvironment.TESTNET; - this.client = new GrvtClient({ - apiKey: options.apiKey, - apiSecret: options.apiSecret, - env: environment, - }); - this.ws = new GrvtWsClient({ apiKey: options.apiKey, env: environment }); - this.tdg = new TDG({ host: `https://trades.${this.client["domain"]}/lite/v1` }); - this.tdg = new TDG({ host: `https://trades.${this.client["domain"]}` }); + private accountSnapshot: AsterAccountSnapshot | null = null; + private openOrders: AsterOrder[] = []; + private positions: AsterAccountPosition[] = []; + private depthSnapshot: AsterDepth | null = null; + private tickerSnapshot: AsterTicker | null = null; + private klineCache: KlineCache | null = null; + + private accountListeners = new Set<(snapshot: AsterAccountSnapshot) => void>(); + private ordersListeners = new Set<(orders: AsterOrder[]) => void>(); + private depthListeners = new Set<(depth: AsterDepth) => void>(); + private tickerListeners = new Set<(ticker: AsterTicker) => void>(); + private klineListeners = new Set<(klines: AsterKline[]) => void>(); + + private accountTimer: ReturnType | null = null; + private ordersTimer: ReturnType | null = null; + private depthTimer: ReturnType | null = null; + private tickerTimer: ReturnType | null = null; + private klineTimer: ReturnType | null = null; + + private initialized = false; + private klineInterval = "CI_1_M"; + + constructor(options: GrvtGatewayOptions) { + const envKey = normalizeEnvironment(options.env); + this.hosts = resolveHosts(envKey, options.hosts); + this.chainId = CHAIN_IDS[envKey]; + this.tdg = new TDG({ host: this.hosts.trades }); + this.mdg = new MDG({ host: this.hosts.market }); + this.subAccountId = options.subAccountId; this.instrument = options.instrument; + this.symbol = options.symbol; + this.signatureProvider = options.signatureProvider; + this.logger = options.logger ?? defaultLogger; + this.pollIntervals = { + account: options.pollIntervals?.account ?? DEFAULT_ACCOUNT_POLL_INTERVAL_MS, + orders: options.pollIntervals?.orders ?? DEFAULT_ORDERS_POLL_INTERVAL_MS, + depth: options.pollIntervals?.depth ?? DEFAULT_DEPTH_POLL_INTERVAL_MS, + ticker: options.pollIntervals?.ticker ?? DEFAULT_TICKER_POLL_INTERVAL_MS, + klines: options.pollIntervals?.klines ?? DEFAULT_KLINE_POLL_INTERVAL_MS, + }; + this.apiKey = options.apiKey; + this.apiSecret = options.apiSecret ?? undefined; + + if (options.cookie && options.accountId) { + const normalizedCookie = normalizeCookieValue(options.cookie); + this.sessionInfo = { + cookie: normalizedCookie, + accountId: options.accountId, + }; + this.updateHeaders(); + } + + this.tdg.axios.interceptors.request.use(async (config) => { + await this.ensureSession(); + config.headers = { ...(config.headers ?? {}), ...this.headers }; + return config; + }); + + this.mdg.axios.interceptors.request.use(async (config) => { + await this.ensureSession(); + config.headers = { ...(config.headers ?? {}), ...this.headers }; + return config; + }); } - async initialize(): Promise { - await this.refreshAccountSnapshot(); - await this.refreshOpenOrders(); - await this.refreshPositions(); - await this.ws.connect(); - this.subscribeStreams(); + async ensureInitialized(klineInterval: string): Promise { + if (this.initialized) return; + this.klineInterval = mapIntervalToGrvt(klineInterval); + await this.ensureSession(); + await this.loadInstrumentInfo(); + await Promise.all([ + this.refreshAccountSnapshot(), + this.refreshOpenOrders(), + this.refreshPositions(), + this.refreshDepth(), + this.refreshTicker(), + this.refreshKlines(), + ]); + this.startPolling(); + this.initialized = true; } - onAccount(listener: (snapshot: GrvtAccountSnapshot) => void): () => void { - this.snapshotListeners.add(listener); - if (this.accountSnapshot) listener(this.accountSnapshot); + onAccount(listener: (snapshot: AsterAccountSnapshot) => void): () => void { + this.accountListeners.add(listener); + if (this.accountSnapshot) listener(cloneAccount(this.accountSnapshot)); return () => { - this.snapshotListeners.delete(listener); + this.accountListeners.delete(listener); }; } - onOrders(listener: (orders: GrvtOrder[]) => void): () => void { + onOrders(listener: (orders: AsterOrder[]) => void): () => void { this.ordersListeners.add(listener); - if (this.openOrders.length) listener([...this.openOrders]); + if (this.openOrders.length) listener(cloneOrders(this.openOrders)); return () => { this.ordersListeners.delete(listener); }; } - onPositions(listener: (positions: GrvtPosition[]) => void): () => void { - this.positionsListeners.add(listener); - if (this.positions.length) listener([...this.positions]); + onDepth(listener: (depth: AsterDepth) => void): () => void { + this.depthListeners.add(listener); + if (this.depthSnapshot) listener(cloneDepth(this.depthSnapshot)); return () => { - this.positionsListeners.delete(listener); + this.depthListeners.delete(listener); }; } - getAccountSnapshot(): GrvtAccountSnapshot | null { - return this.accountSnapshot ? { ...this.accountSnapshot } : null; + onTicker(listener: (ticker: AsterTicker) => void): () => void { + this.tickerListeners.add(listener); + if (this.tickerSnapshot) listener(cloneTicker(this.tickerSnapshot)); + return () => { + this.tickerListeners.delete(listener); + }; } - getOpenOrders(): GrvtOrder[] { - return [...this.openOrders]; + onKlines(listener: (klines: AsterKline[]) => void): () => void { + this.klineListeners.add(listener); + if (this.klineCache) listener(cloneKlines(this.klineCache.values)); + return () => { + this.klineListeners.delete(listener); + }; } - getPositions(): GrvtPosition[] { - return [...this.positions]; + getAccountSnapshot(): AsterAccountSnapshot | null { + return this.accountSnapshot ? cloneAccount(this.accountSnapshot) : null; + } + + getOpenOrders(): AsterOrder[] { + return cloneOrders(this.openOrders); + } + + getPositions(): AsterAccountPosition[] { + return this.positions.map((position) => ({ ...position })); + } + + async createOrder(params: CreateOrderParams): Promise { + if (params.type === "TRAILING_STOP_MARKET") { + throw new Error(TRAILING_NOT_SUPPORTED_ERROR); + } + + await this.ensureSession(); + await this.loadInstrumentInfo(); + + const { order: unsignedOrder, signing } = buildUnsignedOrder({ + params, + instrument: this.instrument, + subAccountId: this.subAccountId, + }); + + const nonce = randomInt(0, 2 ** 32); + const expirationNs = this.computeExpirationNs(); + const instrumentInfo = this.instrumentInfo; + if (!instrumentInfo) { + throw new Error("GRVT instrument metadata not available"); + } + + const signContext: GrvtSignContext = { + order: unsignedOrder, + quantity: signing.quantity, + price: signing.price, + side: signing.side, + isMarket: signing.isMarket, + timeInForce: signing.timeInForce, + postOnly: signing.postOnly, + reduceOnly: signing.reduceOnly, + nonce, + expirationNs, + instrument: instrumentInfo, + chainId: this.chainId, + subAccountId: this.subAccountId, + }; + + const signature = this.signatureProvider + ? await this.signatureProvider(signContext) + : await this.signWithPrivateKey(signContext); + + if (!signature.expiration) { + signature.expiration = expirationNs.toString(); + } + if (signature.nonce == null) { + signature.nonce = nonce; + } + if (!signature.r || !signature.s || typeof signature.v !== "number") { + throw new Error("Invalid signature returned for GRVT order"); + } + + const signedOrder: GrvtSignedOrder = { ...unsignedOrder, signature }; + + try { + const response = await this.tdg.createOrder({ order: signedOrder }); + const order = mapCreateOrderResponse(response, this.symbol); + this.mergeOrder(order); + return order; + } catch (error) { + const payload = { + code: (error as any)?.response?.data?.code, + message: (error as any)?.response?.data?.message, + }; + this.logger("createOrder", { error: extractMessage(error), payload }); + const detail = [payload.code, payload.message].filter(Boolean).join(": "); + throw new Error(detail || extractMessage(error)); + } + } + + async cancelOrder(params: { symbol: string; orderId: number | string }): Promise { + const orderId = String(params.orderId); + await this.tdg.cancelOrder({ sub_account_id: this.subAccountId, order_id: orderId }); + this.removeOrder(orderId); + } + + async cancelOrders(params: { symbol: string; orderIdList: Array }): Promise { + await Promise.all( + params.orderIdList.map((orderId) => + this.tdg + .cancelOrder({ sub_account_id: this.subAccountId, order_id: String(orderId) }) + .catch((error) => { + this.logger("cancelOrders", error); + }) + ) + ); + params.orderIdList.forEach((orderId) => this.removeOrder(String(orderId))); + } + + async cancelAllOrders(): Promise { + const current = [...this.openOrders]; + await this.cancelOrders({ symbol: this.symbol, orderIdList: current.map((o) => o.orderId) }); + } + + private startPolling(): void { + this.stopPolling(); + this.accountTimer = setInterval(() => { + void this.refreshAccountSnapshot().catch((error) => this.logger("accountPoll", error)); + void this.refreshPositions().catch((error) => this.logger("positionPoll", error)); + }, this.pollIntervals.account); + + this.ordersTimer = setInterval(() => { + void this.refreshOpenOrders().catch((error) => this.logger("ordersPoll", error)); + }, this.pollIntervals.orders); + + this.depthTimer = setInterval(() => { + void this.refreshDepth().catch((error) => this.logger("depthPoll", error)); + }, this.pollIntervals.depth); + + this.tickerTimer = setInterval(() => { + void this.refreshTicker().catch((error) => this.logger("tickerPoll", error)); + }, this.pollIntervals.ticker); + + this.klineTimer = setInterval(() => { + void this.refreshKlines().catch((error) => this.logger("klinePoll", error)); + }, this.pollIntervals.klines); + } + + private stopPolling(): void { + if (this.accountTimer) { + clearInterval(this.accountTimer); + this.accountTimer = null; + } + if (this.ordersTimer) { + clearInterval(this.ordersTimer); + this.ordersTimer = null; + } + if (this.depthTimer) { + clearInterval(this.depthTimer); + this.depthTimer = null; + } + if (this.tickerTimer) { + clearInterval(this.tickerTimer); + this.tickerTimer = null; + } + if (this.klineTimer) { + clearInterval(this.klineTimer); + this.klineTimer = null; + } } private async refreshAccountSnapshot(): Promise { - const response = await this.client.getSubAccountSummary({ sub_account_id: this.options.subAccountId }); - const snapshot = mapAccountSnapshot(response); + const response = await this.tdg.subAccountSummary({ sub_account_id: this.subAccountId }); + const snapshot = mapAccountSnapshot(response, this.symbol, this.instrument); this.accountSnapshot = snapshot; this.emitAccount(snapshot); } - private async refreshOpenOrders(): Promise { - const response = await this.client.tdgClient.openOrders({ sub_account_id: this.options.subAccountId }); - const orders = mapOpenOrders(response); - this.openOrders = orders; - this.emitOrders(orders); - } - private async refreshPositions(): Promise { - const response = await this.client.tdgClient.positions({ sub_account_id: this.options.subAccountId }); - const positions = mapPositions(response); - this.positions = positions.filter((position) => position.instrument === this.instrument); - this.emitPositions(this.positions); + const response = await this.tdg.positions({ sub_account_id: this.subAccountId }); + this.positions = mapPositions(response, this.symbol, this.instrument); + const snapshot = this.accountSnapshot; + if (snapshot) { + snapshot.positions = this.positions.map((position) => ({ ...position })); + snapshot.totalUnrealizedProfit = sumUnrealized(snapshot.positions).toString(); + this.emitAccount(snapshot); + } } - private subscribeStreams(): void { - const orderSubscription: IWSTdgOrderRequest = { - stream: `${EStream.ORDER}`, - params: { - sub_account_id: this.options.subAccountId, + private async refreshOpenOrders(): Promise { + const response = await this.tdg.openOrders({ sub_account_id: this.subAccountId }); + this.openOrders = mapOpenOrders(response, this.symbol); + this.emitOrders(this.openOrders); + } + + private async refreshDepth(): Promise { + try { + const response = await this.mdg.orderBook({ instrument: this.instrument, - }, - onData: (message) => this.handleOrderFeed(message), - }; - - const orderStateSubscription: IWSTdgOrderStateRequest = { - stream: `${EStream.STATE}`, - params: { - sub_account_id: this.options.subAccountId, - instrument: this.instrument, - }, - onData: (message) => this.handleOrderStateFeed(message), - }; - - const positionSubscription: IWSTdgPositionRequest = { - stream: `${EStream.POSITION}`, - params: { - sub_account_id: this.options.subAccountId, - instrument: this.instrument, - }, - onData: (message) => this.handlePositionFeed(message), - }; - - this.ws.subscribe(orderSubscription); - this.ws.subscribe(orderStateSubscription); - this.ws.subscribe(positionSubscription); + depth: 50, + }); + this.depthSnapshot = mapDepth(response, this.symbol); + } catch (error) { + this.logger("depthPoll", error); + try { + const fallback = await this.mdg.orderBook({ instrument: this.instrument, depth: 10 }); + this.depthSnapshot = mapDepth(fallback, this.symbol); + } catch (fallbackError) { + this.logger("depthPollFallback", fallbackError); + return; + } + } + if (this.depthSnapshot) this.emitDepth(this.depthSnapshot); } - private handleOrderFeed(feed: GrvtOrder): void { - this.mergeOrder(feed); + private async refreshTicker(): Promise { + const response = await this.mdg.ticker({ instrument: this.instrument }); + this.tickerSnapshot = mapTicker(response, this.symbol); + if (this.tickerSnapshot) this.emitTicker(this.tickerSnapshot); } - private handleOrderStateFeed(feed: GrvtOrderUpdateFeed): void { - this.mergeOrderState(feed); + private async refreshKlines(): Promise { + const response = await this.mdg.candlestick({ + instrument: this.instrument, + interval: this.klineInterval, + type: "TRADE", + limit: 500, + }); + const klines = mapKlines(response, this.symbol); + this.klineCache = { interval: this.klineInterval, values: klines }; + this.emitKlines(klines); } - private handlePositionFeed(feed: GrvtPositionUpdateFeed): void { - this.mergePosition(feed); - } - - private mergeOrder(order: GrvtOrder): void { - const existingIndex = this.openOrders.findIndex((item) => item.order_id === order.order_id); - if (existingIndex >= 0) { - this.openOrders[existingIndex] = { ...this.openOrders[existingIndex], ...order }; + private mergeOrder(order: AsterOrder): void { + const index = this.openOrders.findIndex((item) => String(item.orderId) === String(order.orderId)); + if (index >= 0) { + this.openOrders[index] = order; } else { this.openOrders.push(order); } - this.emitOrders([...this.openOrders]); + this.emitOrders(this.openOrders); } - private mergeOrderState(update: GrvtOrderUpdateFeed): void { - const existing = this.openOrders.find((item) => item.order_id === update.order_id); - if (existing) { - existing.state = update.state; - this.emitOrders([...this.openOrders]); + private removeOrder(orderId: string): void { + const before = this.openOrders.length; + this.openOrders = this.openOrders.filter((item) => String(item.orderId) !== orderId); + if (this.openOrders.length !== before) { + this.emitOrders(this.openOrders); } } - private mergePosition(update: GrvtPositionUpdateFeed): void { - const idx = this.positions.findIndex((item) => item.instrument === update.instrument); - if (idx >= 0) { - this.positions[idx] = { - ...this.positions[idx], - ...update, - }; - } else { - this.positions.push(update); - } - this.emitPositions([...this.positions]); - } - - private emitAccount(snapshot: GrvtAccountSnapshot): void { - this.snapshotListeners.forEach((listener) => { + private emitAccount(snapshot: AsterAccountSnapshot): void { + const cloned = cloneAccount(snapshot); + this.accountListeners.forEach((listener) => { try { - listener({ ...snapshot }); + listener(cloned); } catch (error) { - console.error("[GrvtGateway] account listener failed", error); + this.logger("accountListener", error); } }); } - private emitOrders(orders: GrvtOrder[]): void { + private emitOrders(orders: AsterOrder[]): void { + const cloned = cloneOrders(orders); this.ordersListeners.forEach((listener) => { try { - listener([...orders]); + listener(cloned); } catch (error) { - console.error("[GrvtGateway] orders listener failed", error); + this.logger("ordersListener", error); } }); } - private emitPositions(positions: GrvtPosition[]): void { - this.positionsListeners.forEach((listener) => { + private emitDepth(depth: AsterDepth): void { + const cloned = cloneDepth(depth); + this.depthListeners.forEach((listener) => { try { - listener([...positions]); + listener(cloned); } catch (error) { - console.error("[GrvtGateway] positions listener failed", error); + this.logger("depthListener", error); } }); } + + private emitTicker(ticker: AsterTicker): void { + const cloned = cloneTicker(ticker); + this.tickerListeners.forEach((listener) => { + try { + listener(cloned); + } catch (error) { + this.logger("tickerListener", error); + } + }); + } + + private emitKlines(klines: AsterKline[]): void { + const cloned = cloneKlines(klines); + this.klineListeners.forEach((listener) => { + try { + listener(cloned); + } catch (error) { + this.logger("klinesListener", error); + } + }); + } + + private computeExpirationNs(): bigint { + return BigInt(Date.now()) * 1_000_000n + DEFAULT_ORDER_EXPIRY_NS; + } + + private isSessionExpired(): boolean { + if (!this.sessionInfo) return true; + if (!this.sessionInfo.expiresAt) return false; + return Date.now() > this.sessionInfo.expiresAt - 5000; + } + + private async ensureSession(): Promise { + if (this.sessionInfo && !this.isSessionExpired()) { + this.updateHeaders(); + return; + } + + if (this.sessionPromise) { + await this.sessionPromise; + return; + } + + if (!this.apiKey) { + if (!this.sessionInfo) { + throw new Error( + "GRVT authentication requires GRVT_API_KEY or an existing session cookie" + ); + } + this.updateHeaders(); + return; + } + + const promise = this.authenticate(); + this.sessionPromise = promise.finally(() => { + this.sessionPromise = null; + }); + await promise; + } + + private async authenticate(): Promise { + if (!this.apiKey) { + throw new Error("GRVT_API_KEY is not configured"); + } + try { + const response = await axios.post( + `${this.hosts.edge}/auth/api_key/login`, + { api_key: this.apiKey }, + { + headers: { "Content-Type": "application/json", Cookie: "rm=true;" }, + timeout: 5000, + } + ); + const cookieInfo = parseSetCookieHeader(response.headers["set-cookie"]); + const accountIdHeader = + (response.headers["x-grvt-account-id"] ?? response.headers["X-Grvt-Account-Id"]) ?? ""; + const accountId = Array.isArray(accountIdHeader) + ? accountIdHeader[0]?.toString().trim() + : accountIdHeader?.toString().trim(); + if (!cookieInfo?.cookie || !accountId) { + throw new Error("GRVT authentication response missing session cookie or account id"); + } + this.sessionInfo = { + cookie: cookieInfo.cookie, + accountId, + expiresAt: cookieInfo.expiresAt, + }; + this.updateHeaders(); + } catch (error) { + this.logger("authenticate", error); + throw new Error("Failed to authenticate with GRVT using API key"); + } + } + + private updateHeaders(): void { + if (!this.sessionInfo) return; + this.headers = { + Cookie: this.sessionInfo.cookie, + "X-Grvt-Account-Id": this.sessionInfo.accountId, + }; + } + + private async loadInstrumentInfo(): Promise { + if (this.instrumentInfo) return; + try { + const response = await this.mdg.instrument({ instrument: this.instrument }); + const result: any = response?.result; + if (!result) { + throw new Error("Empty instrument response"); + } + const instrumentHashRaw = + result.instrument_hash ?? result.ih ?? result.instrumentHash ?? result.instrumenthash; + if (!instrumentHashRaw) { + throw new Error("Instrument hash not found in response"); + } + const baseDecimals = Number(result.base_decimals ?? result.bd ?? result.baseDecimals ?? 0); + const quoteDecimals = Number(result.quote_decimals ?? result.qd ?? result.quoteDecimals ?? 0); + this.instrumentInfo = { + instrument: this.instrument, + instrumentHash: normalizeHex(instrumentHashRaw), + baseDecimals, + quoteDecimals, + }; + } catch (error) { + this.logger("loadInstrumentInfo", error); + throw new Error("Unable to load GRVT instrument metadata"); + } + } + + private async signWithPrivateKey(context: GrvtSignContext): Promise { + if (!this.apiSecret) { + throw new Error("GRVT_API_SECRET is not configured for local signing"); + } + if (context.order.legs.length !== 1) { + throw new Error("GRVT adapter currently supports only single-leg orders"); + } + const privateKeyBytes = hexToBytes(padPrivateKey(this.apiSecret)); + const leg = context.order.legs[0]; + const contractSize = scaleDecimal(leg.size, context.instrument.baseDecimals); + const limitPriceSource = + leg.limit_price ?? (context.isMarket ? "0" : context.price?.toString() ?? "0"); + const limitPrice = scaleDecimal(limitPriceSource, 9); + const types: EIP712Types = { + EIP712Domain: EIP712_DOMAIN_FIELDS, + Order: EIP712_ORDER_TYPES.Order, + OrderLeg: EIP712_ORDER_TYPES.OrderLeg, + }; + const message = { + subAccountID: BigInt(context.subAccountId), + isMarket: context.isMarket, + timeInForce: mapTimeInForceToNumeric(context.timeInForce), + postOnly: context.postOnly, + reduceOnly: context.reduceOnly, + legs: [ + { + assetID: BigInt(normalizeHex(context.instrument.instrumentHash)), + contractSize, + limitPrice, + isBuyingContract: context.side === "BUY", + }, + ], + nonce: BigInt(context.nonce), + expiration: context.expirationNs, + }; + const domain = { + name: "GRVT Exchange", + version: "0", + chainId: BigInt(context.chainId), + }; + const domainHash = hashStruct("EIP712Domain", domain, types); + const messageHash = hashStruct("Order", message, types); + const digest = keccak256( + concatBytes(new Uint8Array([0x19, 0x01]), domainHash, messageHash) + ); + const signature = secp256k1.sign(digest, privateKeyBytes); + const compact = signature.toCompactRawBytes(); + const r = `0x${bytesToHex(compact.slice(0, 32))}`; + const s = `0x${bytesToHex(compact.slice(32, 64))}`; + const v = 27 + (signature.recovery ?? 0); + const publicKey = secp256k1.getPublicKey(privateKeyBytes, false).slice(1); + const signer = `0x${bytesToHex(keccak256(publicKey).slice(-20))}`; + return { + signer, + r, + s, + v, + expiration: context.expirationNs.toString(), + nonce: context.nonce, + }; + } } -function mapAccountSnapshot(response: IApiSubAccountSummaryResponse): GrvtAccountSnapshot { - const result = response.result; - if (!result) { - return { - total_unrealized_pnl: "0", - positions: [], - }; +function normalizeEnvironment(env: GrvtEnvironment | undefined): BaseEnvironment { + if (!env) return "testnet"; + const normalized = env.toString().toLowerCase(); + if (normalized in ENVIRONMENT_HOSTS) { + return normalized as BaseEnvironment; } + if (normalized in ENVIRONMENT_ALIASES) { + return ENVIRONMENT_ALIASES[normalized]; + } + return "testnet"; +} + +function resolveHosts(env: BaseEnvironment, override?: GrvtHostsOverride): HostsConfig { + const base = ENVIRONMENT_HOSTS[env]; return { - total_unrealized_pnl: result.unrealized_pnl, - positions: (result.positions ?? []).map(mapPositionEntry), - settle_currency: result.settle_currency, - available_balance: result.available_balance, + trades: override?.trades ?? base.trades, + market: override?.market ?? base.market, + edge: override?.edge ?? base.edge, }; } -function mapOpenOrders(response: IApiOpenOrdersResponse): GrvtOrder[] { - return (response.result ?? []).map((order) => ({ - order_id: order.order_id ?? "", - client_order_id: order.metadata?.client_order_id, - sub_account_id: order.sub_account_id, - is_market: order.is_market, - time_in_force: order.time_in_force, - post_only: order.post_only, - reduce_only: order.reduce_only, - legs: order.legs?.map((leg) => ({ - instrument: leg.instrument ?? "", - size: leg.size ?? "0", - limit_price: leg.limit_price ?? undefined, - is_buying_asset: leg.is_buying_asset, - })), - metadata: order.metadata, - state: order.state, +function normalizeCookieValue(value: string): string { + const parsed = parseSetCookieHeader(value); + if (parsed?.cookie) return parsed.cookie; + return value.split(";")[0].trim(); +} + +function defaultLogger(context: string, error: unknown): void { + console.error(`[GrvtGateway] ${context} failed`, error); +} + +function parseSetCookieHeader( + header?: string | string[] +): { cookie: string; expiresAt?: number } | null { + if (!header) return null; + const entries = Array.isArray(header) ? header : [header]; + for (const entry of entries) { + if (!entry) continue; + const segments = entry.split(";").map((part) => part.trim()).filter(Boolean); + if (!segments.length) continue; + const cookieSegment = segments.find((segment) => segment.toLowerCase().startsWith("gravity")) ?? segments[0]; + if (!cookieSegment) continue; + const expiresSegment = segments.find((segment) => segment.toLowerCase().startsWith("expires=")); + const expiresAt = expiresSegment ? Date.parse(expiresSegment.slice("expires=".length)) : undefined; + return { + cookie: cookieSegment, + expiresAt: Number.isFinite(expiresAt) ? expiresAt : undefined, + }; + } + return null; +} + +function normalizeHex(value: string): string { + const trimmed = value.trim(); + if (trimmed.startsWith("0x") || trimmed.startsWith("0X")) { + return `0x${trimmed.slice(2).toLowerCase()}`; + } + return `0x${trimmed.toLowerCase()}`; +} + +function cloneAccount(snapshot: AsterAccountSnapshot): AsterAccountSnapshot { + return JSON.parse(JSON.stringify(snapshot)); +} + +function cloneOrders(orders: AsterOrder[]): AsterOrder[] { + return orders.map((order) => ({ ...order })); +} + +function cloneDepth(depth: AsterDepth): AsterDepth { + return { + ...depth, + bids: depth.bids.map((level) => [...level] as [string, string]), + asks: depth.asks.map((level) => [...level] as [string, string]), + }; +} + +function cloneTicker(ticker: AsterTicker): AsterTicker { + return { ...ticker }; +} + +function cloneKlines(klines: AsterKline[]): AsterKline[] { + return klines.map((kline) => ({ ...kline })); +} + +function nsToMs(input: string | number | undefined): number { + if (input == null) return Date.now(); + const value = typeof input === "string" ? Number(input) : input; + if (!Number.isFinite(value)) return Date.now(); + return Math.floor(value / ONE_MILLISECOND_IN_NANOSECONDS); +} + +function scaleDecimal(value: string | number | undefined, decimals: number): bigint { + if (value == null) return 0n; + const strValue = typeof value === "number" ? value.toString() : value; + if (!strValue.includes("e") && !strValue.includes("E")) { + const [intPartRaw, fracRaw = ""] = strValue.split("."); + const intPart = intPartRaw === "" ? "0" : intPartRaw.replace(/^\+/, ""); + const fraction = fracRaw.padEnd(decimals, "0").slice(0, decimals); + const combined = `${intPart}${fraction}`; + return BigInt(combined || "0"); + } + const fixed = Number(strValue); + if (!Number.isFinite(fixed)) { + throw new Error(`Unable to scale decimal value: ${value}`); + } + return scaleDecimal(fixed.toFixed(decimals), decimals); +} + +function sumUnrealized(positions: AsterAccountPosition[]): number { + return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0); +} + +function mapAccountSnapshot( + response: IApiSubAccountSummaryResponse, + symbol: string, + instrument: string +): AsterAccountSnapshot { + const result = response.result; + if (!result) { + return emptyAccount(symbol); + } + const updateTime = nsToMs(result.event_time); + const assets = (result.spot_balances ?? []).map((balance) => ({ + asset: balance.currency ?? symbol.slice(-4), + walletBalance: balance.balance ?? "0", + availableBalance: balance.balance ?? "0", + updateTime, + })); + return { + canTrade: true, + canDeposit: true, + canWithdraw: true, + updateTime, + totalWalletBalance: result.total_equity ?? "0", + totalUnrealizedProfit: result.unrealized_pnl ?? "0", + totalMarginBalance: result.total_equity, + totalInitialMargin: result.initial_margin, + totalMaintMargin: result.maintenance_margin, + availableBalance: result.available_balance, + positions: mapPositions({ result: result.positions ?? [] }, symbol, instrument), + assets, + }; +} + +function mapPositions( + response: IApiPositionsResponse, + symbol: string, + instrument: string +): AsterAccountPosition[] { + return (response.result ?? []) + .filter((entry) => !entry.instrument || entry.instrument === instrument) + .map((entry) => ({ + symbol, + positionAmt: entry.size ?? "0", + entryPrice: entry.entry_price ?? "0", + unrealizedProfit: entry.unrealized_pnl ?? "0", + positionSide: "BOTH", + updateTime: nsToMs(entry.event_time), + markPrice: entry.mark_price, + liquidationPrice: entry.est_liquidation_price, + leverage: entry.leverage, + })); +} + +function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): AsterOrder[] { + return (response.result ?? []).map((order) => mapOrder(order, symbol)); +} + +function mapOrder(order: GrvtOrder, symbol: string): AsterOrder { + const leg = order.legs?.[0]; + const state = order.state; + const metadata = order.metadata; + const trigger = metadata?.trigger; + const executedQty = Array.isArray(state?.traded_size) ? state?.traded_size?.[0] ?? "0" : state?.traded_size ?? "0"; + const avgFillPrice = Array.isArray(state?.avg_fill_price) + ? state?.avg_fill_price?.[0] ?? undefined + : state?.avg_fill_price ?? undefined; + return { + orderId: order.order_id ?? metadata?.client_order_id ?? cryptoRandomId(), + clientOrderId: metadata?.client_order_id ?? "", + symbol, + side: leg?.is_buying_asset ? "BUY" : "SELL", + type: order.is_market ? "MARKET" : "LIMIT", + status: state?.status ?? "NEW", + price: leg?.limit_price ?? "0", + origQty: leg?.size ?? "0", + executedQty, + stopPrice: trigger?.tpsl?.trigger_price ?? "0", + time: nsToMs(metadata?.create_time), + updateTime: nsToMs(state?.update_time), + reduceOnly: Boolean(order.reduce_only), + closePosition: Boolean(trigger?.tpsl?.close_position), + timeInForce: mapTimeInForceFromGrvt(order.time_in_force), + avgPrice: avgFillPrice, + cumQuote: undefined, + workingType: undefined, + activationPrice: undefined, + priceRate: undefined, + priceProtect: false, + }; +} + +function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterDepth | null { + const result = response.result; + if (!result) return null; + const toLevel = (entries: Array<{ price?: string; size?: string }> | undefined) => + (entries ?? []).map((entry) => [entry.price ?? "0", entry.size ?? "0"] as [string, string]); + return { + lastUpdateId: Number(result.event_time ?? Date.now()), + eventTime: nsToMs(result.event_time), + symbol, + bids: toLevel(result.bids as any), + asks: toLevel(result.asks as any), + }; +} + +function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker | null { + const result = response.result; + if (!result) return null; + const buyVolume = Number(result.buy_volume_24h_b ?? 0); + const sellVolume = Number(result.sell_volume_24h_b ?? 0); + const buyQuote = Number(result.buy_volume_24h_q ?? 0); + const sellQuote = Number(result.sell_volume_24h_q ?? 0); + return { + symbol, + lastPrice: result.last_price ?? "0", + openPrice: result.open_price ?? "0", + highPrice: result.high_price ?? "0", + lowPrice: result.low_price ?? "0", + volume: (buyVolume + sellVolume).toString(), + quoteVolume: (buyQuote + sellQuote).toString(), + eventTime: nsToMs(result.event_time), + lastQty: result.last_size, + count: undefined, + }; +} + +function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKline[] { + return (response.result ?? []).map((entry) => ({ + openTime: nsToMs(entry.open_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS), + closeTime: nsToMs(entry.close_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS), + open: entry.open ?? "0", + high: entry.high ?? "0", + low: entry.low ?? "0", + close: entry.close ?? "0", + volume: entry.volume_b ?? "0", + numberOfTrades: entry.trades ?? 0, })); } -function mapPositions(response: IApiPositionsResponse): GrvtPosition[] { - return (response.result ?? []).map(mapPositionEntry); +function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): AsterOrder { + const order = response.result ?? (response as unknown as { order?: GrvtOrder }).order; + if (!order) { + return { + orderId: cryptoRandomId(), + clientOrderId: "", + symbol, + side: "BUY", + type: "LIMIT", + status: "NEW", + price: "0", + origQty: "0", + executedQty: "0", + stopPrice: "0", + time: Date.now(), + updateTime: Date.now(), + reduceOnly: false, + closePosition: false, + }; + } + return mapOrder(order as unknown as GrvtOrder, symbol); } -function mapPositionEntry(entry: any): GrvtPosition { +function buildUnsignedOrder(params: { + params: CreateOrderParams; + instrument: string; + subAccountId: string; +}): { + order: GrvtUnsignedOrder; + signing: { + quantity: number; + price?: number; + side: OrderSide; + isMarket: boolean; + timeInForce: string; + postOnly: boolean; + reduceOnly: boolean; + }; +} { + const { params: orderParams, instrument, subAccountId } = params; + const quantity = Number(orderParams.quantity); + if (!Number.isFinite(quantity) || quantity <= 0) { + throw new Error("GRVT requires a positive quantity when creating orders"); + } + + const isMarketOrder = orderParams.type === "MARKET" || orderParams.type === "STOP_MARKET"; + const timeInForce = mapTimeInForceToGrvt(orderParams.timeInForce); + const postOnly = Boolean( + orderParams.timeInForce && orderParams.timeInForce.toUpperCase() === "GTX" + ); + const reduceOnly = normalizeBoolean(orderParams.reduceOnly); + const priceValue = isMarketOrder ? undefined : orderParams.price; + if (!isMarketOrder && (priceValue == null || !Number.isFinite(Number(priceValue)))) { + throw new Error("GRVT limit orders require a valid price"); + } + + const trigger = buildTriggerMetadata(orderParams); + const metadata = { + client_order_id: generateClientOrderId(), + ...(trigger ? { trigger } : {}), + }; + + const order: GrvtUnsignedOrder = { + sub_account_id: subAccountId, + is_market: isMarketOrder, + time_in_force: timeInForce, + post_only: postOnly, + reduce_only: reduceOnly, + legs: [ + { + instrument, + size: toDecimalString(quantity), + limit_price: isMarketOrder ? undefined : toDecimalString(priceValue), + is_buying_asset: orderParams.side === "BUY", + }, + ], + metadata, + }; + return { - instrument: entry.instrument ?? "", - size: entry.size ?? "0", - entry_price: entry.entry_price, - mark_price: entry.mark_price, - unrealized_pnl: entry.unrealized_pnl, - sub_account_id: entry.sub_account_id, - update_time: entry.event_time, + order, + signing: { + quantity, + price: priceValue == null ? undefined : Number(priceValue), + side: orderParams.side, + isMarket: isMarketOrder, + timeInForce, + postOnly, + reduceOnly, + }, }; } +function buildTriggerMetadata(params: CreateOrderParams): GrvtUnsignedOrder["metadata"]["trigger"] | undefined { + if (params.type === "STOP_MARKET") { + const triggerType = params.side === "BUY" ? "TAKE_PROFIT" : "STOP_LOSS"; + const stopPrice = params.stopPrice ?? params.activationPrice; + if (!stopPrice) { + throw new Error("GRVT stop orders require a stopPrice or activationPrice"); + } + return { + trigger_type: triggerType, + tpsl: { + trigger_by: DEFAULT_MARK_PRICE_TRIGGER, + trigger_price: toDecimalString(stopPrice), + close_position: normalizeBoolean(params.closePosition), + }, + }; + } + return undefined; +} + +function mapIntervalToGrvt(interval: string): string { + const normalized = interval.trim().toLowerCase(); + switch (normalized) { + case "1m": + return "CI_1_M"; + case "3m": + return "CI_3_M"; + case "5m": + return "CI_5_M"; + case "15m": + return "CI_15_M"; + case "30m": + return "CI_30_M"; + case "1h": + return "CI_1_H"; + case "2h": + return "CI_2_H"; + case "4h": + return "CI_4_H"; + case "6h": + return "CI_6_H"; + case "8h": + return "CI_8_H"; + case "12h": + return "CI_12_H"; + case "1d": + return "CI_1_D"; + case "1w": + return "CI_1_W"; + default: + return "CI_1_M"; + } +} + +function mapTimeInForceToGrvt(timeInForce: string | undefined): string { + switch ((timeInForce ?? "GTC").toUpperCase()) { + case "IOC": + return "IMMEDIATE_OR_CANCEL"; + case "FOK": + return "FILL_OR_KILL"; + case "GTX": + return "GOOD_TILL_TIME"; + default: + return DEFAULT_TIME_IN_FORCE; + } +} + +function mapTimeInForceToNumeric(timeInForce: string): number { + switch (timeInForce) { + case "ALL_OR_NONE": + return 2; + case "IMMEDIATE_OR_CANCEL": + return 3; + case "FILL_OR_KILL": + return 4; + case "GOOD_TILL_TIME": + default: + return 1; + } +} + +function hashStruct(type: string, value: Record, types: EIP712Types): Uint8Array { + const typeHashBytes = typeHash(type, types); + const fields = types[type]; + if (!fields) { + throw new Error(`Unknown EIP712 type: ${type}`); + } + const buffers = [typeHashBytes]; + for (const field of fields) { + const fieldValue = value[field.name]; + buffers.push(encodeField(field.type, fieldValue, types)); + } + return keccak256(concatBytes(...buffers)); +} + +function encodeField(type: string, value: any, types: EIP712Types): Uint8Array { + if (type.endsWith("[]")) { + const baseType = type.slice(0, type.indexOf("[")); + const arrayValue = Array.isArray(value) ? value : []; + const hashes = arrayValue.map((item) => hashStruct(baseType, item, types)); + return keccak256(hashes.length ? concatBytes(...hashes) : new Uint8Array()); + } + if (types[type]) { + return hashStruct(type, value, types); + } + return encodePrimitive(type, value); +} + +function encodePrimitive(type: string, value: any): Uint8Array { + switch (type) { + case "string": + return keccak256(utf8ToBytes(value ?? "")); + case "bool": + return bigIntToUint8Array(value ? 1n : 0n); + case "uint8": + case "uint32": + case "uint64": + case "uint256": + case "int64": + return bigIntToUint8Array(BigInt(value ?? 0)); + case "bytes": + return keccak256(typeof value === "string" ? hexToBytes(stripHexPrefix(value)) : value ?? new Uint8Array()); + default: + throw new Error(`Unsupported EIP712 field type: ${type}`); + } +} + +function typeHash(primaryType: string, types: EIP712Types): Uint8Array { + return keccak256(utf8ToBytes(encodeType(primaryType, types))); +} + +function encodeType(primaryType: string, types: EIP712Types): string { + const deps = Array.from(findTypeDependencies(primaryType, types)); + deps.splice(deps.indexOf(primaryType), 1); + deps.sort(); + deps.unshift(primaryType); + return deps + .map((type) => { + const fields = types[type]; + if (!fields) throw new Error(`Type ${type} not defined in EIP712 types`); + const fieldStr = fields.map((field) => `${field.type} ${field.name}`).join(","); + return `${type}(${fieldStr})`; + }) + .join(""); +} + +function findTypeDependencies(primaryType: string, types: EIP712Types, results = new Set()): Set { + if (results.has(primaryType)) return results; + const fields = types[primaryType]; + if (!fields) return results; + results.add(primaryType); + for (const field of fields) { + const baseType = field.type.replace(/\[[^\]]*\]/g, ""); + findTypeDependencies(baseType, types, results); + } + return results; +} + +function mapTimeInForceFromGrvt(value: string | undefined): "GTC" | "IOC" | "FOK" | undefined { + switch (value) { + case "IMMEDIATE_OR_CANCEL": + return "IOC"; + case "FILL_OR_KILL": + return "FOK"; + case "GOOD_TILL_TIME": + return "GTC"; + case "RETAIL_PRICE_IMPROVEMENT": + return "GTC"; + default: + return undefined; + } +} + +function normalizeBoolean(value: string | boolean | undefined): boolean { + if (typeof value === "boolean") return value; + if (typeof value === "string") return value.toLowerCase() === "true"; + return false; +} + +function toDecimalString(value: number | string | undefined): string { + if (value == null) return "0"; + if (typeof value === "string") return value; + if (!Number.isFinite(value)) return "0"; + return value.toString(); +} + +function generateClientOrderId(): string { + const random = Math.floor(Math.random() * 1_000_000) + .toString() + .padStart(6, "0"); + return `${Date.now()}${random}`; +} + +function cryptoRandomId(): string { + return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`; +} + +function bigIntToUint8Array(value: bigint): Uint8Array { + if (value < 0n) { + throw new Error("Negative values are not supported in EIP712 encoding"); + } + const hex = value.toString(16).padStart(64, "0"); + return hexToBytes(hex); +} + +function stripHexPrefix(value: string): string { + return value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value; +} + +function padPrivateKey(value: string): string { + const hex = stripHexPrefix(value).padStart(64, "0"); + if (hex.length !== 64) { + throw new Error("GRVT_API_SECRET must be a 32-byte hex string"); + } + return hex; +} + +function emptyAccount(symbol: string): AsterAccountSnapshot { + return { + canTrade: true, + canDeposit: true, + canWithdraw: true, + updateTime: Date.now(), + totalWalletBalance: "0", + totalUnrealizedProfit: "0", + positions: [], + assets: [ + { + asset: symbol.slice(-4), + walletBalance: "0", + availableBalance: "0", + updateTime: Date.now(), + }, + ], + } as AsterAccountSnapshot; +} diff --git a/src/exchanges/types.ts b/src/exchanges/types.ts index cc66c70..43ce546 100644 --- a/src/exchanges/types.ts +++ b/src/exchanges/types.ts @@ -195,6 +195,51 @@ export interface GrvtKline { number_of_trades?: number; } +export interface GrvtSignature { + signer: string; + r: string; + s: string; + v: number; + expiration: string; + nonce: number; +} + +export interface GrvtUnsignedOrderLeg { + instrument: string; + size: string; + limit_price?: string; + is_buying_asset: boolean; +} + +export interface GrvtTriggerMetadata { + trigger_type: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS"; + tpsl: { + trigger_by: "UNSPECIFIED" | "INDEX" | "LAST" | "MID" | "MARK"; + trigger_price: string; + close_position: boolean; + }; +} + +export interface GrvtOrderMetadataInput { + client_order_id: string; + trigger?: GrvtTriggerMetadata; + broker?: string | null; +} + +export interface GrvtUnsignedOrder { + sub_account_id: string; + is_market: boolean; + time_in_force: string; + post_only: boolean; + reduce_only: boolean; + legs: GrvtUnsignedOrderLeg[]; + metadata: GrvtOrderMetadataInput; +} + +export interface GrvtSignedOrder extends GrvtUnsignedOrder { + signature: GrvtSignature; +} + export interface AsterAccountAsset { asset: string; walletBalance: string; @@ -290,7 +335,7 @@ export interface AsterKline { } export interface AsterOrder { - orderId: number; + orderId: number | string; clientOrderId: string; symbol: string; side: OrderSide; diff --git a/src/ui/MakerApp.tsx b/src/ui/MakerApp.tsx index 4c1a88d..ffbe30e 100644 --- a/src/ui/MakerApp.tsx +++ b/src/ui/MakerApp.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Box, Text, useInput } from "ink"; import { makerConfig } from "../config"; -import { AsterExchangeAdapter } from "../exchanges/aster-adapter"; +import { createExchangeAdapter, resolveExchangeId } from "../exchanges/create-adapter"; import { MakerEngine, type MakerEngineSnapshot } from "../core/maker-engine"; import { DataTable, type TableColumn } from "./components/DataTable"; import { formatNumber } from "../utils/format"; @@ -28,18 +28,28 @@ export function MakerApp({ onExit }: MakerAppProps) { ); useEffect(() => { - const apiKey = process.env.ASTER_API_KEY; - const apiSecret = process.env.ASTER_API_SECRET; - if (!apiKey || !apiSecret) { - setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); - return; - } try { - const adapter = new AsterExchangeAdapter({ - apiKey, - apiSecret, - symbol: makerConfig.symbol, - }); + const exchangeId = resolveExchangeId(); + let adapter; + if (exchangeId === "aster") { + const apiKey = process.env.ASTER_API_KEY; + const apiSecret = process.env.ASTER_API_SECRET; + if (!apiKey || !apiSecret) { + setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); + return; + } + adapter = createExchangeAdapter({ + exchange: exchangeId, + symbol: makerConfig.symbol, + aster: { apiKey, apiSecret }, + }); + } else { + adapter = createExchangeAdapter({ + exchange: exchangeId, + symbol: makerConfig.symbol, + grvt: { symbol: makerConfig.symbol }, + }); + } const engine = new MakerEngine(makerConfig, adapter); engineRef.current = engine; setSnapshot(engine.getSnapshot()); diff --git a/src/ui/OffsetMakerApp.tsx b/src/ui/OffsetMakerApp.tsx index bd9a1d9..826a425 100644 --- a/src/ui/OffsetMakerApp.tsx +++ b/src/ui/OffsetMakerApp.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Box, Text, useInput } from "ink"; import { makerConfig } from "../config"; -import { AsterExchangeAdapter } from "../exchanges/aster-adapter"; +import { createExchangeAdapter, resolveExchangeId } from "../exchanges/create-adapter"; import { OffsetMakerEngine, type OffsetMakerEngineSnapshot } from "../core/offset-maker-engine"; import { DataTable, type TableColumn } from "./components/DataTable"; import { formatNumber } from "../utils/format"; @@ -28,18 +28,28 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) { ); useEffect(() => { - const apiKey = process.env.ASTER_API_KEY; - const apiSecret = process.env.ASTER_API_SECRET; - if (!apiKey || !apiSecret) { - setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); - return; - } try { - const adapter = new AsterExchangeAdapter({ - apiKey, - apiSecret, - symbol: makerConfig.symbol, - }); + const exchangeId = resolveExchangeId(); + let adapter; + if (exchangeId === "aster") { + const apiKey = process.env.ASTER_API_KEY; + const apiSecret = process.env.ASTER_API_SECRET; + if (!apiKey || !apiSecret) { + setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); + return; + } + adapter = createExchangeAdapter({ + exchange: exchangeId, + symbol: makerConfig.symbol, + aster: { apiKey, apiSecret }, + }); + } else { + adapter = createExchangeAdapter({ + exchange: exchangeId, + symbol: makerConfig.symbol, + grvt: { symbol: makerConfig.symbol }, + }); + } const engine = new OffsetMakerEngine(makerConfig, adapter); engineRef.current = engine; setSnapshot(engine.getSnapshot()); @@ -192,4 +202,3 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) { ); } - diff --git a/src/ui/TrendApp.tsx b/src/ui/TrendApp.tsx index 6eef9a2..31ee9e1 100644 --- a/src/ui/TrendApp.tsx +++ b/src/ui/TrendApp.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Box, Text, useInput } from "ink"; import { tradingConfig } from "../config"; -import { AsterExchangeAdapter } from "../exchanges/aster-adapter"; +import { createExchangeAdapter, resolveExchangeId } from "../exchanges/create-adapter"; import { TrendEngine, type TrendEngineSnapshot } from "../core/trend-engine"; import { formatNumber } from "../utils/format"; import { DataTable, type TableColumn } from "./components/DataTable"; @@ -30,18 +30,28 @@ export function TrendApp({ onExit }: TrendAppProps) { ); useEffect(() => { - const apiKey = process.env.ASTER_API_KEY; - const apiSecret = process.env.ASTER_API_SECRET; - if (!apiKey || !apiSecret) { - setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); - return; - } try { - const adapter = new AsterExchangeAdapter({ - apiKey, - apiSecret, - symbol: tradingConfig.symbol, - }); + const exchangeId = resolveExchangeId(); + let adapter; + if (exchangeId === "aster") { + const apiKey = process.env.ASTER_API_KEY; + const apiSecret = process.env.ASTER_API_SECRET; + if (!apiKey || !apiSecret) { + setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); + return; + } + adapter = createExchangeAdapter({ + exchange: exchangeId, + symbol: tradingConfig.symbol, + aster: { apiKey, apiSecret }, + }); + } else { + adapter = createExchangeAdapter({ + exchange: exchangeId, + symbol: tradingConfig.symbol, + grvt: { symbol: tradingConfig.symbol }, + }); + } const engine = new TrendEngine(tradingConfig, adapter); engineRef.current = engine; setSnapshot(engine.getSnapshot()); diff --git a/tests/exchange-factory.test.ts b/tests/exchange-factory.test.ts new file mode 100644 index 0000000..f3134ed --- /dev/null +++ b/tests/exchange-factory.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, beforeEach, afterEach } from "vitest"; +import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter"; +import { AsterExchangeAdapter } from "../src/exchanges/aster-adapter"; +import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter"; + +const ORIGINAL_ENV = { ...process.env }; + +beforeEach(() => { + process.env = { ...ORIGINAL_ENV }; +}); + +afterEach(() => { + process.env = { ...ORIGINAL_ENV }; +}); + +describe("exchange factory", () => { + it("defaults to aster when no env is provided", () => { + delete process.env.EXCHANGE; + const adapter = createExchangeAdapter({ symbol: "BTCUSDT" }); + expect(adapter).toBeInstanceOf(AsterExchangeAdapter); + expect(adapter.id).toBe("aster"); + }); + + it("resolves exchange id case-insensitively", () => { + expect(resolveExchangeId("Grvt")).toBe("grvt"); + expect(resolveExchangeId("ASTER")).toBe("aster"); + }); + + it("creates grvt adapter when EXCHANGE=grvt", () => { + process.env.EXCHANGE = "grvt"; + process.env.GRVT_API_KEY = "api-key"; + process.env.GRVT_API_SECRET = "0x" + "1".repeat(64); + process.env.GRVT_SUB_ACCOUNT_ID = "sub"; + process.env.GRVT_INSTRUMENT = "BTC_USDT_Perp"; + process.env.GRVT_SYMBOL = "BTCUSDT"; + delete process.env.GRVT_SIGNER_PATH; + + const adapter = createExchangeAdapter({ symbol: "BTCUSDT" }); + expect(adapter).toBeInstanceOf(GrvtExchangeAdapter); + expect(adapter.id).toBe("grvt"); + }); +});