From 0ef0d487057ad3cd2bd4fefba2c7ffd7b86fae1f Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 22 Nov 2025 21:51:50 +0000 Subject: [PATCH 01/37] update python package config --- .gitignore | 3 + pyproject.toml | 5 + src/designer_plugin/py.typed | 0 uv.lock | 714 ++++++++++++++++++++++++++++++++++- 4 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 src/designer_plugin/py.typed diff --git a/.gitignore b/.gitignore index 0a19790..3d6f6de 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# MacOS +.DS_Store diff --git a/pyproject.toml b/pyproject.toml index e2b23f7..29fcd8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ authors = [ { name = "Taegyun Ha", email = "taegyun.ha@disguise.one" } ] dependencies = [ + "aiohttp>=3.13.2", + "pydantic>=2.12.4", "zeroconf>=0.39.0", ] requires-python = ">=3.11" @@ -21,6 +23,9 @@ classifiers = [ ] readme = "README.md" +[tool.setuptools.package-data] +designer_plugin = ["py.typed"] + [tool.setuptools.packages.find] where = ["src"] diff --git a/src/designer_plugin/py.typed b/src/designer_plugin/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock index c3aa3fa..71e2c5c 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,148 @@ version = 1 revision = 3 requires-python = ">=3.11" +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, + { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, + { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, + { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, + { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, + { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "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, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -25,6 +167,8 @@ name = "disguise-designer-plugin" version = "1.1.0" source = { editable = "." } dependencies = [ + { name = "aiohttp" }, + { name = "pydantic" }, { name = "zeroconf" }, ] @@ -37,7 +181,11 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "zeroconf", specifier = ">=0.39.0" }] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.13.2" }, + { name = "pydantic", specifier = ">=2.12.4" }, + { name = "zeroconf", specifier = ">=0.39.0" }, +] [package.metadata.requires-dev] dev = [ @@ -65,6 +213,111 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "identify" version = "2.6.15" @@ -74,6 +327,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + [[package]] name = "ifaddr" version = "0.2.0" @@ -92,6 +354,123 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + [[package]] name = "mypy" version = "1.18.2" @@ -200,6 +579,217 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, ] +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -315,6 +905,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "virtualenv" version = "20.35.4" @@ -329,6 +931,116 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + [[package]] name = "zeroconf" version = "0.148.0" From 5d8b389b6a2da4960ae17af41406dcfcf2600481 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 22 Nov 2025 23:17:13 +0000 Subject: [PATCH 02/37] add d3sdk --- pyproject.toml | 2 +- src/designer_plugin/__init__.py | 27 +- src/designer_plugin/api.py | 370 +++++++++ src/designer_plugin/d3sdk/__init__.py | 27 + src/designer_plugin/d3sdk/ast_utils.py | 386 +++++++++ src/designer_plugin/d3sdk/client.py | 420 ++++++++++ src/designer_plugin/d3sdk/function.py | 485 +++++++++++ src/designer_plugin/d3sdk/session.py | 312 ++++++++ src/designer_plugin/designer_plugin.py | 9 +- src/designer_plugin/models.py | 165 ++++ test/test_ast_utils.py | 1023 ++++++++++++++++++++++++ test/test_core.py | 292 +++++++ test/test_plugin.py | 5 + uv.lock | 108 +++ 14 files changed, 3628 insertions(+), 3 deletions(-) create mode 100644 src/designer_plugin/api.py create mode 100644 src/designer_plugin/d3sdk/__init__.py create mode 100644 src/designer_plugin/d3sdk/ast_utils.py create mode 100644 src/designer_plugin/d3sdk/client.py create mode 100644 src/designer_plugin/d3sdk/function.py create mode 100644 src/designer_plugin/d3sdk/session.py create mode 100644 src/designer_plugin/models.py create mode 100644 test/test_ast_utils.py create mode 100644 test/test_core.py diff --git a/pyproject.toml b/pyproject.toml index 29fcd8c..05b0704 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ authors = [ dependencies = [ "aiohttp>=3.13.2", "pydantic>=2.12.4", + "requests>=2.32.5", "zeroconf>=0.39.0", ] requires-python = ">=3.11" @@ -43,7 +44,6 @@ dev = [ ] [tool.ruff] -line-length = 100 target-version = "py311" exclude = [ "test", diff --git a/src/designer_plugin/__init__.py b/src/designer_plugin/__init__.py index 8292781..fb9fb60 100644 --- a/src/designer_plugin/__init__.py +++ b/src/designer_plugin/__init__.py @@ -1,3 +1,28 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + from .designer_plugin import DesignerPlugin +from .models import ( + PluginError, + PluginException, + PluginPayload, + PluginRegisterResponse, + PluginResponse, + PluginStatus, + PluginStatusDetail, + RegisterPayload, +) -__all__ = ["DesignerPlugin"] +__all__: list[str] = [ + "DesignerPlugin", + "PluginError", + "PluginException", + "PluginPayload", + "PluginRegisterResponse", + "PluginResponse", + "PluginStatus", + "PluginStatusDetail", + "RegisterPayload", +] diff --git a/src/designer_plugin/api.py b/src/designer_plugin/api.py new file mode 100644 index 0000000..ac7a6bc --- /dev/null +++ b/src/designer_plugin/api.py @@ -0,0 +1,370 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +from enum import StrEnum +from typing import Any, Unpack + +import aiohttp +import requests +from pydantic import ValidationError + +from designer_plugin.models import ( + D3_PLUGIN_ENDPOINT, + D3_PLUGIN_MODULE_REG_ENDPOINT, + PluginError, + PluginException, + PluginPayload, + PluginRegisterResponse, + PluginResponse, + RegisterPayload, + RetType, +) + + +############################################################################### +# Plugin endpoint constants +def get_plugin_endpoint_url(hostname: str, port: int) -> str: + """Get the full URL for the plugin execution endpoint. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + + Returns: + Full HTTP URL for plugin execution endpoint. + """ + return f"http://{hostname}:{port}/{D3_PLUGIN_ENDPOINT}" + + +def get_plugin_module_register_url(hostname: str, port: int) -> str: + """Get the full URL for the module registration endpoint. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + + Returns: + Full HTTP URL for module registration endpoint. + """ + return f"http://{hostname}:{port}/{D3_PLUGIN_MODULE_REG_ENDPOINT}" + + +############################################################################### +# Low level request +class Method(StrEnum): + GET = "GET" + OPTIONS = "OPTIONS" + HEAD = "HEAD" + POST = "POST" + PUT = "PUT" + PATCH = "PATCH" + DELETE = "DELETE" + + +def d3_api_request( + method: Method, + hostname: str, + port: int, + url_endpoint: str, + **kwargs, +) -> Any: + """Make a synchronous HTTP request to Designer API. + + Args: + method: HTTP method to use. + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + url_endpoint: The API endpoint path. + **kwargs: Additional arguments to pass to requests.request. + + Returns: + JSON response from the API. + """ + url: str = f"http://{hostname}:{port}/{url_endpoint.lstrip('/')}" + response = requests.request( + method, + url, + **kwargs, + ) + return response.json() + + +async def d3_api_arequest( + method: Method, + hostname: str, + port: int, + url_endpoint: str, + **kwargs: Unpack[aiohttp.client._RequestOptions], +) -> Any: + """Make an asynchronous HTTP request to Designer API. + + Args: + method: HTTP method to use. + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + url_endpoint: The API endpoint path. + **kwargs: Additional arguments to pass to aiohttp session.request. + + Returns: + JSON response from the API. + """ + url: str = f"http://{hostname}:{port}/{url_endpoint.lstrip('/')}" + async with aiohttp.ClientSession() as session: + async with session.request( + method, + url, + **kwargs, + ) as response: + return await response.json() + + +############################################################################### +# API async interface +async def d3_api_aplugin_raw( + hostname: str, + port: int, + json: dict[str, str], + timeout_sec: float | None = None, +) -> PluginResponse: + """Execute a raw plugin script asynchronously on Designer. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + json: Raw JSON payload containing script to execute. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginResponse containing the execution result. + + Raises: + PluginException: If the plugin execution fails. + """ + response: Any = await d3_api_arequest( + Method.POST, + hostname, + port, + D3_PLUGIN_ENDPOINT, + json=json, + timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, + ) + + try: + return PluginResponse.model_validate(response) + except ValidationError: + error_response: PluginError = PluginError.model_validate(response) + raise PluginException( + status=error_response.status, + d3Log=error_response.d3Log, + pythonLog=error_response.pythonLog, + ) from None + + +async def d3_api_aplugin( + hostname: str, + port: int, + payload: PluginPayload[RetType], + timeout_sec: float | None = None, +) -> PluginResponse[RetType]: + """Execute a plugin script asynchronously on Designer with type-safe payload. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + payload: PluginPayload containing script and optional module name. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginResponse with typed return value. + + Raises: + PluginException: If the plugin execution fails. + """ + response: Any = await d3_api_arequest( + Method.POST, + hostname, + port, + D3_PLUGIN_ENDPOINT, + json=payload.model_dump(), + timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, + ) + try: + return PluginResponse[RetType].model_validate(response) + except ValidationError: + error_response: PluginError = PluginError.model_validate(response) + raise PluginException( + status=error_response.status, + d3Log=error_response.d3Log, + pythonLog=error_response.pythonLog, + ) from None + + +async def d3_api_aregister_module( + hostname: str, port: int, payload: RegisterPayload, timeout_sec: float | None = None +) -> PluginRegisterResponse: + """Register a module asynchronously with Designer. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + payload: RegisterPayload containing module name and contents. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginRegisterResponse confirming successful registration. + + Raises: + Exception: If the network request fails. + PluginException: If module registration fails on Designer side. + """ + try: + response: Any = await d3_api_arequest( + Method.POST, + hostname, + port, + D3_PLUGIN_MODULE_REG_ENDPOINT, + json=payload.model_dump(), + timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, + ) + except Exception as e: + raise Exception(f"Failed to register module '{payload.moduleName}") from e + + plugin_response: PluginRegisterResponse = PluginRegisterResponse.model_validate( + response + ) + + # if we fail to register module, all d3functions plugin will fail. + # therefore, we should raise exception + if plugin_response.status.code != 0: + raise PluginException(status=plugin_response.status) + + return plugin_response + + +############################################################################### +# API sync interface +def d3_api_plugin_raw( + hostname: str, + port: int, + json: dict[str, str], + timeout_sec: float | None = None, +) -> PluginResponse: + """Execute a raw plugin script synchronously on Designer. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + json: Raw JSON payload containing script to execute. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginResponse containing the execution result. + + Raises: + PluginException: If the plugin execution fails. + """ + response: Any = d3_api_request( + Method.POST, + hostname, + port, + D3_PLUGIN_ENDPOINT, + json=json, + timeout=timeout_sec if timeout_sec else None, + ) + + try: + return PluginResponse.model_validate(response) + except ValidationError: + error_response: PluginError = PluginError.model_validate(response) + raise PluginException( + status=error_response.status, + d3Log=error_response.d3Log, + pythonLog=error_response.pythonLog, + ) from None + + +def d3_api_plugin( + hostname: str, + port: int, + payload: PluginPayload[RetType], + timeout_sec: float | None = None, +) -> PluginResponse[RetType]: + """Execute a plugin script synchronously on Designer with type-safe payload. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + payload: PluginPayload containing script and optional module name. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginResponse with typed return value. + + Raises: + PluginException: If the plugin execution fails. + """ + response = d3_api_request( + Method.POST, + hostname, + port, + D3_PLUGIN_ENDPOINT, + json=payload.model_dump(), + timeout=timeout_sec if timeout_sec else None, + ) + + try: + return PluginResponse[RetType].model_validate(response) + except ValidationError: + error_response: PluginError = PluginError.model_validate(response) + raise PluginException( + status=error_response.status, + d3Log=error_response.d3Log, + pythonLog=error_response.pythonLog, + ) from None + + +def d3_api_register_module( + hostname: str, + port: int, + payload: RegisterPayload, + timeout_sec: float | None = None, +) -> PluginRegisterResponse: + """Register a module synchronously with Designer. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + payload: RegisterPayload containing module name and contents. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginRegisterResponse confirming successful registration. + + Raises: + Exception: If the network request fails. + PluginException: If module registration fails on Designer side. + """ + try: + response: Any = d3_api_request( + Method.POST, + hostname, + port, + D3_PLUGIN_MODULE_REG_ENDPOINT, + json=payload.model_dump(), + timeout=timeout_sec if timeout_sec else None, + ) + except Exception as e: + raise Exception(f"Failed to register module: {payload.moduleName}") from e + + plugin_response: PluginRegisterResponse = PluginRegisterResponse.model_validate( + response + ) + + # if we fail to register module, all d3functions plugin will fail. + # therefore, we should raise exception + if plugin_response.status.code != 0: + raise PluginException(status=plugin_response.status) + + return plugin_response diff --git a/src/designer_plugin/d3sdk/__init__.py b/src/designer_plugin/d3sdk/__init__.py new file mode 100644 index 0000000..7f5989e --- /dev/null +++ b/src/designer_plugin/d3sdk/__init__.py @@ -0,0 +1,27 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +from .client import D3PluginClient +from .function import ( + add_packages_in_current_file, + d3function, + d3pythonscript, + get_all_d3functions, + get_all_modules, + get_register_payload, +) +from .session import D3AsyncSession, D3Session + +__all__: list[str] = [ + "D3AsyncSession", + "D3PluginClient", + "D3Session", + "d3pythonscript", + "d3function", + "add_packages_in_current_file", + "get_register_payload", + "get_all_d3functions", + "get_all_modules", +] diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py new file mode 100644 index 0000000..7aaa5a5 --- /dev/null +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -0,0 +1,386 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +import ast +import inspect +import textwrap +import types + + +############################################################################### +# Source code extraction utilities +def get_source(frame: types.FrameType) -> str | None: + """Extract and dedent source code from a frame object. + + Args: + frame: The frame object to extract source code from + + Returns: + Dedented source code as a string, or None if source cannot be found + + Raises: + OSError: If the source file cannot be found or read + """ + source_lines, _ = inspect.findsource(frame) + return textwrap.dedent("".join(source_lines)) if source_lines else None + + +def get_class_node(tree, class_name: str) -> ast.ClassDef | None: + """Find a class definition node by name in an AST. + + Args: + tree: The AST tree to search + class_name: The name of the class to find + + Returns: + The ClassDef node if found, None otherwise + """ + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == class_name: + return node + return None + + +############################################################################### +# AST node filtering utilities +def filter_base_classes(class_node: ast.ClassDef): + """Remove all base classes from a class definition for Python 2.7 compatibility. + + This function modifies the class_node in-place by clearing its base class list. + Inheritance is not supported in the current D3 Designer plugin system. + + Args: + class_node: The class definition node to process + """ + class_node.bases = [] + + +def filter_init_args(class_node: ast.ClassDef) -> list[str]: + """Remove excluded arguments from __init__ method and extract parameter names. + + This function modifies the class_node in-place by: + 1. Removing excluded parameters from __init__ signature + 2. Removing excluded arguments from super().__init__() calls + 3. Returning the list of remaining parameter names (excluding 'self') + + Args: + class_node: The class definition node to process + + Returns: + List of parameter names that remain after filtering (excluding 'self') + """ + for node in class_node.body: + if not isinstance(node, ast.FunctionDef): + continue + if node.name != "__init__": + continue + + # Return filtered parameter names (excluding 'self' which is implicit) + return [arg.arg for arg in node.args.args if arg.arg != "self"] + + return [] + + +############################################################################### +# Type hint removal utilities +class ConvertToPython27(ast.NodeTransformer): + """AST transformer to convert Python 3 code to Python 2.7 compatible format. + + This transformer performs the following conversions: + - Removes function return type annotations (def func() -> int) + - Removes argument type annotations (def func(x: int)) + - Converts annotated assignments to regular assignments (x: int = 5 → x = 5) + - Removes await keywords from async expressions (await func() → func()) + - Converts f-strings to .format() style (f"Hello {name}" → "Hello {}".format(name)) + """ + + def visit_FunctionDef(self, node: ast.FunctionDef): + """Remove return type annotation from function definitions. + + Transforms 'def func() -> int:' to 'def func():' for Python 2.7 compatibility. + + Args: + node: The function definition AST node to transform. + + Returns: + The function node without return type annotation. + """ + node.returns = None + self.generic_visit(node) + return node + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): + """Convert async function to regular function for Python 2.7 compatibility. + + Transforms 'async def func() -> int:' to 'def func():' by: + 1. Creating a new FunctionDef node with the same properties + 2. Removing return type annotation via visit_FunctionDef + 3. Returning the FunctionDef to replace the AsyncFunctionDef in the AST + + Args: + node: The async function definition AST node to transform. + + Returns: + A regular FunctionDef node without async keyword or return type annotation. + """ + # Build the replacement FunctionDef + new = ast.FunctionDef( + name=node.name, + args=node.args, + body=node.body, + decorator_list=node.decorator_list, + returns=node.returns, + type_comment=getattr(node, "type_comment", None), + ) + + # Preserve source location + new = ast.copy_location(new, node) + + # Now run normal FunctionDef logic + recurse + return self.visit_FunctionDef(new) + + def visit_arg(self, node: ast.arg): + """Remove type annotation from argument. + + Args: + node: The argument AST node to transform. + + Returns: + The argument node without type annotation. + """ + node.annotation = None + return node + + def visit_AnnAssign(self, node: ast.AnnAssign): + """Remove type hint. + + Converts type-annotated variable assignments (e.g., 'x: int = 5') into regular + assignments (e.g., 'x = 5'). If the annotated assignment has no value (e.g., 'x: int'), + it is removed entirely as Python 2.7 does not support variable declarations without values. + + Args: + node: The annotated assignment AST node to transform. + + Returns: + Regular Assign node without type annotation if value exists, None otherwise. + """ + if node.value is None: + return None + + return ast.Assign( + targets=[node.target], + value=node.value, + lineno=node.lineno, + col_offset=node.col_offset, + ) + + def visit_Await(self, node: ast.Await): + """Remove await keyword. + + Remove await keyword and return the underlying expression. + Transforms 'await expr()' to 'expr()'. + + Args: + node: The await AST node to transform. + + Returns: + The underlying expression without the await wrapper. + """ + return self.visit(node.value) + + def visit_JoinedStr(self, node: ast.JoinedStr): + # Don't use generic_visit here because we need to handle format_spec specially + # Process the node values manually to preserve format specs + + fmt_parts = [] + args = [] + + for value in node.values: + # Literal pieces of the f-string + if isinstance(value, ast.Constant) and isinstance(value.value, str): + # Escape braces so they are not taken as format fields + text = value.value.replace("{", "{{").replace("}", "}}") + fmt_parts.append(text) + + # { … } expressions + elif isinstance(value, ast.FormattedValue): + placeholder = "{" + + # Handle !r / !s / !a + if value.conversion != -1: + placeholder += "!" + chr(value.conversion) + + # Handle format specs, e.g. {x:.2f} + if value.format_spec is not None: + # f-string format specs themselves are JoinedStr nodes + fspec = value.format_spec + if ( + isinstance(fspec, ast.JoinedStr) + and len(fspec.values) == 1 + and isinstance(fspec.values[0], ast.Constant) + and isinstance(fspec.values[0].value, str) + ): + placeholder += ":" + fspec.values[0].value + else: + # For more complex specs we could fall back, but let's keep it simple + pass + + placeholder += "}" + fmt_parts.append(placeholder) + + # Transform the expression value (but not the format_spec) + args.append(self.visit(value.value)) + + else: + # Unusual case for f-strings – just in case + raise NotImplementedError( + f"Unsupported JoinedStr part: {ast.dump(value)}" + ) + + # Build "string".format(*args) + fmt_str = ast.Constant("".join(fmt_parts)) + new_node = ast.Call( + func=ast.Attribute(value=fmt_str, attr="format", ctx=ast.Load()), + args=args, + keywords=[], + ) + return ast.copy_location(new_node, node) + + +############################################################################### +# Python 2.7 conversion utilities +def convert_function_to_py27( + function_node: ast.FunctionDef | ast.AsyncFunctionDef, +) -> ast.FunctionDef: + """Convert a function AST node to Python 2.7 compatible format. + + This function removes all type annotations from a function definition, + including return type annotations, parameter type annotations, and + type hints within the function body to ensure Python 2.7 compatibility. + + WARNING: This function modifies the input node in-place for FunctionDef nodes. + For AsyncFunctionDef nodes, a new FunctionDef node is created. + + Args: + function_node: The function AST node to convert to Python 2.7 format. + This node will be modified in-place if it's a FunctionDef. + + Returns: + The converted FunctionDef node. For FunctionDef input, returns the same + (modified) node. For AsyncFunctionDef input, returns a new FunctionDef node. + """ + transformer = ConvertToPython27() + return transformer.visit(function_node) + + +def convert_class_to_py27(class_node: ast.ClassDef) -> None: + """Convert all methods in a class to Python 2.7 compatible format. + + This function modifies the class_node in-place by converting all function definitions + (both sync and async) to Python 2.7 compatible format. This includes: + 1. Converting AsyncFunctionDef nodes to regular FunctionDef nodes + 2. Removing type annotations from all methods + 3. Recursively processing method bodies using convert_function_to_py27 + + Args: + class_node: The class definition node to convert + """ + for i, node in enumerate(class_node.body): + if isinstance(node, ast.AsyncFunctionDef) or isinstance(node, ast.FunctionDef): + class_node.body[i] = convert_function_to_py27(node) + + +############################################################################### +# Python package finder utility +def find_packages_in_current_file(caller_stack: int = 1) -> list[str]: + """Find all import statements in the caller's file by inspecting the call stack. + + This function walks up the call stack to find the module where it was called from, + then parses that module's source code to extract all import statements that are + compatible with Python 2.7 and safe to send to D3 Designer. + + Args: + caller_stack: Number of frames to go up the call stack. Default is 1 (immediate caller). + Use higher values to inspect files further up the call chain. + + Returns: + Sorted list of unique import statement strings (e.g., "import ast", "from pathlib import Path"). + + Filters applied: + - Excludes imports inside `if TYPE_CHECKING:` blocks (type checking only) + - Excludes imports from the 'd3blobgen' package (client-side only) + - Excludes imports from the 'typing' module (not supported in Python 2.7) + - Excludes imports of this function itself to avoid circular references + """ + # Get the this file frame + current_frame: types.FrameType | None = inspect.currentframe() + if not current_frame: + return [] + + # Get the caller's frame (file where this function is called) + caller_frame: types.FrameType | None = current_frame + for _ in range(caller_stack): + if not caller_frame or not caller_frame.f_back: + return [] + caller_frame = caller_frame.f_back + + if not caller_frame: + return [] + + modules: types.ModuleType | None = inspect.getmodule(caller_frame) + if not modules: + return [] + + source: str = inspect.getsource(modules) + + # Parse the source code + tree = ast.parse(source) + + # Get the name of this function to filter it out + # For example, we don't want `from core import find_packages_in_current_file` + function_name: str = current_frame.f_code.co_name + # Skip any package from d3blobgen + d3blobgen_package_name: str = "d3blobgen" + # typing not supported in python2.7 + typing_package_name: str = "typing" + + def is_type_checking_block(node: ast.If) -> bool: + """Check if an if statement is 'if TYPE_CHECKING:'""" + return isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING" + + imports: list[str] = [] + for node in tree.body: + # Skip TYPE_CHECKING blocks entirely + if isinstance(node, ast.If) and is_type_checking_block(node): + continue + + if isinstance(node, ast.Import): + imported_modules: list[str] = [alias.name for alias in node.names] + # Skip imports that include d3blobgen + if any(d3blobgen_package_name in module for module in imported_modules): + continue + if any(typing_package_name in module for module in imported_modules): + continue + import_text: str = f"import {', '.join(imported_modules)}" + imports.append(import_text) + + elif isinstance(node, ast.ImportFrom): + imported_module: str | None = node.module + imported_names: list[str] = [alias.name for alias in node.names] + if not imported_module: + continue + # Skip imports that include d3blobgen + if d3blobgen_package_name in imported_module: + continue + elif typing_package_name in imported_module: + continue + # Skip imports that include this function itself + if function_name in imported_names: + continue + + line_text = f"from {imported_module} import {', '.join(imported_names)}" + imports.append(line_text) + + return sorted(set(imports)) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py new file mode 100644 index 0000000..17c01d4 --- /dev/null +++ b/src/designer_plugin/d3sdk/client.py @@ -0,0 +1,420 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +import ast +import functools +import inspect +import types +from collections.abc import Callable +from contextlib import asynccontextmanager, contextmanager +from typing import Any, ParamSpec, TypeVar + +from designer_plugin.api import ( + d3_api_aplugin, + d3_api_aregister_module, + d3_api_plugin, + d3_api_register_module, +) +from designer_plugin.d3sdk.ast_utils import ( + convert_class_to_py27, + filter_base_classes, + filter_init_args, + get_class_node, + get_source, +) +from designer_plugin.models import ( + PluginPayload, + PluginResponse, + RegisterPayload, +) + +P = ParamSpec("P") +T = TypeVar("T") + + +def build_payload(self, method_name: str, args, kwargs) -> PluginPayload[Any]: + """Build plugin payload for remote method execution. + + Args: + self: The plugin client instance. + method_name: Name of the method to execute remotely. + args: Positional arguments for the method. + kwargs: Keyword arguments for the method. + + Returns: + PluginPayload containing the script to execute remotely. + """ + # Serialize arguments to string representation for remote execution + args_parts: list[str] = [repr(arg) for arg in args] + kwargs_parts: list[str] = [f"{key}={repr(value)}" for key, value in kwargs.items()] + all_args: str = ", ".join(args_parts + kwargs_parts) + + # Build the Python script that will execute remotely on Designer + script = f"return plugin.{method_name}({all_args})" + + # Create payload containing script and module info + return PluginPayload[Any](moduleName=self.module_name, script=script) + + +def create_d3_plugin_method_wrapper(method_name: str, original_method: Callable[P, T]): + """Create a wrapper that executes a method remotely via Designer API calls. + + This wrapper intercepts method calls and instead of executing locally: + 1. Serializes the arguments using repr() + 2. Builds a script string in the form: "return plugin.{method_name}({args})" + 3. Creates a PluginPayload with the script and module information + 4. Sends it to Designer via d3_api_plugin or d3_api_aplugin + 5. Returns the result from the remote execution + + Args: + method_name: Name of the method to wrap + original_method: The original method object (used for type hints and async detection) + + Returns: + An async wrapper if the original method is async, otherwise a sync wrapper. + Both wrappers preserve the original method's metadata via functools.wraps. + """ + # Determine whether to create async or sync wrapper based on original method + if inspect.iscoroutinefunction(original_method): + # Create async wrapper that uses async Designer API call + @functools.wraps(original_method) + async def async_wrapper(self, *args, **kwargs): + payload = build_payload(self, method_name, args, kwargs) + response: PluginResponse[T] = await d3_api_aplugin( + self.hostname, self.port, payload + ) + return response.returnValue + + return async_wrapper + else: + # Create sync wrapper that uses synchronous Designer API call + @functools.wraps(original_method) + def sync_wrapper(self, *args, **kwargs): + payload = build_payload(self, method_name, args, kwargs) + response: PluginResponse[T] = d3_api_plugin( + self.hostname, self.port, payload + ) + return response.returnValue + + return sync_wrapper + + +def create_d3_payload_wrapper(method_name: str, original_method: Callable[P, T]): + """Create a wrapper that generates plugin payload without executing. + + Args: + method_name: Name of the method to wrap. + original_method: The original method object for type hints. + + Returns: + Wrapper function that returns PluginPayload instead of executing remotely. + """ + + @functools.wraps(original_method) + def sync_wrapper(self, *args, **kwargs) -> PluginPayload[T]: + return build_payload(self, method_name, args, kwargs) + + return sync_wrapper + + +class D3PluginClientMeta(type): + """Metaclass for Designer plugin clients that enables remote method execution. + + This metaclass intercepts class creation to perform several transformations: + + 1. Source Code Extraction: + - Extracts the source code of the class being defined using frame inspection + - Parses it into an AST for manipulation + + 2. Code Filtering: + - Removes client-side-only class variables (e.g., module_name) + - Filters out client-side-only __init__ parameters (hostname, port) + + 3. Python 2.7 Conversion: + - Converts async methods to sync for Designer's Python 2.7 runtime + - Generates both Python 3 and Python 2.7 versions of the source code + + 4. Method Wrapping: + - Wraps all user-defined methods to execute remotely via Designer API + - Preserves async/sync nature of original methods + + 5. Code Generation: + - Creates templates for instantiating the plugin on the remote side + - Stores metadata needed for module registration + + Class Attributes (set dynamically on subclasses): + filtered_init_args: List of __init__ parameter names after filtering + source_code: Python 3 source code with filtered variables + source_code_py27: Python 2.7 compatible source code + module_name: Name used to register the module with Designer + instance_code_template: Template string for instantiating the plugin remotely + instance_code: Actual instantiation code with concrete argument values + """ + + # Type hints for dynamically set class attributes + filtered_init_args: list[str] + source_code: str + source_code_py27: str + module_name: str + instance_code_template: str + instance_code: str + + def __new__(cls, name, bases, attrs): + # Skip the base class + if name == "D3PluginClient": + return super().__new__(cls, name, bases, attrs) + + # Use class name as default module_name if not explicitly provided + attrs["module_name"] = name + + # Get the caller's frame (where the class is being defined in user code) + frame: types.FrameType | None = inspect.currentframe() + if not frame: + raise ValueError( + f"D3PluginClientMeta: Failed to extract source code for {name}" + ) + + caller_frame = frame.f_back + if not caller_frame: + raise ValueError( + f"D3PluginClientMeta: Failed to extract source code for {name}" + ) + + # Extract full source code from the calling frame's file + source_code: str | None = get_source(caller_frame) + if not source_code: + raise ValueError( + f"D3PluginClientMeta: Failed to extract source code for {name}" + ) + + # Parse source code into Abstract Syntax Tree for manipulation + tree: ast.Module = ast.parse(source_code) + + # Locate the specific class definition node within the AST + class_node: ast.ClassDef | None = get_class_node(tree, name) + if not class_node: + raise ValueError( + f"D3PluginClientMeta: Failed to find class definition for {name}" + ) + + # Remove all base class for now as we don't support inheritance + filter_base_classes(class_node) + + # Filter out client-side-only __init__ arguments and get remaining params + filtered_init_args: list[str] = filter_init_args(class_node) + formated_filtered_init_args: list[str] = [ + f"{{{arg}}}" for arg in filtered_init_args + ] + + # Unparse modified AST back to Python 3 source code (clean, no comments) + attrs["source_code"] = f"{ast.unparse(class_node)}" + # Create template for instantiating the plugin remotely with placeholders + attrs["instance_code_template"] = ( + f"plugin = {name}({','.join(formated_filtered_init_args)})" + ) + attrs["filtered_init_args"] = filtered_init_args + + # Convert async methods to Python 2.7 compatible sync methods + convert_class_to_py27(class_node) + attrs["source_code_py27"] = f"{ast.unparse(class_node)}" + + # Wrap all user-defined public methods to execute remotely via D3 API + # Skip private methods (_*) and internal framework methods + for attr_name, attr_value in attrs.items(): + if callable(attr_value) and not attr_name.startswith("__"): + attrs[attr_name] = create_d3_plugin_method_wrapper( + attr_name, attr_value + ) + + return super().__new__(cls, name, bases, attrs) + + def __call__(cls, *args, **kwargs): + """Create an instance and generate its remote instantiation code. + + This method is called when a class instance is created (e.g., MyPlugin(...)). + It maps the provided arguments to the filtered parameter names and generates + the instance_code that will be used to instantiate the plugin remotely. + + Args: + *args: Positional arguments for the plugin __init__ + **kwargs: Keyword arguments for the plugin __init__ + + Returns: + An instance of the plugin class with instance_code attribute set + """ + # Build mapping from parameter names to their repr() values for remote instantiation + param_names: list[str] = cls.filtered_init_args + arg_mapping: dict[str, str] = {} + + # Map positional arguments + for i, param_name in enumerate(param_names): + filtered_idx = i # Account for excluded client-side args + if filtered_idx < len(args): + arg_mapping[param_name] = repr(args[filtered_idx]) + + # Map keyword arguments that match filtered parameter names + for key, value in kwargs.items(): + if key in param_names: + arg_mapping[key] = repr(value) + + # Replace placeholders in template with actual serialized argument values + instance_code: str = cls.instance_code_template.format(**arg_mapping) + + # Create the actual client instance with all original arguments + instance = super().__call__(*args, **kwargs) + + # Attach the generated instance_code for use during module registration + instance.instance_code = instance_code + + return instance + + +class D3PluginClient(metaclass=D3PluginClientMeta): + """Base class for creating Designer plugin clients. + + This class provides the foundation for building plugins that execute remotely + on Designer. When you subclass D3PluginClient, the metaclass automatically: + - Extracts and processes your class source code + - Converts it to Python 2.7 compatible code + - Wraps all your methods to execute remotely + - Manages module registration with Designer + + Usage: + ```python + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from d3blobgen.scripts.d3 import * + + class MyPlugin(D3PluginClient): + def __init__(self, arg1: int, arg2: str): + # Passed argument will be cached and used on register + self.arg1: int = arg1 + self.arg2: str = arg2 + + def get_surface_uid(self, surface_name: str) -> dict[str, str]: + surface: Screen2 = resourceManager.load( + Path('objects/screen2/{}.apx'.format(surface_name)), + Screen2 + ) + return { + "name": surface.description, + "uid": surface.uid, + } + + # Instantiate MyPlugin + plugin = MyPlugin(1, "myplugin") + + # Use as sync context manager + with plugin.session("localhost", 80): + result = await plugin.get_surface_uid("surface 1") + ``` + Attributes: + instance_code: The code used to instantiate the plugin remotely (set on init) + """ + + def __init__(self): + self.hostname: str | None = None + self.port: int | None = None + + def in_session(self): + """Check if the client is currently in an active session. + + Returns: + True if both hostname and port are set, False otherwise. + """ + return self.hostname and self.port + + @asynccontextmanager + async def async_session( + self, hostname: str, port: int, module_name: str | None = None + ): + """Async context manager for plugin session with Designer. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + module_name: Optional module name to override the default. + + Yields: + The plugin client instance with active session. + """ + try: + if module_name: + self.module_name = module_name + + self.hostname = hostname + self.port = port + await self._aregister(hostname, port) + print("Entering D3PluginModule context") + yield self + finally: + self.hostname = None + self.port = None + print("Exiting D3PluginModule context") + + @contextmanager + def session(self, hostname: str, port: int, module_name: str | None = None): + """Sync context manager for plugin session with Designer. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + module_name: Optional module name to override the default. + + Yields: + The plugin client instance with active session. + """ + try: + if module_name: + self.module_name = module_name + + self.hostname = hostname + self.port = port + self._register(hostname, port) + print("Entering D3PluginModule context") + yield self + finally: + self.hostname = None + self.port = None + print("Exiting D3PluginModule context") + + async def _aregister(self, hostname: str, port: int) -> None: + """Register the plugin module with Designer asynchronously. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + """ + await d3_api_aregister_module( + hostname, port, self._get_register_module_payload() + ) + + def _register(self, hostname: str, port: int) -> None: + """Register the plugin module with Designer synchronously. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + """ + d3_api_register_module(hostname, port, self._get_register_module_payload()) + + def _get_register_module_content(self) -> str: + """Generate the complete module content to register with Designer. + + Returns: + String containing the full module code to execute on Designer. + """ + return f"{self.source_code_py27}\n\n{self.instance_code}" # type: ignore[attr-defined] + + def _get_register_module_payload(self) -> RegisterPayload: + """Build the module registration payload for Designer. + + Returns: + RegisterPayload containing moduleName and contents for registration. + """ + return RegisterPayload( + moduleName=self.module_name, # type: ignore[attr-defined] + contents=self._get_register_module_content(), + ) diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py new file mode 100644 index 0000000..36ca5f1 --- /dev/null +++ b/src/designer_plugin/d3sdk/function.py @@ -0,0 +1,485 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +import ast +import functools +import inspect +import textwrap +from collections import defaultdict +from collections.abc import Callable +from typing import Any, Generic, ParamSpec, TypeVar + +from pydantic import BaseModel, Field + +from designer_plugin.d3sdk.ast_utils import ( + convert_function_to_py27, + find_packages_in_current_file, +) +from designer_plugin.models import ( + PluginPayload, + RegisterPayload, +) + + +############################################################################### +# Plugin function related implementations +class FunctionInfo(BaseModel): + """Container for parsed function information extracted from Python source code. + + This model holds all the essential components of a function after parsing, + including its complete definition, name, body statements, and argument list. + """ + + source_code: str = Field( + description="full body of function blob without decorator (Function definition + body)" + ) + source_code_py27: str = Field( + description="full body of function blob without decorator in python2.7 format (Function definition + body)" + ) + name: str = Field(description="name of extracted function") + body: str = Field(description="body of extracted function in str format") + body_py27: str = Field( + description="body of extracted function in python2.7 str format" + ) + args: list[str] = Field( + default=[], description="list of arguments from extracted function" + ) + + +def extract_function_info(func: Callable[..., Any]) -> FunctionInfo: + """Parse function source code and extract name, body statements, and argument list. + + This function uses AST parsing to extract function information from the source code + of a callable Python function. It removes decorators and provides the clean function + definition along with parsed components. + + Args: + func: A callable Python function to analyse. + + Returns: + FunctionInfo: Object containing function name, body code, and argument names. + + Raises: + ValueError: If the input is not a function or cannot be parsed. + """ + + source_code = inspect.getsource(func) + # Remove common leading whitespace to handle functions defined with indentation + source_code = textwrap.dedent(source_code) + tree: ast.Module = ast.parse(source_code) + + # Check if first node exists and is a function + if not tree.body: + raise ValueError(f"Given input is not a function\ninput:{source_code}") + + first_node = tree.body[0] + if not isinstance(first_node, ast.FunctionDef) and not isinstance( + first_node, ast.AsyncFunctionDef + ): + raise ValueError(f"Given input is not a function\ninput:{source_code}") + + # Extract function blob without decorator + first_node.decorator_list.clear() + + # Extract blob in python 3 format + source_code: str = ast.unparse(first_node) + + # Extract function name + function_name: str = first_node.name + + # Extract body statements + body_nodes: list[ast.stmt] = first_node.body + + # Convert back to source code + body: str = "" + for stmt in body_nodes: + body += ast.unparse(stmt) + "\n" + + # Extract function arguments + args: list[str] = [] + for arg in first_node.args.args: + args.append(arg.arg) + + first_node_py27 = convert_function_to_py27(first_node) + source_code_py27: str = ast.unparse(first_node_py27) + + body_py27: str = "" + for stmt in body_nodes: + body_py27 += ast.unparse(stmt) + "\n" + + return FunctionInfo( + source_code=source_code, + source_code_py27=source_code_py27, + name=function_name, + body=body.strip(), + body_py27=body_py27, + args=args, + ) + + +P = ParamSpec("P") +T = TypeVar("T") + + +class D3PythonScript(Generic[P, T]): + def __init__(self, func: Callable[P, T]): + """Initialise a D3PythonScript wrapper around a Python function. + + Args: + func: The Python function to wrap for D3 execution. + """ + self._function: Callable[P, T] = func + self._function_info: FunctionInfo = extract_function_info(func) + + # Update wrapper to preserve function metadata for IDE + functools.update_wrapper(self, func) + + @property + def __signature__(self) -> inspect.Signature: + """Expose function signature for IDE introspection. + + Returns: + The signature of the wrapped function for IDE support. + """ + return inspect.signature(self._function) + + @property + def function_info(self) -> FunctionInfo: + """Get the parsed function information. + + Returns: + FunctionInfo object containing parsed details about the wrapped function. + """ + return self._function_info + + @property + def name(self) -> str: + """Get the name of the wrapped function. + + Returns: + The name of the wrapped function. + """ + return self._function_info.name + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + return self._function(*args, **kwargs) + + def __getattr__(self, name: str) -> Any: + """Proxy attribute access to the original function for IDE support. + + Args: + name: The attribute name to retrieve from the wrapped function. + + Returns: + The attribute value from the wrapped function. + """ + return getattr(self._function, name) + + def _args_to_assign(self, *args, **kwargs) -> str: + """Convert function arguments to assignment statements for standalone execution. + + Args: + *args: Positional arguments to convert. + **kwargs: Keyword arguments to convert. + + Returns: + String containing variable assignment statements, one per line. + """ + args_parts = [ + f"{self._function_info.args[i]}={repr(arg)}" for i, arg in enumerate(args) + ] + kwargs_parts = [f"{name}={repr(value)}" for name, value in kwargs.items()] + return "\n".join(args_parts + kwargs_parts) + + def payload(self, *args: P.args, **kwargs: P.kwargs) -> PluginPayload[T]: + """Generate an execution payload for standalone script execution. + + Creates a payload by inlining the function body with argument assignments. + + Args: + *args: Positional arguments to pass to the function. + **kwargs: Keyword arguments to pass to the function. + + Returns: + PluginPayload containing the script with argument assignments and function body. + """ + all_args: str = self._args_to_assign(*args, **kwargs) + return PluginPayload[T](script=f"{all_args}\n{self._function_info.body_py27}") + + +class D3Function(D3PythonScript[P, T]): + """Wrapper class for Python functions to be executed in Designer environment. + + This class transforms regular Python functions into Designer plugin compatible functions + that can be registered as modules and executed remotely. Unlike D3PythonScript which + inlines function bodies, D3Function registers the complete function definition as part + of a module, allowing efficient reuse across multiple executions. + + Class Attributes: + _available_packages: Registry mapping module names to their required import packages. + _available_d3functions: Registry mapping module names to their D3Function instances. + """ + + _available_packages: defaultdict[str, set[str]] = defaultdict(set) + _available_d3functions: defaultdict[str, set["D3Function"]] = defaultdict(set) + + def __init__(self, module_name: str, func: Callable[P, T]): + """Initialise a D3Function wrapper around a Python function. + + Registers the function in the module's function registry for later module registration. + + Args: + module_name: Name of the module to register this function under. + func: The Python function to wrap for D3 execution. + """ + # Add this function into available_d3_functions + self._module_name: str = module_name + + super().__init__(func) + + D3Function._available_d3functions[module_name].add(self) + + def __eq__(self, other) -> bool: + """Check equality based on function name for unique registration. + + Returns: + True if both are D3Functions with the same name, False otherwise. + """ + if not isinstance(other, D3Function): + return False + return self.name == other.name + + def __hash__(self) -> int: + """Generate hash based on function name for unique registration. + + Returns: + Hash value of the function name. + """ + return hash(self.name) + + def get_register_payload(self) -> RegisterPayload | None: + """Get the registration payload for this function's module. + + Returns: + RegisterPayload for the module containing this function, or None if module not found. + """ + return self.get_module_register_payload(self.module_name) + + @staticmethod + def get_module_register_payload(module_name: str) -> RegisterPayload | None: + """Generate a registration payload for all functions in a specific module. + + Combines all package imports and function definitions registered under the module name. + + Args: + module_name: The name of the module to generate the payload for. + + Returns: + RegisterPayload containing module name and combined package imports and function definitions, + or None if the module has no registered functions. + """ + if module_name not in D3Function._available_d3functions: + return None + + contents_packages: str = "\n".join( + list(D3Function._available_packages[module_name]) + ) + contents_functions: str = "\n\n".join( + [ + func.function_info.source_code_py27 + for func in D3Function._available_d3functions[module_name] + ] + ) + return RegisterPayload( + moduleName=module_name, + contents=f"{contents_packages}\n\n{contents_functions}", + ) + + @property + def module_name(self) -> str: + """Get the module name this function is registered under. + + Returns: + The module name for this function. + """ + return self._module_name + + def _args_to_string(self, *args, **kwargs) -> str: + """Convert function arguments to a string representation for function call generation. + + Args: + *args: Positional arguments to convert. + **kwargs: Keyword arguments to convert. + + Returns: + Comma-separated string representation of all arguments suitable for function calls. + """ + # Convert positional args + args_parts = [repr(arg) for arg in args] + # Convert keyword args + kwargs_parts = [f"{key}={repr(value)}" for key, value in kwargs.items()] + # Combine them + all_parts = args_parts + kwargs_parts + return f"{', '.join(all_parts)}" + + def payload(self, *args: P.args, **kwargs: P.kwargs) -> PluginPayload[T]: + """Generate an execution payload for module-based function execution. + + Creates a payload that calls the registered function by name within its module. + + Args: + *args: Positional arguments to pass to the function. + **kwargs: Keyword arguments to pass to the function. + + Returns: + PluginPayload containing the module name and script that calls the function by name. + """ + return PluginPayload[T]( + moduleName=self._module_name, + script=f"return {self._function_info.name}({self._args_to_string(*args, **kwargs)})", + ) + + +############################################################################### +# d3function API +def d3pythonscript(func: Callable[P, T]) -> D3PythonScript[P, T]: + """Decorator to wrap a Python function for standalone D3 Designer script execution. + + This decorator transforms a regular Python function into a D3PythonScript that generates + execution payloads for direct script execution in D3 Designer. Unlike @d3function, this + decorator does not register the function as a module and is intended for one-off script + execution where the function body is inlined with the arguments. + + Args: + func: The Python function to wrap. + + Returns: + D3PythonScript instance that wraps the function and provides payload generation. + + Examples: + ```python + @d3pythonscript + def my_add(a: int, b: int) -> int: + return a + b + + # Generate payload for execution + payload = my_add.payload(5, 3) + # The payload.script will contain: + ''' + a=5 + b=3 + return a + b + ''' + ``` + """ + return D3PythonScript(func) + + +# Actual implementation +def d3function(module_name: str = "") -> Callable[[Callable[P, T]], D3Function[P, T]]: + """Decorator to wrap a Python function for D3 Designer module registration and execution. + + This decorator transforms a regular Python function into a D3Function that can be registered + as a reusable module in D3 Designer and executed remotely. The decorated function is added to + the module's function registry and can be called by name after module registration. + + Unlike @d3pythonscript which inlines the function body, @d3function registers the complete + function definition as part of a module, allowing efficient reuse across multiple executions. + + Args: + module_name: The module name to register this function under. This should be a unique + identifier for the module that will contain this function. Multiple functions + can share the same module_name to be registered together as a single module. + + Returns: + A decorator function that wraps the target function in a D3Function instance. + + Examples: + ```python + @d3function("my_d3module") + def capture_image(cam_name: str) -> str: + camera = d3.resourceManager.load( + d3.Path('objects/camera/{cam_name}.apx'), + d3.Camera + ) + return camera.uid + + # Generate payload for execution (calls the function by name) + payload = capture_image.payload("camera1") + # The payload.script will contain: + ''' + return capture_image('camera1') + ''' + ``` + """ + + def decorator(func: Callable[P, T]) -> D3Function[P, T]: + return D3Function(module_name, func) + + return decorator + + +def add_packages_in_current_file(module_name: str) -> None: + """Add all import statements from the caller's file to a D3 module's package list. + + This function scans the calling file's import statements and registers them with + the specified module name, making those imports available when the module is + registered with Designer. This is useful for ensuring all dependencies are included + when deploying Python functions to D3 Designer. + + Args: + module_name: The name of the D3 module to associate the packages with. + Must match the module_name used in @d3function decorator. + + Example: + ```python + import numpy as np + + @d3function("my_module") + def my_function(): + return np.array([1, 2, 3]) + + # Register all imports in the file (numpy) + add_packages_in_current_file("my_module") + ``` + """ + # caller_stack is 2, 1 for this, 1 for caller of this function. + packages: list[str] = find_packages_in_current_file(2) + D3Function._available_packages[module_name].update(packages) + + +def get_register_payload(module_name: str) -> RegisterPayload | None: + """Get the registration payload for a specific module. + + Args: + module_name: The name of the module to get the payload for. + + Returns: + RegisterPayload for the module, or None if the module has no registered functions. + """ + return D3Function.get_module_register_payload(module_name) + + +def get_all_d3functions() -> list[tuple[str, str]]: + """Retrieve all available d3function as module_name-function_name pairs. + + Returns: + List of tuples containing (module_name, function_name) for all registered D3 functions. + """ + module_function_pairs: list[tuple[str, str]] = [] + for module_name, funcs in D3Function._available_d3functions.items(): + module_function_pairs += [ + (module_name, func.function_info.name) for func in funcs + ] + return module_function_pairs + + +def get_all_modules() -> list[str]: + """Retrieve names of all modules registered with @d3function decorator. + + Returns: + List of module names that have registered D3 functions. + """ + return list(D3Function._available_d3functions.keys()) diff --git a/src/designer_plugin/d3sdk/session.py b/src/designer_plugin/d3sdk/session.py new file mode 100644 index 0000000..dbea2c8 --- /dev/null +++ b/src/designer_plugin/d3sdk/session.py @@ -0,0 +1,312 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +from typing import Any, Unpack + +import aiohttp + +from designer_plugin.api import ( + Method, + d3_api_aplugin, + d3_api_aregister_module, + d3_api_arequest, + d3_api_plugin, + d3_api_register_module, + d3_api_request, +) +from designer_plugin.d3sdk.function import D3Function +from designer_plugin.models import ( + PluginPayload, + PluginResponse, + RegisterPayload, + RetType, +) + + +class D3SessionBase: + """Base class for Designer session management.""" + + def __init__(self, hostname: str, port: int, context_modules: list[str]) -> None: + """Initialize base session with connection details and module context. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + context_modules: List of module names to register when entering session context. + """ + self.hostname: str = hostname + self.port: int = port + self.context_modules: list[str] = context_modules + + +class D3Session(D3SessionBase): + """Synchronous session for executing plugins on Designer. + + Manages connection to a Designer instance and provides synchronous API for + plugin execution, module registration, and generic HTTP requests. + """ + + def __init__( + self, hostname: str, port: int, context_modules: list[str] | None = None + ) -> None: + """Initialize synchronous Designer session. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + context_modules: Optional list of module names to register when entering session context. + """ + super().__init__(hostname, port, context_modules or []) + + def __enter__(self) -> "D3Session": + """Enter context manager and register all context modules. + + Returns: + The session instance. + + Raises: + RuntimeError: If any context module is not registered with @d3function. + """ + for module_name in self.context_modules: + is_registered: bool = self.register_module(module_name) + if not is_registered: + raise RuntimeError( + f"module {module_name} is not registered with d3function" + ) + return self + + def __exit__(self, _type, _value, _traceback) -> None: + """Exit context manager.""" + pass + + def rpc( + self, payload: PluginPayload[RetType], timeout_sec: float | None = None + ) -> RetType: + """Execute a remote procedure call and return only the return value. + + Args: + payload: Plugin payload containing script and optional module name. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + The return value from the plugin execution. + + Raises: + PluginException: If the plugin execution fails. + """ + return self.plugin(payload, timeout_sec).returnValue + + def plugin( + self, payload: PluginPayload[RetType], timeout_sec: float | None = None + ) -> PluginResponse[RetType]: + """Execute a plugin script on Designer. + + Args: + payload: Plugin payload containing script and optional module name. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginResponse containing status, logs, and return value. + + Raises: + PluginException: If the plugin execution fails. + """ + return d3_api_plugin(self.hostname, self.port, payload, timeout_sec) + + def request(self, method: Method, url_endpoint: str, **kwargs): + """Make a generic HTTP request to Designer API. + + Args: + method: HTTP method to use. + url_endpoint: The API endpoint path. + **kwargs: Additional arguments to pass to requests.request. + + Returns: + JSON response from the API. + """ + return d3_api_request(method, self.hostname, self.port, url_endpoint, **kwargs) + + def register_module( + self, module_name: str, timeout_sec: float | None = None + ) -> bool: + """Register a module with Designer. + + Args: + module_name: Name of the module to register. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + True if module was registered successfully, False if module not found. + + Raises: + PluginException: If module registration fails on Designer side. + """ + payload: RegisterPayload | None = D3Function.get_module_register_payload( + module_name + ) + if payload: + d3_api_register_module(self.hostname, self.port, payload, timeout_sec) + return True + return False + + def register_all_modules(self, timeout_sec: float | None = None) -> dict[str, bool]: + """Register all modules decorated with @d3function. + + Args: + timeout_sec: Optional timeout in seconds for each registration request. + + Returns: + Dictionary mapping module names to registration success status. + + Raises: + PluginException: If any module registration fails on Designer side. + """ + modules: list[str] = list(D3Function._available_d3functions.keys()) + register_success: dict[str, bool] = {} + for module_name in modules: + is_registered: bool = self.register_module(module_name, timeout_sec) + register_success[module_name] = is_registered + return register_success + + +class D3AsyncSession(D3SessionBase): + """Asynchronous session for executing plugins on Designer. + + Manages connection to a Designer instance and provides asynchronous API for + plugin execution, module registration, and generic HTTP requests. + """ + + def __init__( + self, hostname: str, port: int, context_modules: list[str] | None = None + ) -> None: + """Initialize asynchronous Designer session. + + Args: + hostname: The hostname of the Designer instance. + port: The port number of the Designer instance. + context_modules: Optional list of module names to register when entering session context. + """ + super().__init__(hostname, port, context_modules or []) + + async def __aenter__(self) -> "D3AsyncSession": + """Enter async context manager and register all context modules. + + Returns: + The session instance. + + Raises: + RuntimeError: If any context module is not registered with @d3function. + """ + for module_name in self.context_modules: + is_registered: bool = await self.register_module(module_name) + if not is_registered: + raise RuntimeError( + f"module {module_name} is not registered with d3function" + ) + return self + + async def __aexit__(self, _exc_type, _exc, _tb) -> None: + """Exit async context manager.""" + pass + + async def request( + self, + method: Method, + url_endpoint: str, + **kwargs: Unpack[aiohttp.client._RequestOptions], + ) -> Any: + """Make a generic HTTP request to Designer API asynchronously. + + Args: + method: HTTP method to use. + url_endpoint: The API endpoint path. + **kwargs: Additional arguments to pass to aiohttp session.request. + + Returns: + JSON response from the API. + """ + return await d3_api_arequest( + method, self.hostname, self.port, url_endpoint, **kwargs + ) + + async def rpc( + self, payload: PluginPayload[RetType], timeout_sec: float | None = None + ) -> RetType: + """Execute a remote procedure call asynchronously and return only the return value. + + Args: + payload: Plugin payload containing script and optional module name. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + The return value from the plugin execution. + + Raises: + PluginException: If the plugin execution fails. + """ + return (await self.plugin(payload, timeout_sec)).returnValue + + async def plugin( + self, payload: PluginPayload[RetType], timeout_sec: float | None = None + ) -> PluginResponse[RetType]: + """Execute a plugin script on Designer asynchronously. + + Args: + payload: Plugin payload containing script and optional module name. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + PluginResponse containing status, logs, and return value. + + Raises: + PluginException: If the plugin execution fails. + """ + return await d3_api_aplugin(self.hostname, self.port, payload, timeout_sec) + + async def register_module( + self, module_name: str, timeout_sec: float | None = None + ) -> bool: + """Register a module with Designer asynchronously. + + Args: + module_name: Name of the module to register. + timeout_sec: Optional timeout in seconds for the request. + + Returns: + True if module was registered successfully, False if module not found. + + Raises: + PluginException: If module registration fails on Designer side. + """ + payload: RegisterPayload | None = D3Function.get_module_register_payload( + module_name + ) + if payload: + await d3_api_aregister_module( + self.hostname, self.port, payload, timeout_sec + ) + return True + return False + + async def register_all_modules( + self, timeout_sec: float | None = None + ) -> dict[str, bool]: + """Register all modules decorated with @d3function asynchronously. + + Args: + timeout_sec: Optional timeout in seconds for each registration request. + + Returns: + Dictionary mapping module names to registration success status. + + Raises: + PluginException: If any module registration fails on Designer side. + """ + modules: list[str] = list(D3Function._available_d3functions.keys()) + register_success: dict[str, bool] = {} + for module_name in modules: + is_registered: bool = await self.register_module(module_name, timeout_sec) + register_success[module_name] = is_registered + return register_success diff --git a/src/designer_plugin/designer_plugin.py b/src/designer_plugin/designer_plugin.py index a0ad7d5..64228da 100644 --- a/src/designer_plugin/designer_plugin.py +++ b/src/designer_plugin/designer_plugin.py @@ -1,3 +1,8 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + import asyncio import socket from json import load as json_load @@ -37,7 +42,9 @@ def default_init(port: int, hostname: str | None = None) -> "DesignerPlugin": ) @staticmethod - def from_json_file(file_path: str, port: int, hostname: str | None = None) -> "DesignerPlugin": + def from_json_file( + file_path: str, port: int, hostname: str | None = None + ) -> "DesignerPlugin": """Convert a JSON file (expected d3plugin.json) to PluginOptions. hostname and port are required.""" with open(file_path) as f: options = json_load(f) diff --git a/src/designer_plugin/models.py b/src/designer_plugin/models.py new file mode 100644 index 0000000..82c7487 --- /dev/null +++ b/src/designer_plugin/models.py @@ -0,0 +1,165 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +import json +import traceback +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import typing_extensions +from pydantic import BaseModel, Field, TypeAdapter, field_validator + +############################################################################### +# Plugin endpoint constants +D3_PLUGIN_ENDPOINT = "api/session/python/execute" +D3_PLUGIN_MODULE_REG_ENDPOINT = "api/session/python/registermodule" + + +############################################################################### +# Plugin response types +class PluginStatusDetail(BaseModel): + """Detail information for plugin status errors.""" + + type_url: str = Field(description="Type URL identifying the error detail.") + value: str = Field(description="Error detail value.") + + +class PluginStatus(BaseModel): + """Status information from plugin API calls.""" + + code: int = Field(description="Return code of plugin API call. 0 if good.") + message: str = Field(description="Message from plugin API call.") + details: list[PluginStatusDetail] = Field( + description="List of detailed error information." + ) + + +# RetType is the return type of the function +RetType = typing_extensions.TypeVar("RetType", default=Any) +RetCastType = TypeVar("RetCastType") + + +class PluginResponse(BaseModel, Generic[RetType]): + """Response from a plugin API call.""" + + status: PluginStatus = Field(description="Status of plugin API call.") + d3Log: str | None = Field( + default=None, + description="The D3 log field captures the console log for the time the Python script is executing, recording any output generated by running a Python command, including the time taken to execute the Python command. As this captures the Designer console output, it is possible other threads may write to this output during this period causing additional irrelevant output.", + ) + pythonLog: str | None = Field( + default=None, + description="This output field is the pure Python output, recording any print statements or warnings occurring during the execution of the script. This output will also appear in the d3Log field, however, it may be mixed with other application output.", + ) + returnValue: RetType = Field(description="Return value of python plugin execution") + + @field_validator("returnValue", mode="before") + @classmethod + def parse_returnValue(cls, v): + if isinstance(v, str): + if v == "null": + return None + else: + return json.loads(v) + return v + + def returnCastValue(self, castType: type[RetCastType]) -> RetCastType: + """Validate and cast the return value to the specified type. + + Args: + castType: The type to validate and cast the return value to. + + Returns: + The validated return value as the specified type. + + Raises: + ValidationError: If the return value cannot be validated as the specified type. + """ + adapter = TypeAdapter(castType) + return adapter.validate_python(self.returnValue) + + +class PluginError(PluginResponse[None]): + """Error response when plugin execution fails.""" + + returnValue: None = Field( + default=None, description="Always None for error responses." + ) + + +class PluginRegisterResponse(BaseModel): + """Response from a plugin module registration API call.""" + + status: PluginStatus = Field( + description="Status of plugin module registration API call." + ) + + +@dataclass +class PluginException(Exception): + """ + Exception raised when plugin execution fails. + + Attributes: + status: The status information from the failed plugin call + d3Log: D3 Designer console log output + pythonLog: Python-specific log output + """ + + status: PluginStatus + d3Log: str | None = None + pythonLog: str | None = None + + _traceback_str: str | None = None + _str: str | None = None + + def __post_init__(self): + # Capture current stack trace if not already provided + if self._traceback_str is None: + self._traceback_str = "".join(traceback.format_stack()[:-1]) + + def __str__(self) -> str: + if self._str is None: + details_str = "" + if self.status.details: + details_list = "\n".join( + [f" - {d.type_url}: {d.value}" for d in self.status.details] + ) + details_str = f"\ndetails :\n{details_list}" + self._str = "\n".join( + [ + Exception.__str__(self), + "D3PluginError:", + f"- code : {self.status.code}", + f"- messages :\n{self.status.message}{details_str}", + f"- d3Log : {self.d3Log}", + f"- pythonLog : {self.pythonLog}", + f"- Traceback :\n{self._traceback_str.strip() if self._traceback_str else ''}", + ] + ) + return self._str + + +############################################################################### +# Payload types +class PluginPayload(BaseModel, Generic[RetType]): + """Type-safe execution payload for plugin calls.""" + + moduleName: str | None = Field( + default=None, + exclude_if=lambda v: v is None, + description="Module name to run script on Designer.", + ) + script: str = Field(description="Script to run on Designer.") + + def is_module_payload(self) -> bool: + return bool(self.moduleName) + + +class RegisterPayload(BaseModel): + """Payload for registering a Python module with Designer.""" + + moduleName: str = Field(description="Module name to register contents.") + contents: str = Field(description="Python code to register with the module name.") diff --git a/test/test_ast_utils.py b/test/test_ast_utils.py new file mode 100644 index 0000000..6fb78f0 --- /dev/null +++ b/test/test_ast_utils.py @@ -0,0 +1,1023 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +import ast +import inspect +import textwrap +import types + +import pytest + +from designer_plugin.d3sdk.ast_utils import ( + ConvertToPython27, + convert_class_to_py27, + convert_function_to_py27, + filter_base_classes, + filter_init_args, + find_packages_in_current_file, + get_class_node, + get_source, +) + + +class TestConvertToPython27Transformer: + """Tests for the ConvertToPython27 AST transformer.""" + + def test_remove_return_type_annotation(self): + """Test that return type annotations are removed from functions.""" + source = textwrap.dedent(""" + def my_function() -> int: + return 42 + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + # Find the function node + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assert func.returns is None + + def test_remove_argument_annotations(self): + """Test that argument type annotations are removed.""" + source = textwrap.dedent(""" + def my_function(x: int, y: str, z: list[int]) -> None: + pass + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + # Check all arguments have no annotations + for arg in func.args.args: + assert arg.annotation is None + + def test_convert_async_to_sync_function(self): + """Test that async functions are converted to regular functions.""" + source = textwrap.dedent(""" + async def async_function() -> int: + return 42 + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + # Should be converted to FunctionDef, not AsyncFunctionDef + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assert not isinstance(func, ast.AsyncFunctionDef) + assert func.name == "async_function" + assert func.returns is None # Return annotation should be removed too + + def test_remove_await_expressions(self): + """Test that await expressions are converted to normal calls.""" + source = textwrap.dedent(""" + async def my_function(): + result = await some_async_call() + return result + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + # Get the assignment statement: result = await some_async_call() + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + # The value should now be a Call, not an Await + assert isinstance(assign_stmt.value, ast.Call) + assert not isinstance(assign_stmt.value, ast.Await) + + def test_convert_annotated_assignment(self): + """Test that annotated assignments are converted to regular assignments.""" + source = textwrap.dedent(""" + def my_function(): + x: int = 5 + y: str = "hello" + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + # Both statements should be regular Assign nodes, not AnnAssign + for stmt in func.body: + assert isinstance(stmt, ast.Assign) + assert not isinstance(stmt, ast.AnnAssign) + + def test_annotated_assignment_without_value(self): + """Test that annotated assignments without values are removed.""" + source = textwrap.dedent(""" + def my_function(): + x: int + y = 10 + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + # Only one statement should remain (y = 10) + assert len(func.body) == 1 + assert isinstance(func.body[0], ast.Assign) + + def test_nested_async_functions(self): + """Test that nested async functions are also converted.""" + source = textwrap.dedent(""" + async def outer(): + async def inner() -> str: + return await some_call() + return await inner() + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + outer_func = transformed.body[0] + # Outer should be regular function + assert isinstance(outer_func, ast.FunctionDef) + + # Inner should also be regular function + inner_func = outer_func.body[0] + assert isinstance(inner_func, ast.FunctionDef) + assert inner_func.returns is None + + def test_complex_type_annotations(self): + """Test removal of complex type annotations.""" + source = textwrap.dedent(""" + def my_function( + items: list[str], + mapping: dict[str, int], + *args: int, + **kwargs: str + ) -> tuple[int, str]: + result: dict[str, int] = {} + return (0, "") + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + # Return type removed + assert func.returns is None + + # Regular args annotations removed + for arg in func.args.args: + assert arg.annotation is None + + # *args annotation removed + if func.args.vararg: + assert func.args.vararg.annotation is None + + # **kwargs annotation removed + if func.args.kwarg: + assert func.args.kwarg.annotation is None + + # Annotated assignment converted + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + def test_convert_basic_fstring(self): + """Test that basic f-strings are converted to .format() style.""" + source = textwrap.dedent(""" + def my_function(name): + message = f"Hello {name}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assign_stmt = func.body[0] + + # The value should be a Call node (str.format()) + assert isinstance(assign_stmt.value, ast.Call) + + # It should be calling the 'format' attribute + assert isinstance(assign_stmt.value.func, ast.Attribute) + assert assign_stmt.value.func.attr == "format" + + # The base string should be "Hello {}" + assert isinstance(assign_stmt.value.func.value, ast.Constant) + assert assign_stmt.value.func.value.value == "Hello {}" + + # It should have one argument (name) + assert len(assign_stmt.value.args) == 1 + assert ast.unparse(assign_stmt.value.args[0]) == "name" + + # Verify the complete conversion: f"Hello {name}" -> "Hello {}".format(name) + assert ast.unparse(assign_stmt.value) == "'Hello {}'.format(name)" + + def test_convert_fstring_with_format_spec(self): + """Test that f-strings with format specifications are converted correctly.""" + source = textwrap.dedent(""" + def my_function(x): + message = f"Value: {x:.2f}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + # Should be a .format() call + assert isinstance(assign_stmt.value, ast.Call) + assert isinstance(assign_stmt.value.func, ast.Attribute) + assert assign_stmt.value.func.attr == "format" + + # The format string should preserve the format spec + assert isinstance(assign_stmt.value.func.value, ast.Constant) + assert assign_stmt.value.func.value.value == "Value: {:.2f}" + + # It should have one argument (x) + assert len(assign_stmt.value.args) == 1 + assert isinstance(assign_stmt.value.args[0], ast.Name) + assert assign_stmt.value.args[0].id == "x" + + # Verify the complete conversion: f"Value: {x:.2f}" -> "Value: {:.2f}".format(x) + assert ast.unparse(assign_stmt.value) == "'Value: {:.2f}'.format(x)" + + def test_convert_fstring_with_conversion_flag(self): + """Test that f-strings with conversion flags (!r, !s, !a) are converted correctly.""" + source = textwrap.dedent(""" + def my_function(obj): + message = f"Repr: {obj!r}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + # Should be a .format() call + assert isinstance(assign_stmt.value, ast.Call) + assert isinstance(assign_stmt.value.func, ast.Attribute) + + # The format string should preserve the conversion flag + assert isinstance(assign_stmt.value.func.value, ast.Constant) + assert assign_stmt.value.func.value.value == "Repr: {!r}" + + # It should have one argument (obj) + assert len(assign_stmt.value.args) == 1 + + # Verify the complete conversion: f"Repr: {obj!r}" -> "Repr: {!r}".format(obj) + assert ast.unparse(assign_stmt.value) == "'Repr: {!r}'.format(obj)" + + def test_convert_fstring_with_multiple_expressions(self): + """Test that f-strings with multiple expressions are converted correctly.""" + source = textwrap.dedent(""" + def my_function(name, age): + message = f"Name: {name}, Age: {age}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + # Should be a .format() call + assert isinstance(assign_stmt.value, ast.Call) + assert isinstance(assign_stmt.value.func, ast.Attribute) + + # The format string should have two placeholders + assert isinstance(assign_stmt.value.func.value, ast.Constant) + assert assign_stmt.value.func.value.value == "Name: {}, Age: {}" + + # It should have two arguments (name, age) + assert len(assign_stmt.value.args) == 2 + + # Verify the complete conversion: f"Name: {name}, Age: {age}" -> "Name: {}, Age: {}".format(name, age) + assert ast.unparse(assign_stmt.value) == "'Name: {}, Age: {}'.format(name, age)" + + def test_convert_fstring_with_literal_braces(self): + """Test that f-strings with literal braces (escaped) are converted correctly.""" + source = textwrap.dedent(""" + def my_function(value): + message = f"Dict: {{key: {value}}}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + # Should be a .format() call + assert isinstance(assign_stmt.value, ast.Call) + assert isinstance(assign_stmt.value.func, ast.Attribute) + + # The format string should preserve the escaped braces + assert isinstance(assign_stmt.value.func.value, ast.Constant) + assert assign_stmt.value.func.value.value == "Dict: {{key: {}}}" + + # It should have one argument (value) + assert len(assign_stmt.value.args) == 1 + + # Verify the complete conversion: f"Dict: {{key: {value}}}" -> "Dict: {{key: {}}}".format(value) + assert ast.unparse(assign_stmt.value) == "'Dict: {{key: {}}}'.format(value)" + + def test_convert_fstring_with_complex_expression(self): + """Test that f-strings with complex expressions are converted correctly.""" + source = textwrap.dedent(""" + def my_function(items): + message = f"Count: {len(items)}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + # Should be a .format() call + assert isinstance(assign_stmt.value, ast.Call) + assert isinstance(assign_stmt.value.func, ast.Attribute) + assert assign_stmt.value.func.attr == "format" + + # The format string should have one placeholder + assert isinstance(assign_stmt.value.func.value, ast.Constant) + assert assign_stmt.value.func.value.value == "Count: {}" + + # It should have one argument (len(items)) + assert len(assign_stmt.value.args) == 1 + call_arg = assign_stmt.value.args[0] + assert ast.unparse(call_arg) == "len(items)" + + # Verify the complete conversion: f"Count: {len(items)}" -> "Count: {}".format(len(items)) + assert ast.unparse(assign_stmt.value) == "'Count: {}'.format(len(items))" + + def test_convert_fstring_combined_features(self): + """Test f-string with combined conversion flag and format spec.""" + source = textwrap.dedent(""" + def my_function(x): + message = f"Value: {x!s:>10}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assign_stmt = func.body[0] + assert isinstance(assign_stmt, ast.Assign) + + # Should be a .format() call + assert isinstance(assign_stmt.value, ast.Call) + assert isinstance(assign_stmt.value.func, ast.Attribute) + + # The format string should preserve conversion flag at minimum + assert isinstance(assign_stmt.value.func.value, ast.Constant) + format_str = assign_stmt.value.func.value.value + assert isinstance(format_str, str) + assert "Value:" in format_str + assert "{!s" in format_str or "{" in format_str + + # It should have one argument (x) + assert len(assign_stmt.value.args) == 1 + + # Verify the complete conversion: f"Value: {x!s:>10}" -> "Value: {!s:>10}".format(x) + assert ast.unparse(assign_stmt.value) == "'Value: {!s:>10}'.format(x)" + + +class TestConvertFunctionToPy27: + """Tests for convert_function_to_py27 function.""" + + def test_simple_function_conversion(self): + """Test basic function conversion.""" + source = textwrap.dedent(""" + def my_function(x: int, y: str) -> bool: + result: str = f"{x} {y}" + return True + """) + + tree = ast.parse(source) + func_node = tree.body[0] + assert isinstance(func_node, ast.FunctionDef) + + convert_function_to_py27(func_node) + + # Verify conversions + assert func_node.returns is None + for arg in func_node.args.args: + assert arg.annotation is None + + # Check body was transformed + assign_stmt = func_node.body[0] + assert isinstance(assign_stmt, ast.Assign) + + def test_async_function_body_conversion(self): + """Test that async function bodies are converted.""" + source = textwrap.dedent(""" + async def my_function(): + result = await some_call() + data: int = 42 + return result + """) + + tree = ast.parse(source) + func_node = tree.body[0] + assert isinstance(func_node, ast.AsyncFunctionDef) + + # First convert async to regular (simulating what happens in real usage) + transformer = ConvertToPython27() + new_func = transformer.visit_AsyncFunctionDef(func_node) + + # The function should now be regular FunctionDef + assert isinstance(new_func, ast.FunctionDef) + + # Check await was removed + assign_stmt = new_func.body[0] + assert isinstance(assign_stmt, ast.Assign) + assert isinstance(assign_stmt.value, ast.Call) + + # Check annotated assignment was converted + assign_stmt2 = new_func.body[1] + assert isinstance(assign_stmt2, ast.Assign) + + +class TestConvertClassToPy27: + """Tests for convert_class_to_py27 function.""" + + def test_class_with_async_methods(self): + """Test that class with async methods is converted.""" + source = textwrap.dedent(""" + class MyClass: + async def async_method(self) -> int: + return await something() + + def regular_method(self, x: int) -> str: + return str(x) + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + convert_class_to_py27(class_node) + + # All methods should be regular FunctionDef + for node in class_node.body: + if isinstance(node, ast.FunctionDef): + assert not isinstance(node, ast.AsyncFunctionDef) + # Return annotations should be removed + assert node.returns is None + # Argument annotations should be removed + for arg in node.args.args: + assert arg.annotation is None + + +class TestAsyncFunctionDefInvestigation: + """Investigation tests for visit_AsyncFunctionDef behavior.""" + + def test_async_def_converted_to_def(self): + """Verify that AsyncFunctionDef is replaced with FunctionDef in the tree.""" + source = textwrap.dedent(""" + async def my_async_func(x: int) -> str: + result = await call_something(x) + return result + """) + + tree = ast.parse(source) + original_func = tree.body[0] + assert isinstance(original_func, ast.AsyncFunctionDef) + + # Apply transformer + transformer = ConvertToPython27() + transformed_tree = transformer.visit(tree) + + # The node in the tree should now be FunctionDef + new_func = transformed_tree.body[0] + assert isinstance(new_func, ast.FunctionDef) + assert not isinstance(new_func, ast.AsyncFunctionDef) + + # Verify properties are preserved + assert new_func.name == "my_async_func" + assert len(new_func.args.args) == 1 + assert new_func.args.args[0].arg == "x" + + # Return type should be removed + assert new_func.returns is None + + # Argument annotation should be removed + assert new_func.args.args[0].annotation is None + + def test_async_with_nested_transformations(self): + """Test that async function with nested await and annotations works.""" + source = textwrap.dedent(""" + async def process_data(items: list[str]) -> dict[str, int]: + results: dict[str, int] = {} + for item in items: + value = await fetch_value(item) + results[item] = value + return results + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + + # Should be regular function + assert isinstance(func, ast.FunctionDef) + + # No return type + assert func.returns is None + + # No argument annotations + assert func.args.args[0].annotation is None + + # First statement should be regular assignment (not annotated) + first_stmt = func.body[0] + assert isinstance(first_stmt, ast.Assign) + assert not isinstance(first_stmt, ast.AnnAssign) + + # The await in the loop should be converted to regular call + for_loop = func.body[1] + assert isinstance(for_loop, ast.For) + assign_in_loop = for_loop.body[0] + assert isinstance(assign_in_loop, ast.Assign) + assert isinstance(assign_in_loop.value, ast.Call) + + def test_location_metadata_preserved(self): + """Test that source location metadata is preserved during conversion.""" + source = textwrap.dedent(""" + async def my_func(): + pass + """) + + tree = ast.parse(source) + original_func = tree.body[0] + original_lineno = original_func.lineno + original_col = original_func.col_offset + + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + new_func = transformed.body[0] + # Location should be preserved via ast.copy_location + assert new_func.lineno == original_lineno + assert new_func.col_offset == original_col + + +class TestGetSource: + """Tests for get_source function.""" + + def test_get_source_from_frame(self): + """Test that source code can be extracted from a frame.""" + # Get current frame + frame = inspect.currentframe() + assert frame is not None + + # Extract source + source = get_source(frame) + + # Verify source contains this test file's content + assert source is not None + assert isinstance(source, str) + assert "test_get_source_from_frame" in source + assert "def test_get_source_from_frame" in source + + def test_get_source_dedents_code(self): + """Test that extracted source code is dedented.""" + def nested_function(): + frame = inspect.currentframe() + return get_source(frame) if frame else None + + source = nested_function() + + # Verify the source is dedented (doesn't start with leading whitespace) + assert source is not None + # The source should contain the entire file, dedented + lines = source.split('\n') + # Find lines that should be at the start of the file + for line in lines: + if line.startswith('"""Tests for AST'): + # This line should be at column 0 after dedenting + assert line[0] != ' ' + break + + +class TestGetClassNode: + """Tests for get_class_node function.""" + + def test_find_class_by_name(self): + """Test finding a class definition by name.""" + source = textwrap.dedent(""" + class FirstClass: + pass + + class SecondClass: + pass + + class ThirdClass: + pass + """) + + tree = ast.parse(source) + class_node = get_class_node(tree, "SecondClass") + + assert class_node is not None + assert isinstance(class_node, ast.ClassDef) + assert class_node.name == "SecondClass" + + def test_class_not_found(self): + """Test that None is returned when class is not found.""" + source = textwrap.dedent(""" + class MyClass: + pass + """) + + tree = ast.parse(source) + class_node = get_class_node(tree, "NonExistentClass") + + assert class_node is None + + +class TestFilterBaseClasses: + """Tests for filter_base_classes function.""" + + def test_remove_all_base_classes(self): + """Test that all base classes are removed from a class definition.""" + source = textwrap.dedent(""" + class MyClass(BaseClass, AnotherBase): + pass + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + # Verify bases exist before filtering + assert len(class_node.bases) == 2 + + filter_base_classes(class_node) + + # Verify all bases are removed + assert len(class_node.bases) == 0 + + def test_empty_base_classes(self): + """Test that filtering works on classes with no base classes.""" + source = textwrap.dedent(""" + class MyClass: + pass + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + filter_base_classes(class_node) + + assert len(class_node.bases) == 0 + + +class TestFilterInitArgs: + """Tests for filter_init_args function.""" + + def test_class_with_init_returns_param_names(self): + """Test that __init__ parameters (excluding self) are returned.""" + source = textwrap.dedent(""" + class MyClass: + def __init__(self, a: int): + pass + def other_method(self): + pass + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + param_names = filter_init_args(class_node) + + assert param_names == ["a"] + + def test_class_without_init(self): + """Test that classes without __init__ return empty list.""" + source = textwrap.dedent(""" + class MyClass: + def other_method(self): + pass + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + param_names = filter_init_args(class_node) + + assert param_names == [] + + def test_init_with_multiple_params(self): + """Test that all parameters except self are returned.""" + source = textwrap.dedent(""" + class MyClass: + def __init__(self, a: int, b: str, c: list): + pass + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + param_names = filter_init_args(class_node) + + assert param_names == ["a", "b", "c"] + + def test_init_with_only_self(self): + """Test that __init__ with only self returns empty list.""" + source = textwrap.dedent(""" + class MyClass: + def __init__(self): + pass + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + param_names = filter_init_args(class_node) + + assert param_names == [] + + +class TestFindPackagesInCurrentFile: + """Tests for find_packages_in_current_file function.""" + + def test_finds_imports_from_current_file(self): + """Test that the function finds import statements from the calling file.""" + # This test file has imports at the top - they should be found + imports = find_packages_in_current_file() + + # Should find at least some of our imports + assert isinstance(imports, list) + assert len(imports) > 0 + + # Should be sorted + assert imports == sorted(imports) + + # Check for specific imports we know exist in this file + assert "import ast" in imports + assert "import pytest" in imports + assert "import textwrap" in imports + + def test_excludes_typing_imports(self): + """Test that typing module imports are excluded.""" + # Since this file doesn't import typing, we can't directly test exclusion here + # But we can verify the function doesn't crash and returns valid results + imports = find_packages_in_current_file() + + # Verify no typing imports are present + typing_imports = [imp for imp in imports if "typing" in imp] + assert len(typing_imports) == 0 + + def test_excludes_d3blobgen_imports(self): + """Test that d3blobgen package imports are excluded.""" + imports = find_packages_in_current_file() + + # Verify no d3blobgen imports are present + d3blobgen_imports = [imp for imp in imports if "d3blobgen" in imp] + assert len(d3blobgen_imports) == 0 + + def test_excludes_find_packages_function_itself(self): + """Test that the function itself is excluded from imports.""" + imports = find_packages_in_current_file() + + # Should not include import of find_packages_in_current_file itself + # even though we import it at the top of this file + function_imports = [imp for imp in imports if "find_packages_in_current_file" in imp] + assert len(function_imports) == 0 + + def test_returns_unique_sorted_imports(self): + """Test that returned imports are unique and sorted.""" + imports = find_packages_in_current_file() + + # Check uniqueness + assert len(imports) == len(set(imports)) + + # Check sorting + assert imports == sorted(imports) + + +class TestDecoratorHandling: + """Tests for handling decorators in AST transformations.""" + + def test_function_with_decorators(self): + """Test that function decorators are preserved during conversion.""" + source = textwrap.dedent(""" + @decorator1 + @decorator2 + def my_function(x: int) -> str: + return str(x) + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + + # Decorators should be preserved + assert len(func.decorator_list) == 2 + + # Type annotations should be removed + assert func.returns is None + assert func.args.args[0].annotation is None + + def test_async_function_with_decorators(self): + """Test that async function decorators are preserved during conversion.""" + source = textwrap.dedent(""" + @async_decorator + async def my_function(x: int) -> str: + return await something(x) + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + + # Should be converted to regular function + assert isinstance(func, ast.FunctionDef) + assert not isinstance(func, ast.AsyncFunctionDef) + + # Decorator should be preserved + assert len(func.decorator_list) == 1 + assert isinstance(func.decorator_list[0], ast.Name) + assert func.decorator_list[0].id == "async_decorator" + + def test_class_with_decorators(self): + """Test that class decorators are preserved.""" + source = textwrap.dedent(""" + @dataclass + class MyClass: + def method(self, x: int) -> str: + return str(x) + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + + # Verify decorator exists + assert len(class_node.decorator_list) == 1 + + # Convert the class + convert_class_to_py27(class_node) + + # Decorator should still be there + assert len(class_node.decorator_list) == 1 + + # Method should be converted + method = class_node.body[0] + assert isinstance(method, ast.FunctionDef) + assert method.returns is None + + +class TestEdgeCases: + """Tests for edge cases and error handling.""" + + def test_empty_function_body(self): + """Test conversion of function with only pass statement.""" + source = textwrap.dedent(""" + def empty_function(x: int) -> None: + pass + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assert func.returns is None + assert len(func.body) == 1 + assert isinstance(func.body[0], ast.Pass) + + def test_function_with_multiple_decorators_and_complex_types(self): + """Test complex scenario with multiple decorators and type hints.""" + source = textwrap.dedent(""" + @decorator1 + @decorator2(arg="value") + def complex_function( + a: int, + b: str, + *args: int, + **kwargs: str + ) -> dict[str, int]: + result: dict[str, int] = {} + return result + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + + # All decorators preserved + assert len(func.decorator_list) == 2 + + # All type hints removed + assert func.returns is None + for arg in func.args.args: + assert arg.annotation is None + if func.args.vararg: + assert func.args.vararg.annotation is None + if func.args.kwarg: + assert func.args.kwarg.annotation is None + + def test_nested_classes_and_functions(self): + """Test conversion of nested class and function definitions.""" + source = textwrap.dedent(""" + class OuterClass: + def outer_method(self) -> None: + class InnerClass: + def inner_method(self, x: int) -> str: + return str(x) + """) + + tree = ast.parse(source) + class_node = tree.body[0] + assert isinstance(class_node, ast.ClassDef) + convert_class_to_py27(class_node) + + # Outer method should be converted + outer_method = class_node.body[0] + assert isinstance(outer_method, ast.FunctionDef) + assert outer_method.returns is None + + # Inner class should exist + inner_class = outer_method.body[0] + assert isinstance(inner_class, ast.ClassDef) + + def test_fstring_unsupported_part_raises_error(self): + """Test that unsupported JoinedStr parts raise NotImplementedError.""" + # This is a hypothetical test - in practice, it's hard to create + # an unsupported JoinedStr part that the parser would accept. + # The transformer handles Constant and FormattedValue nodes. + # For now, we verify the basic f-string cases work correctly. + source = textwrap.dedent(""" + def my_function(x, y): + # Standard f-string cases + msg1 = f"Value: {x}" + msg2 = f"Values: {x} and {y}" + return msg1 + msg2 + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + # Should not raise NotImplementedError for standard cases + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + assert len(func.body) == 3 # Two assignments and one return + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/test/test_core.py b/test/test_core.py new file mode 100644 index 0000000..02d2603 --- /dev/null +++ b/test/test_core.py @@ -0,0 +1,292 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + +from designer_plugin.d3sdk.function import ( + D3Function, + D3PythonScript, + FunctionInfo, + d3function, + d3pythonscript, + extract_function_info, + get_all_d3functions, + get_all_modules, + get_register_payload, +) + + +def example_function(): + """Example function for testing""" + return "hello world" + + +def example_function_with_args(name, value): + """Example function with arguments for testing""" + return f"Hello {name}, value is {value}" + + +def typed_function_with_args(name: str, value: int) -> str: + """Type-annotated function with arguments for testing""" + result: str = f"Hello {name}, value is {value}" + return result + + +def typed_function_complex(items: list[str], count: int = 5) -> dict[str, int]: + """Complex type-annotated function for testing""" + data: dict[str, int] = {} + for i, item in enumerate(items[:count]): + data[item] = i + return data + + +@d3function("test_module") +def decorated_example_function(): + """Decorated example function for testing""" + return "decorated hello world" + + +@d3function() # No module name +def standalone_function(x, y): + """Standalone function for testing""" + return x + y + + +@d3function("module_a") +def function_in_module_a(): + """Function in module A for testing""" + return "module_a_result" + + +@d3function("module_b") +def function_in_module_b(value): + """Function in module B for testing""" + return f"module_b_{value}" + + +class TestExtractFunctionInfo: + def test_extract_info_simple_function(self): + info = extract_function_info(example_function) + + assert info.name == "example_function" + assert "return 'hello world'" in info.body + assert info.args == [] + assert "def example_function():" in info.source_code + + def test_extract_info_function_with_args(self): + info = extract_function_info(example_function_with_args) + + assert info.name == "example_function_with_args" + assert "return f'Hello {name}, value is {value}'" in info.body + assert info.args == ["name", "value"] + assert "def example_function_with_args(name, value):" in info.source_code + + def test_extract_info_decorated_function(self): + # Test that decorators are removed from the blob + info = extract_function_info(decorated_example_function._function) + + assert info.name == "decorated_example_function" + assert "return 'decorated hello world'" in info.body + assert info.args == [] + # Should not contain the decorator in the blob + assert "@d3function" not in info.source_code + + def test_extract_info_typed_function(self): + # Test type annotation handling + info = extract_function_info(typed_function_with_args) + + assert info.name == "typed_function_with_args" + assert info.args == ["name", "value"] + + # Regular blob should contain type annotations + assert ": str" in info.source_code + assert "-> str" in info.source_code + assert "result: str" in info.source_code + + # Python 2.7 blob should NOT contain type annotations + assert ": str" not in info.source_code_py27 + assert "-> str" not in info.source_code_py27 + assert "result: str" not in info.source_code_py27 + # But should contain the variable assignment without type hint + assert "result =" in info.source_code_py27 + + def test_extract_info_complex_typed_function(self): + # Test complex type annotations + info = extract_function_info(typed_function_complex) + + assert info.name == "typed_function_complex" + assert info.args == ["items", "count"] + + # Regular blob should contain complex type annotations + assert "list[str]" in info.source_code + assert "dict[str, int]" in info.source_code + assert "-> dict[str, int]" in info.source_code + assert "data: dict[str, int]" in info.source_code + + # Python 2.7 blob should NOT contain type annotations + assert "list[str]" not in info.source_code_py27 + assert "dict[str, int]" not in info.source_code_py27 + assert "-> dict[str, int]" not in info.source_code_py27 + assert "data: dict[str, int]" not in info.source_code_py27 + # But should contain the variable assignment without type hint + assert "data =" in info.source_code_py27 + + +class TestD3Function: + def test_d3_function_creation(self): + d3_func = D3Function("test_module", example_function) + + assert d3_func.name == "example_function" + assert d3_func.module_name == "test_module" + + def test_d3_function_standalone(self): + d3_func = D3Function("", standalone_function._function) + + assert d3_func.name == "standalone_function" + assert d3_func.module_name == "" + + def test_d3_function_call(self): + # Test that the wrapped function can still be called + result = decorated_example_function() + assert result == "decorated hello world" + + result = standalone_function(5, 3) + assert result == 8 + + def test_get_execute_blob_module_function(self): + payload = decorated_example_function.payload() + + assert payload.moduleName == "test_module" + assert payload.script == "return decorated_example_function()" + + def test_get_execute_blob_standalone_function(self): + # standalone_function is still a D3Function (module function with empty module name) + # so it generates module-style execution scripts + payload = standalone_function.payload(10, 20) + + assert payload.moduleName == "" + assert "return standalone_function(10, 20)" in payload.script + + def test_get_module_register_blob(self): + payload = get_register_payload("test_module") + + assert payload is not None + assert payload.moduleName == "test_module" + assert "def decorated_example_function():" in payload.contents + + + +class TestFunctionInfo: + def test_function_info_creation(self): + info = FunctionInfo( + source_code="def test_func(x: int, y: int) -> int:\n return 42", + source_code_py27="def test_func(x, y):\n return 42", + name="test_func", + body="return 42", + body_py27="return 42", + args=["x", "y"], + ) + + assert info.name == "test_func" + assert info.body == "return 42" + assert info.body_py27 == "return 42" + assert info.args == ["x", "y"] + assert info.source_code == "def test_func(x: int, y: int) -> int:\n return 42" + assert info.source_code_py27 == "def test_func(x, y):\n return 42" + + def test_function_info_defaults(self): + info = FunctionInfo( + source_code="def test_func() -> int:\n return 42", + source_code_py27="def test_func():\n return 42", + name="test_func", + body="return 42", + body_py27="return 42", + ) + assert info.args == [] + + +class TestD3FunctionDecorator: + def test_decorator_with_module(self): + @d3function("my_module") + def test_func(): + return "test result" + + assert isinstance(test_func, D3Function) + assert test_func.module_name == "my_module" + assert test_func.name == "test_func" + + def test_decorator_without_module(self): + @d3function() + def test_func(): + return "test result" + + assert isinstance(test_func, D3Function) + assert test_func.module_name == "" + assert test_func.name == "test_func" + + +class TestRegistrationFunctions: + def test_get_all_modules(self): + modules = get_all_modules() + assert "test_module" in modules + + def test_get_all_d3functions(self): + functions = get_all_d3functions() + function_names = [name for _, name in functions] + assert "decorated_example_function" in function_names + assert "standalone_function" in function_names + + +class TestD3FunctionEquality: + def test_hash_and_equality(self): + func1 = D3Function("module1", example_function) + func2 = D3Function("module2", example_function) # Different module, same function + + # Should be equal and have same hash because they wrap the same function + assert func1 == func2 + assert hash(func1) == hash(func2) + + def test_inequality_different_functions(self): + func1 = D3Function("module1", example_function) + func2 = D3Function("module1", example_function_with_args) + + # Should not be equal because they wrap different functions + assert func1 != func2 + + + +class TestD3PythonScript: + def test_d3pythonscript_decorator(self): + @d3pythonscript + def test_func(a: int, b: int) -> int: + return a + b + + assert isinstance(test_func, D3PythonScript) + assert test_func.name == "test_func" + # Verify it can be called + assert test_func(3, 4) == 7 + + def test_d3pythonscript_payload(self): + @d3pythonscript + def test_func(a: int, b: int) -> int: + return a + b + + payload = test_func.payload(5, 3) + + # Script should contain variable assignments and function body + assert "a=5" in payload.script + assert "b=3" in payload.script + assert "return a + b" in payload.script + # Should not have moduleName for standalone scripts + assert not hasattr(payload, "moduleName") or payload.moduleName is None + + def test_d3pythonscript_with_kwargs(self): + @d3pythonscript + def test_func(a: int, b: int = 10) -> int: + return a + b + + payload = test_func.payload(5, b=15) + + assert "a=5" in payload.script + assert "b=15" in payload.script + assert "return a + b" in payload.script diff --git a/test/test_plugin.py b/test/test_plugin.py index 717f117..a244a52 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,3 +1,8 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd +""" + from json import JSONDecodeError from json import dumps as json_dumps from unittest import TestCase diff --git a/uv.lock b/uv.lock index 71e2c5c..e1ec7de 100644 --- a/uv.lock +++ b/uv.lock @@ -144,6 +144,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -153,6 +162,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -169,6 +251,7 @@ source = { editable = "." } dependencies = [ { name = "aiohttp" }, { name = "pydantic" }, + { name = "requests" }, { name = "zeroconf" }, ] @@ -184,6 +267,7 @@ dev = [ requires-dist = [ { name = "aiohttp", specifier = ">=3.13.2" }, { name = "pydantic", specifier = ">=2.12.4" }, + { name = "requests", specifier = ">=2.32.5" }, { name = "zeroconf", specifier = ">=0.39.0" }, ] @@ -870,6 +954,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + [[package]] name = "ruff" version = "0.14.5" @@ -917,6 +1016,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + [[package]] name = "virtualenv" version = "20.35.4" From a0b838af5c8c85a2b859b7cd1874bbc7a0360fb7 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sun, 23 Nov 2025 11:07:10 +0000 Subject: [PATCH 03/37] Update README --- README.md | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 956c037..292cf27 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ -# python-plugin +# designer-plugin -A small Python library which publishes the DNS-SD service for a plugin. +A Python library for creating and managing plugins for Disguise Designer. This library provides: +- DNS-SD service publishing for plugin discovery +- Remote Python execution on Designer instances +- Multiple execution patterns (Client SDK, Function SDK) ## Installation @@ -10,7 +13,9 @@ To install the plugin, use pip: pip install git+https://github.com/disguise-one/python-plugin ``` -## Publish Plugin +
+ +# Publish Plugin The `DesignerPlugin` class allows you to publish a plugin for the Disguise Designer application. The `port` parameter corresponds to an HTTP server that serves the plugin's web user interface. Below is an example of how to use it (without a server, for clarity). @@ -55,7 +60,155 @@ asyncio.run(main()) If you would prefer not to use the `d3plugin.json` file, construct the `DesignerPlugin` object directly. The plugin's name and port number are required parameters. Optionally, the plugin can specify `hostname`, which can be used to direct Designer to a specific hostname when opening the plugin's web UI, and other metadata parameters are available, also. -## License +
+ +# Execute Python + +Python scripts can be executed remotely on Designer via the plugin system. + +Direct interaction with the plugin API endpoint requires extensive boilerplate code and JSON parsing. However, the Client SDK and Function SDK simplify this process by providing an RPC (Remote Procedure Call) interface that abstracts away the underlying HTTP communication and payload management. + +> **Important:** The Designer plugin API only supports Python 2.7, not Python 3. Both the Client SDK and Function SDK attempt to automatically convert your Python 3 code to Python 2.7 (f-strings and type hints are supported). However, some Python 3 features may not be fully compatible and conversion may fail in certain cases. + +## Client SDK + +The Client SDK allows you to define a class with methods that execute remotely on Designer by simply inheriting from `D3PluginClient`. The Client SDK supports both async and sync methods. + +**Example** + +```python +from designer_plugin.d3sdk import D3PluginClient +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from designer_plugin.d3sdk.script.d3 import * + +# 1. Async example ---------------------------------- +# Define your class by inheriting from D3PluginClient +class MyAsyncPlugin(D3PluginClient): + + async def my_time(self) -> str: + # Builtin imports must be done within methods for remote execution + import datetime + return str(datetime.datetime.now()) + + async def get_surface_uid_with_time( + self, + surface_name: str + ) -> dict[str, str]: + surface: Screen2 = resourceManager.load( + Path('objects/screen2/{}.apx'.format(surface_name)), + Screen2) + return { + "name": surface.description, + "uid": str(surface.uid), + "time": await self.my_time() # Supports method chaining + } + +# Instantiate your plugin +my_async_plugin = MyAsyncPlugin() +# Start async session with Designer instance +async with my_async_plugin.async_session('localhost', 80): + # Methods execute remotely on Designer and return values + surface_info = await my_async_plugin.get_surface_uid_with_time("surface 1") + + +# 2. Sync example ----------------------------------- +class MySyncPlugin(D3PluginClient): + def get_uid(self, surface_name: str) -> str: + surface: Screen2 = resourceManager.load( + Path('objects/screen2/{}.apx'.format(surface_name)), + Screen2) + return str(surface.uid) + +my_sync_plugin = MySyncPlugin() +with my_sync_plugin.session('localhost', 80): + uid = my_sync_plugin.get_uid("surface 1") +``` + +## Function SDK + +The Function SDK provides finer control over remote execution compared to the Client SDK. While the Client SDK automatically manages the entire execution lifecycle (registration and execution are transparent), the Function SDK gives you explicit control over: + +- **Payload generation**: Decorators add a `payload()` method to generate execution payloads +- **Session management**: You manually create sessions and control when to register modules +- **Function grouping**: Group related functions into modules for efficient reuse +- **Response handling**: Choose between `session.plugin()` for full response (status, logs, return value) or `session.rpc()` for just the return value + +The Function SDK offers two decorators: `@d3pythonscript` and `@d3function`: +- **`@d3pythonscript`**: + - Does not require registration. + - Best for simple scripts executed once or infrequently. +- **`@d3function`**: + - Must be registered on Designer before execution. + - Functions decorated with the same `module_name` are grouped together and can call each other, enabling function chaining and code reuse. + - Both `D3AsyncSession` and `D3Session` handle registration when you specify module names in the context manager. + +### Session API Methods + +Both `D3AsyncSession` and `D3Session` provide two methods for executing functions: + +- **`session.rpc(payload)`** - Returns only the return value from the function execution. Simpler for most use cases. +- **`session.plugin(payload)`** - Returns a `PluginResponse` object containing: + - `returnValue`: The function's return value + - `status`: Execution status (code, message, details) + - `d3Log`: Designer console output during execution + - `pythonLog`: Python-specific output (print statements, warnings) + +**Example** + +```python +from designer_plugin.d3sdk import d3pythonscript, d3function, D3AsyncSession +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from designer_plugin.d3sdk.script.d3 import * + +# 1. @d3pythonscript - simple one-off execution +@d3pythonscript +def rename_surface(surface_name: str, new_name: str): + surface: Screen2 = resourceManager.load( + Path('objects/screen2/{}.apx'.format(surface_name)), + Screen2) + surface.rename(surface.path.replaceFilename(new_name)) + +# 2. @d3function - reusable module-based functions +@d3function("mymodule") +def rename_surface_get_time(surface_name: str, new_name: str) -> str: + surface: Screen2 = resourceManager.load( + Path('objects/screen2/{}.apx'.format(surface_name)), + Screen2) + surface.rename(surface.path.replaceFilename(new_name)) + return my_time() # Call other functions in the same module + +@d3function("mymodule") +def my_time() -> str: + import datetime + return str(datetime.datetime.now()) + +# Usage with async session +async with D3AsyncSession('localhost', 80, ["mymodule"]) as session: + # d3pythonscript: no registration needed + await session.rpc(rename_surface.payload("surface 1", "surface 2")) + + # d3function: registered automatically via context manager + time: str = await session.rpc( + rename_surface_get_time.payload("surface 1", "surface 2")) + + # Use plugin() for full response with logs and status + from designer_plugin.d3sdk import PluginResponse + response: PluginResponse = await session.plugin( + rename_surface_get_time.payload("surface 1", "surface 2")) + print(f"Status: {response.status.code}") + print(f"Return value: {response.returnValue}") + +# Sync usage +from designer_plugin.d3sdk import D3Session +with D3Session('localhost', 80, ["mymodule"]) as session: + session.rpc(rename_surface.payload("surface 1", "surface 2")) +``` + +
+ +# License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. From a3107003bf1df737f23c0ab1650e09144bd62e34 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Mon, 24 Nov 2025 22:27:05 +0000 Subject: [PATCH 04/37] fix fstring conversion --- src/designer_plugin/d3sdk/ast_utils.py | 2 +- src/designer_plugin/d3sdk/function.py | 5 +- test/test_ast_utils.py | 108 +++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py index 7aaa5a5..9e0618d 100644 --- a/src/designer_plugin/d3sdk/ast_utils.py +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -171,7 +171,7 @@ def visit_AnnAssign(self, node: ast.AnnAssign): return ast.Assign( targets=[node.target], - value=node.value, + value=self.visit(node.value), # Recursively transform the value lineno=node.lineno, col_offset=node.col_offset, ) diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index 36ca5f1..260adce 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -105,8 +105,9 @@ def extract_function_info(func: Callable[..., Any]) -> FunctionInfo: first_node_py27 = convert_function_to_py27(first_node) source_code_py27: str = ast.unparse(first_node_py27) + body_nodes_py27: list[ast.stmt] = first_node_py27.body body_py27: str = "" - for stmt in body_nodes: + for stmt in body_nodes_py27: body_py27 += ast.unparse(stmt) + "\n" return FunctionInfo( @@ -114,7 +115,7 @@ def extract_function_info(func: Callable[..., Any]) -> FunctionInfo: source_code_py27=source_code_py27, name=function_name, body=body.strip(), - body_py27=body_py27, + body_py27=body_py27.strip(), args=args, ) diff --git a/test/test_ast_utils.py b/test/test_ast_utils.py index 6fb78f0..599edf9 100644 --- a/test/test_ast_utils.py +++ b/test/test_ast_utils.py @@ -350,6 +350,114 @@ def my_function(value): # Verify the complete conversion: f"Dict: {{key: {value}}}" -> "Dict: {{key: {}}}".format(value) assert ast.unparse(assign_stmt.value) == "'Dict: {{key: {}}}'.format(value)" + def test_convert_fstring_with_literal_braces_and_intermediate_variable(self): + """Test f-string with literal braces using an intermediate variable with type annotations.""" + source = textwrap.dedent(""" + def my_function(value: str) -> str: + my_message: str = value + message = f"Dict: {{key: {my_message}}}" + return message + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + + # Type annotations should be removed + assert func.returns is None + assert func.args.args[0].annotation is None + + # First assignment: my_message = value (type annotation removed) + first_assign = func.body[0] + assert isinstance(first_assign, ast.Assign) + assert not isinstance(first_assign, ast.AnnAssign) + assert ast.unparse(first_assign.targets[0]) == "my_message" + assert ast.unparse(first_assign.value) == "value" + + # Second assignment: message = f"Dict: {{key: {my_message}}}" converted to .format() + second_assign = func.body[1] + assert isinstance(second_assign, ast.Assign) + + # Should be a .format() call + assert isinstance(second_assign.value, ast.Call) + assert isinstance(second_assign.value.func, ast.Attribute) + assert second_assign.value.func.attr == "format" + + # The format string should preserve the escaped braces + assert isinstance(second_assign.value.func.value, ast.Constant) + assert second_assign.value.func.value.value == "Dict: {{key: {}}}" + + # It should have one argument (my_message) + assert len(second_assign.value.args) == 1 + assert ast.unparse(second_assign.value.args[0]) == "my_message" + + # Verify the complete conversion + assert ast.unparse(second_assign.value) == "'Dict: {{key: {}}}'.format(my_message)" + + def test_convert_fstring_in_annotated_assignment_with_nested_call(self): + """Test f-string nested in function call within annotated assignment. + + This is a regression test for a bug where f-strings inside annotated assignments + were not being converted because visit_AnnAssign was not recursively visiting + the value expression. + """ + source = textwrap.dedent(""" + def simple_script(surface_name: str) -> dict[str, str]: + surface: Screen2 = resourceManager.load( + Path(f"objects/screen2/{surface_name}.apx"), + Screen2 + ) + return {"name": surface.description} + """) + + tree = ast.parse(source) + transformer = ConvertToPython27() + transformed = transformer.visit(tree) + + func = transformed.body[0] + assert isinstance(func, ast.FunctionDef) + + # Type annotations should be removed + assert func.returns is None + assert func.args.args[0].annotation is None + + # First statement: annotated assignment converted to regular assignment + first_assign = func.body[0] + assert isinstance(first_assign, ast.Assign) + assert not isinstance(first_assign, ast.AnnAssign) + + # The f-string inside the Path() call should be converted to .format() + # The structure is: surface = resourceManager.load(Path(...), Screen2) + # We need to check the first argument of resourceManager.load() which is Path(...) + load_call = first_assign.value + assert isinstance(load_call, ast.Call) + + # First argument is Path(...) + path_call = load_call.args[0] + assert isinstance(path_call, ast.Call) + + # The argument to Path() should be a .format() call, not an f-string + format_call = path_call.args[0] + assert isinstance(format_call, ast.Call) + assert isinstance(format_call.func, ast.Attribute) + assert format_call.func.attr == "format" + + # Verify the format string + assert isinstance(format_call.func.value, ast.Constant) + assert format_call.func.value.value == "objects/screen2/{}.apx" + + # Verify the format argument is surface_name + assert len(format_call.args) == 1 + assert ast.unparse(format_call.args[0]) == "surface_name" + + # Verify the complete unparsed output doesn't contain f-strings + unparsed = ast.unparse(first_assign) + assert "f'" not in unparsed and 'f"' not in unparsed + assert ".format(" in unparsed + def test_convert_fstring_with_complex_expression(self): """Test that f-strings with complex expressions are converted correctly.""" source = textwrap.dedent(""" From 53683ba6ce6d0cfccfe76637a7ebced5db19d7a2 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Thu, 27 Nov 2025 09:48:18 +0000 Subject: [PATCH 05/37] add register_module arg --- src/designer_plugin/d3sdk/client.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 17c01d4..0bc2455 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -328,13 +328,15 @@ def in_session(self): @asynccontextmanager async def async_session( - self, hostname: str, port: int, module_name: str | None = None + self, hostname: str, port: int, register_module: bool = True, module_name: str | None = None ): """Async context manager for plugin session with Designer. Args: hostname: The hostname of the Designer instance. port: The port number of the Designer instance. + register_module: Whether to register the module. Set to False when the + module has already been registered with Designer. module_name: Optional module name to override the default. Yields: @@ -346,7 +348,8 @@ async def async_session( self.hostname = hostname self.port = port - await self._aregister(hostname, port) + if register_module: + await self._aregister(hostname, port) print("Entering D3PluginModule context") yield self finally: @@ -355,12 +358,14 @@ async def async_session( print("Exiting D3PluginModule context") @contextmanager - def session(self, hostname: str, port: int, module_name: str | None = None): + def session(self, hostname: str, port: int, register_module: bool = True, module_name: str | None = None): """Sync context manager for plugin session with Designer. Args: hostname: The hostname of the Designer instance. port: The port number of the Designer instance. + register_module: Whether to register the module. Set to False when the + module has already been registered with Designer. module_name: Optional module name to override the default. Yields: @@ -372,7 +377,8 @@ def session(self, hostname: str, port: int, module_name: str | None = None): self.hostname = hostname self.port = port - self._register(hostname, port) + if register_module: + self._register(hostname, port) print("Entering D3PluginModule context") yield self finally: From d46830668b842730588be3044453a7bf9b73baf7 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 16:30:45 +0000 Subject: [PATCH 06/37] add logger --- README.md | 19 ++++ src/designer_plugin/api.py | 104 +++++---------------- src/designer_plugin/d3sdk/client.py | 15 +-- src/designer_plugin/logger.py | 136 ++++++++++++++++++++++++++++ src/designer_plugin/models.py | 19 +++- 5 files changed, 204 insertions(+), 89 deletions(-) create mode 100644 src/designer_plugin/logger.py diff --git a/README.md b/README.md index 292cf27..d28fa7b 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,25 @@ with D3Session('localhost', 80, ["mymodule"]) as session:
+# Logging + +By default, `designer_plugin` logging is disabled. To enable it: + +```python +# Quick debug mode +from designer_plugin.logger import enable_debug_logging +enable_debug_logging() + +# Or configure via standard logging +import logging +logging.basicConfig(level=logging.INFO) +logging.getLogger('designer_plugin').setLevel(logging.DEBUG) +``` + +For production, use your application's logging configuration instead of `enable_debug_logging()`. + +
+ # License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/src/designer_plugin/api.py b/src/designer_plugin/api.py index ac7a6bc..40bb12f 100644 --- a/src/designer_plugin/api.py +++ b/src/designer_plugin/api.py @@ -3,6 +3,7 @@ Copyright (c) 2025 Disguise Technologies ltd """ +import logging from enum import StrEnum from typing import Any, Unpack @@ -22,6 +23,7 @@ RetType, ) +logger: logging.Logger = logging.getLogger(__name__) ############################################################################### # Plugin endpoint constants @@ -122,46 +124,6 @@ async def d3_api_arequest( ############################################################################### # API async interface -async def d3_api_aplugin_raw( - hostname: str, - port: int, - json: dict[str, str], - timeout_sec: float | None = None, -) -> PluginResponse: - """Execute a raw plugin script asynchronously on Designer. - - Args: - hostname: The hostname of the Designer instance. - port: The port number of the Designer instance. - json: Raw JSON payload containing script to execute. - timeout_sec: Optional timeout in seconds for the request. - - Returns: - PluginResponse containing the execution result. - - Raises: - PluginException: If the plugin execution fails. - """ - response: Any = await d3_api_arequest( - Method.POST, - hostname, - port, - D3_PLUGIN_ENDPOINT, - json=json, - timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, - ) - - try: - return PluginResponse.model_validate(response) - except ValidationError: - error_response: PluginError = PluginError.model_validate(response) - raise PluginException( - status=error_response.status, - d3Log=error_response.d3Log, - pythonLog=error_response.pythonLog, - ) from None - - async def d3_api_aplugin( hostname: str, port: int, @@ -182,6 +144,7 @@ async def d3_api_aplugin( Raises: PluginException: If the plugin execution fails. """ + logger.debug(f"Send plugin api:{payload.debug_string()}") response: Any = await d3_api_arequest( Method.POST, hostname, @@ -191,7 +154,13 @@ async def d3_api_aplugin( timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, ) try: - return PluginResponse[RetType].model_validate(response) + plugin_response: PluginResponse[RetType] = PluginResponse[RetType].model_validate(response) + + if plugin_response.pythonLog: + print(plugin_response.pythonLog) + logger.debug(f"PluginResponse:{plugin_response.debug_string()}") + + return plugin_response except ValidationError: error_response: PluginError = PluginError.model_validate(response) raise PluginException( @@ -220,6 +189,8 @@ async def d3_api_aregister_module( PluginException: If module registration fails on Designer side. """ try: + # logger.debug(f"Register module: {payload.moduleName}\n{payload.contents}") + logger.debug(f"Register module:{payload.debug_string()}") response: Any = await d3_api_arequest( Method.POST, hostname, @@ -245,46 +216,6 @@ async def d3_api_aregister_module( ############################################################################### # API sync interface -def d3_api_plugin_raw( - hostname: str, - port: int, - json: dict[str, str], - timeout_sec: float | None = None, -) -> PluginResponse: - """Execute a raw plugin script synchronously on Designer. - - Args: - hostname: The hostname of the Designer instance. - port: The port number of the Designer instance. - json: Raw JSON payload containing script to execute. - timeout_sec: Optional timeout in seconds for the request. - - Returns: - PluginResponse containing the execution result. - - Raises: - PluginException: If the plugin execution fails. - """ - response: Any = d3_api_request( - Method.POST, - hostname, - port, - D3_PLUGIN_ENDPOINT, - json=json, - timeout=timeout_sec if timeout_sec else None, - ) - - try: - return PluginResponse.model_validate(response) - except ValidationError: - error_response: PluginError = PluginError.model_validate(response) - raise PluginException( - status=error_response.status, - d3Log=error_response.d3Log, - pythonLog=error_response.pythonLog, - ) from None - - def d3_api_plugin( hostname: str, port: int, @@ -305,6 +236,8 @@ def d3_api_plugin( Raises: PluginException: If the plugin execution fails. """ + + logger.debug(f"Send plugin api:{payload.debug_string()}") response = d3_api_request( Method.POST, hostname, @@ -315,7 +248,13 @@ def d3_api_plugin( ) try: - return PluginResponse[RetType].model_validate(response) + plugin_response: PluginResponse[RetType] = PluginResponse[RetType].model_validate(response) + + if plugin_response.pythonLog: + print(plugin_response.pythonLog) + logger.debug(f"PluginResponse:{plugin_response.debug_string()}") + + return plugin_response except ValidationError: error_response: PluginError = PluginError.model_validate(response) raise PluginException( @@ -347,6 +286,7 @@ def d3_api_register_module( PluginException: If module registration fails on Designer side. """ try: + logger.debug(f"Register module:{payload.debug_string()}") response: Any = d3_api_request( Method.POST, hostname, diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 0bc2455..d08943f 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -6,6 +6,7 @@ import ast import functools import inspect +import logging import types from collections.abc import Callable from contextlib import asynccontextmanager, contextmanager @@ -30,6 +31,8 @@ RegisterPayload, ) +logger = logging.getLogger(__name__) + P = ParamSpec("P") T = TypeVar("T") @@ -350,12 +353,12 @@ async def async_session( self.port = port if register_module: await self._aregister(hostname, port) - print("Entering D3PluginModule context") + logger.debug("Entering D3PluginModule context") yield self finally: self.hostname = None self.port = None - print("Exiting D3PluginModule context") + logger.debug("Exiting D3PluginModule context") @contextmanager def session(self, hostname: str, port: int, register_module: bool = True, module_name: str | None = None): @@ -364,8 +367,8 @@ def session(self, hostname: str, port: int, register_module: bool = True, module Args: hostname: The hostname of the Designer instance. port: The port number of the Designer instance. - register_module: Whether to register the module. Set to False when the - module has already been registered with Designer. + register_module: Whether to register the module. + Set to False when the module has already been registered with Designer. module_name: Optional module name to override the default. Yields: @@ -379,12 +382,12 @@ def session(self, hostname: str, port: int, register_module: bool = True, module self.port = port if register_module: self._register(hostname, port) - print("Entering D3PluginModule context") + logger.debug("Entering D3PluginModule context") yield self finally: self.hostname = None self.port = None - print("Exiting D3PluginModule context") + logger.debug("Exiting D3PluginModule context") async def _aregister(self, hostname: str, port: int) -> None: """Register the plugin module with Designer asynchronously. diff --git a/src/designer_plugin/logger.py b/src/designer_plugin/logger.py new file mode 100644 index 0000000..00f2d83 --- /dev/null +++ b/src/designer_plugin/logger.py @@ -0,0 +1,136 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd + +Logging configuration for designer_plugin. + +By default, logging is disabled. +To see logs from this package, configure logging in your application: + + import logging + logging.basicConfig(level=logging.INFO) + +Or configure the designer_plugin logger specifically: + + logging.getLogger('designer_plugin').setLevel(logging.DEBUG) + +For quick debugging, you can use the convenience function: + + from designer_plugin.logger import enable_debug_logging + enable_debug_logging() + +Internal Usage (for library developers): + +Module-level loggers should be created using standard Python logging: + + import logging + logger = logging.getLogger(__name__) + +Advanced Usage - Granular Control: + +This library uses module-level loggers, allowing you to control logging +for specific submodules independently: + + # Enable DEBUG only for d3sdk submodule + logging.getLogger('designer_plugin.d3sdk').setLevel(logging.DEBUG) + + # Enable INFO for the main package + logging.getLogger('designer_plugin').setLevel(logging.INFO) + + # Enable DEBUG only for the API module + logging.getLogger('designer_plugin.api').setLevel(logging.DEBUG) + +Log messages will show their source module: + + 2025-11-29 10:15:23 [designer_plugin.api:INFO] API initialized + 2025-11-29 10:15:24 [designer_plugin.d3sdk.client:DEBUG] Connecting to server + 2025-11-29 10:15:25 [designer_plugin.models:INFO] Model loaded + +This module hierarchy allows you to troubleshoot specific components +without being overwhelmed by logs from the entire package. +""" + +import logging +import sys + +# Package root logger name +LOGGER_NAME = "designer_plugin" + +# Add NullHandler by default to prevent "No handler found" warnings +logging.getLogger(LOGGER_NAME).addHandler(logging.NullHandler()) + + +def enable_debug_logging( + level: int = logging.DEBUG, + stream: object | None = None, + format_string: str | None = None, +) -> None: + """ + Enable debug logging for designer_plugin. + + For the production environments, it is advised to configure logging through + the application's logging configuration: + - i.e. `logging.basicConfig()` or `dictConfig()` + + Note: This will remove any existing handlers on the designer_plugin logger + to avoid duplicate log messages. + + Args: + level: Logging level (default: `logging.DEBUG`) + stream: Output stream (default: `sys.stderr`) + format_string: Custom format string for log messages + (default: `'%(asctime)s [%(name)s:%(levelname)s] %(message)s'`) + + Example: + ```python + from designer_plugin.logger import enable_debug_logging + enable_debug_logging() + + # With custom level + enable_debug_logging(level=logging.INFO) + + # With custom format + enable_debug_logging(format_string='[%(levelname)s] %(message)s') + ``` + """ + logger = logging.getLogger(LOGGER_NAME) + + # Remove existing handlers to avoid duplicates + # Keep the NullHandler removal to prevent accumulation + logger.handlers.clear() + + # Create and configure handler + handler = logging.StreamHandler(stream or sys.stderr) + handler.setLevel(level) + + # Create and set formatter + fmt = format_string or '%(asctime)s [%(name)s:%(levelname)s] %(message)s' + formatter = logging.Formatter(fmt) + handler.setFormatter(formatter) + + # Configure logger + logger.addHandler(handler) + logger.setLevel(level) + + # Prevent propagation to avoid duplicate logs if root logger is also configured + logger.propagate = False + + +def disable_logging() -> None: + """ + Disable all logging from designer_plugin. + + This removes all handlers and adds back the NullHandler. + Useful for silencing logs during testing or in production. + + Example: + ```python + from designer_plugin.logger import disable_logging + disable_logging() + ``` + """ + logger = logging.getLogger(LOGGER_NAME) + logger.handlers.clear() + logger.addHandler(logging.NullHandler()) + logger.setLevel(logging.NOTSET) + logger.propagate = True diff --git a/src/designer_plugin/models.py b/src/designer_plugin/models.py index 82c7487..ebff601 100644 --- a/src/designer_plugin/models.py +++ b/src/designer_plugin/models.py @@ -80,6 +80,11 @@ def returnCastValue(self, castType: type[RetCastType]) -> RetCastType: adapter = TypeAdapter(castType) return adapter.validate_python(self.returnValue) + def debug_string(self) -> str: + return f""" +{'json ':{'='}<60} +{self.model_dump_json(indent=2)}""" + class PluginError(PluginResponse[None]): """Error response when plugin execution fails.""" @@ -136,7 +141,6 @@ def __str__(self) -> str: f"- messages :\n{self.status.message}{details_str}", f"- d3Log : {self.d3Log}", f"- pythonLog : {self.pythonLog}", - f"- Traceback :\n{self._traceback_str.strip() if self._traceback_str else ''}", ] ) return self._str @@ -157,9 +161,22 @@ class PluginPayload(BaseModel, Generic[RetType]): def is_module_payload(self) -> bool: return bool(self.moduleName) + def debug_string(self) -> str: + return f""" +{'json ':{'='}<60} +{self.model_dump_json(indent=2)} +{'script ':{'='}<60} +{self.script}""" class RegisterPayload(BaseModel): """Payload for registering a Python module with Designer.""" moduleName: str = Field(description="Module name to register contents.") contents: str = Field(description="Python code to register with the module name.") + + def debug_string(self) -> str: + return f""" +{'json ':{'='}<60} +{self.model_dump_json(indent=2)} +{'contents ':{'='}<60} +{self.contents}""" From e48bb90fe24c863e43235de58176be39f231a9c1 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 16:37:20 +0000 Subject: [PATCH 07/37] add D3_PLUGIN_DEFAULT_PORT --- src/designer_plugin/d3sdk/client.py | 31 ++++++++++++++-------------- src/designer_plugin/d3sdk/session.py | 5 +++-- src/designer_plugin/models.py | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index d08943f..be58c4c 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -26,6 +26,7 @@ get_source, ) from designer_plugin.models import ( + D3_PLUGIN_DEFAULT_PORT, PluginPayload, PluginResponse, RegisterPayload, @@ -86,7 +87,7 @@ def create_d3_plugin_method_wrapper(method_name: str, original_method: Callable[ async def async_wrapper(self, *args, **kwargs): payload = build_payload(self, method_name, args, kwargs) response: PluginResponse[T] = await d3_api_aplugin( - self.hostname, self.port, payload + self._hostname, self._port, payload ) return response.returnValue @@ -97,7 +98,7 @@ async def async_wrapper(self, *args, **kwargs): def sync_wrapper(self, *args, **kwargs): payload = build_payload(self, method_name, args, kwargs) response: PluginResponse[T] = d3_api_plugin( - self.hostname, self.port, payload + self._hostname, self._port, payload ) return response.returnValue @@ -318,8 +319,8 @@ def get_surface_uid(self, surface_name: str) -> dict[str, str]: """ def __init__(self): - self.hostname: str | None = None - self.port: int | None = None + self._hostname: str | None = None + self._port: int | None = None def in_session(self): """Check if the client is currently in an active session. @@ -327,11 +328,11 @@ def in_session(self): Returns: True if both hostname and port are set, False otherwise. """ - return self.hostname and self.port + return self._hostname and self._port @asynccontextmanager async def async_session( - self, hostname: str, port: int, register_module: bool = True, module_name: str | None = None + self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None ): """Async context manager for plugin session with Designer. @@ -349,19 +350,19 @@ async def async_session( if module_name: self.module_name = module_name - self.hostname = hostname - self.port = port + self._hostname = hostname + self._port = port if register_module: await self._aregister(hostname, port) logger.debug("Entering D3PluginModule context") yield self finally: - self.hostname = None - self.port = None + self._hostname = None + self._port = None logger.debug("Exiting D3PluginModule context") @contextmanager - def session(self, hostname: str, port: int, register_module: bool = True, module_name: str | None = None): + def session(self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None): """Sync context manager for plugin session with Designer. Args: @@ -378,15 +379,15 @@ def session(self, hostname: str, port: int, register_module: bool = True, module if module_name: self.module_name = module_name - self.hostname = hostname - self.port = port + self._hostname = hostname + self._port = port if register_module: self._register(hostname, port) logger.debug("Entering D3PluginModule context") yield self finally: - self.hostname = None - self.port = None + self._hostname = None + self._port = None logger.debug("Exiting D3PluginModule context") async def _aregister(self, hostname: str, port: int) -> None: diff --git a/src/designer_plugin/d3sdk/session.py b/src/designer_plugin/d3sdk/session.py index dbea2c8..f51d8f5 100644 --- a/src/designer_plugin/d3sdk/session.py +++ b/src/designer_plugin/d3sdk/session.py @@ -18,6 +18,7 @@ ) from designer_plugin.d3sdk.function import D3Function from designer_plugin.models import ( + D3_PLUGIN_DEFAULT_PORT, PluginPayload, PluginResponse, RegisterPayload, @@ -49,7 +50,7 @@ class D3Session(D3SessionBase): """ def __init__( - self, hostname: str, port: int, context_modules: list[str] | None = None + self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, context_modules: list[str] | None = None ) -> None: """Initialize synchronous Designer session. @@ -179,7 +180,7 @@ class D3AsyncSession(D3SessionBase): """ def __init__( - self, hostname: str, port: int, context_modules: list[str] | None = None + self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, context_modules: list[str] | None = None ) -> None: """Initialize asynchronous Designer session. diff --git a/src/designer_plugin/models.py b/src/designer_plugin/models.py index ebff601..c0062bf 100644 --- a/src/designer_plugin/models.py +++ b/src/designer_plugin/models.py @@ -15,7 +15,7 @@ # Plugin endpoint constants D3_PLUGIN_ENDPOINT = "api/session/python/execute" D3_PLUGIN_MODULE_REG_ENDPOINT = "api/session/python/registermodule" - +D3_PLUGIN_DEFAULT_PORT = 80 ############################################################################### # Plugin response types From ae97de5fb7a70af8beb2f26aa64a27f103c8c1c9 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 16:50:45 +0000 Subject: [PATCH 08/37] rename test directory to tests --- pyproject.toml | 4 ++-- {test => tests}/__init__.py | 0 {test => tests}/test_ast_utils.py | 0 {test => tests}/test_core.py | 0 {test => tests}/test_plugin.py | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename {test => tests}/__init__.py (100%) rename {test => tests}/test_ast_utils.py (100%) rename {test => tests}/test_core.py (100%) rename {test => tests}/test_plugin.py (100%) diff --git a/pyproject.toml b/pyproject.toml index 05b0704..47c4f64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dev = [ [tool.ruff] target-version = "py311" exclude = [ - "test", + "tests", ".venv", ".devcontainer", ] @@ -99,7 +99,7 @@ module = "zeroconf.*" ignore_missing_imports = true [tool.pytest.ini_options] -testpaths = ["test"] +testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/test/test_ast_utils.py b/tests/test_ast_utils.py similarity index 100% rename from test/test_ast_utils.py rename to tests/test_ast_utils.py diff --git a/test/test_core.py b/tests/test_core.py similarity index 100% rename from test/test_core.py rename to tests/test_core.py diff --git a/test/test_plugin.py b/tests/test_plugin.py similarity index 100% rename from test/test_plugin.py rename to tests/test_plugin.py From 3529688b608b8ed3eb282cdeeb65b73fc7f119bf Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 20:39:10 +0000 Subject: [PATCH 09/37] Update readme --- README.md | 73 ++++++++++++++++++---------- src/designer_plugin/api.py | 5 +- src/designer_plugin/d3sdk/client.py | 14 +++++- src/designer_plugin/d3sdk/session.py | 10 +++- src/designer_plugin/logger.py | 2 +- src/designer_plugin/models.py | 12 +++-- 6 files changed, 79 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index d28fa7b..9e8052c 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,23 @@ Direct interaction with the plugin API endpoint requires extensive boilerplate c > **Important:** The Designer plugin API only supports Python 2.7, not Python 3. Both the Client SDK and Function SDK attempt to automatically convert your Python 3 code to Python 2.7 (f-strings and type hints are supported). However, some Python 3 features may not be fully compatible and conversion may fail in certain cases. +## Stub file + +To enable IDE autocomplete and type checking for Designer's Python API, install the stub file package: + +```bash +pip install designer-plugin-pystub +``` + +Once installed, import the stubs using the `TYPE_CHECKING` pattern. This provides type hints in your IDE without affecting runtime execution: +```python +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from designer_plugin.pystub.d3 import * +``` + +This allows you to get autocomplete for Designer objects like `resourceManager`, `Screen2`, `Path`, etc., while writing your plugin code. + ## Client SDK The Client SDK allows you to define a class with methods that execute remotely on Designer by simply inheriting from `D3PluginClient`. The Client SDK supports both async and sync methods. @@ -80,9 +97,22 @@ The Client SDK allows you to define a class with methods that execute remotely o from designer_plugin.d3sdk import D3PluginClient from typing import TYPE_CHECKING if TYPE_CHECKING: - from designer_plugin.d3sdk.script.d3 import * + from designer_plugin.pystub.d3 import * -# 1. Async example ---------------------------------- +# 1. Sync example ----------------------------------- +class MySyncPlugin(D3PluginClient): + def get_uid(self, surface_name: str) -> str: + surface: Screen2 = resourceManager.load( + Path(f'objects/screen2/{surface_name}.apx'), + Screen2) + return str(surface.uid) + +my_sync_plugin = MySyncPlugin() +with my_sync_plugin.session('localhost', 80): + uid = my_sync_plugin.get_uid("surface 1") + + +# 2. Async example ---------------------------------- # Define your class by inheriting from D3PluginClient class MyAsyncPlugin(D3PluginClient): @@ -96,7 +126,7 @@ class MyAsyncPlugin(D3PluginClient): surface_name: str ) -> dict[str, str]: surface: Screen2 = resourceManager.load( - Path('objects/screen2/{}.apx'.format(surface_name)), + Path(f'objects/screen2/{surface_name}.apx'), Screen2) return { "name": surface.description, @@ -104,25 +134,18 @@ class MyAsyncPlugin(D3PluginClient): "time": await self.my_time() # Supports method chaining } -# Instantiate your plugin -my_async_plugin = MyAsyncPlugin() -# Start async session with Designer instance -async with my_async_plugin.async_session('localhost', 80): - # Methods execute remotely on Designer and return values - surface_info = await my_async_plugin.get_surface_uid_with_time("surface 1") - - -# 2. Sync example ----------------------------------- -class MySyncPlugin(D3PluginClient): - def get_uid(self, surface_name: str) -> str: - surface: Screen2 = resourceManager.load( - Path('objects/screen2/{}.apx'.format(surface_name)), - Screen2) - return str(surface.uid) +# Usage +async def main(): + # Instantiate your plugin + my_async_plugin = MyAsyncPlugin() + # Start async session with Designer + async with my_async_plugin.async_session('localhost', 80): + # Methods execute remotely on Designer and return values + surface_info = await my_async_plugin.get_surface_uid_with_time("surface 1") + print(surface_info) -my_sync_plugin = MySyncPlugin() -with my_sync_plugin.session('localhost', 80): - uid = my_sync_plugin.get_uid("surface 1") +import asyncio +asyncio.run(main()) ``` ## Function SDK @@ -141,7 +164,7 @@ The Function SDK offers two decorators: `@d3pythonscript` and `@d3function`: - **`@d3function`**: - Must be registered on Designer before execution. - Functions decorated with the same `module_name` are grouped together and can call each other, enabling function chaining and code reuse. - - Both `D3AsyncSession` and `D3Session` handle registration when you specify module names in the context manager. + - Registration is automatic when you pass module names to the session context manager (e.g., `D3AsyncSession('localhost', 80, ["mymodule"])`). If you don't provide module names, no registration occurs. ### Session API Methods @@ -160,13 +183,13 @@ Both `D3AsyncSession` and `D3Session` provide two methods for executing function from designer_plugin.d3sdk import d3pythonscript, d3function, D3AsyncSession from typing import TYPE_CHECKING if TYPE_CHECKING: - from designer_plugin.d3sdk.script.d3 import * + from designer_plugin.pystub.d3 import * # 1. @d3pythonscript - simple one-off execution @d3pythonscript def rename_surface(surface_name: str, new_name: str): surface: Screen2 = resourceManager.load( - Path('objects/screen2/{}.apx'.format(surface_name)), + Path(f'objects/screen2/{surface_name}.apx'), Screen2) surface.rename(surface.path.replaceFilename(new_name)) @@ -174,7 +197,7 @@ def rename_surface(surface_name: str, new_name: str): @d3function("mymodule") def rename_surface_get_time(surface_name: str, new_name: str) -> str: surface: Screen2 = resourceManager.load( - Path('objects/screen2/{}.apx'.format(surface_name)), + Path(f'objects/screen2/{surface_name}.apx'), Screen2) surface.rename(surface.path.replaceFilename(new_name)) return my_time() # Call other functions in the same module diff --git a/src/designer_plugin/api.py b/src/designer_plugin/api.py index 40bb12f..b2f3d90 100644 --- a/src/designer_plugin/api.py +++ b/src/designer_plugin/api.py @@ -25,6 +25,7 @@ logger: logging.Logger = logging.getLogger(__name__) + ############################################################################### # Plugin endpoint constants def get_plugin_endpoint_url(hostname: str, port: int) -> str: @@ -154,7 +155,7 @@ async def d3_api_aplugin( timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, ) try: - plugin_response: PluginResponse[RetType] = PluginResponse[RetType].model_validate(response) + plugin_response = PluginResponse[RetType].model_validate(response) if plugin_response.pythonLog: print(plugin_response.pythonLog) @@ -248,7 +249,7 @@ def d3_api_plugin( ) try: - plugin_response: PluginResponse[RetType] = PluginResponse[RetType].model_validate(response) + plugin_response = PluginResponse[RetType].model_validate(response) if plugin_response.pythonLog: print(plugin_response.pythonLog) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index be58c4c..f72025b 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -332,7 +332,11 @@ def in_session(self): @asynccontextmanager async def async_session( - self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None + self, + hostname: str, + port: int = D3_PLUGIN_DEFAULT_PORT, + register_module: bool = True, + module_name: str | None = None, ): """Async context manager for plugin session with Designer. @@ -362,7 +366,13 @@ async def async_session( logger.debug("Exiting D3PluginModule context") @contextmanager - def session(self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None): + def session( + self, + hostname: str, + port: int = D3_PLUGIN_DEFAULT_PORT, + register_module: bool = True, + module_name: str | None = None, + ): """Sync context manager for plugin session with Designer. Args: diff --git a/src/designer_plugin/d3sdk/session.py b/src/designer_plugin/d3sdk/session.py index f51d8f5..8536dbf 100644 --- a/src/designer_plugin/d3sdk/session.py +++ b/src/designer_plugin/d3sdk/session.py @@ -50,7 +50,10 @@ class D3Session(D3SessionBase): """ def __init__( - self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, context_modules: list[str] | None = None + self, + hostname: str, + port: int = D3_PLUGIN_DEFAULT_PORT, + context_modules: list[str] | None = None, ) -> None: """Initialize synchronous Designer session. @@ -180,7 +183,10 @@ class D3AsyncSession(D3SessionBase): """ def __init__( - self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, context_modules: list[str] | None = None + self, + hostname: str, + port: int = D3_PLUGIN_DEFAULT_PORT, + context_modules: list[str] | None = None, ) -> None: """Initialize asynchronous Designer session. diff --git a/src/designer_plugin/logger.py b/src/designer_plugin/logger.py index 00f2d83..e9e9cd0 100644 --- a/src/designer_plugin/logger.py +++ b/src/designer_plugin/logger.py @@ -104,7 +104,7 @@ def enable_debug_logging( handler.setLevel(level) # Create and set formatter - fmt = format_string or '%(asctime)s [%(name)s:%(levelname)s] %(message)s' + fmt = format_string or "%(asctime)s [%(name)s:%(levelname)s] %(message)s" formatter = logging.Formatter(fmt) handler.setFormatter(formatter) diff --git a/src/designer_plugin/models.py b/src/designer_plugin/models.py index c0062bf..d9337e9 100644 --- a/src/designer_plugin/models.py +++ b/src/designer_plugin/models.py @@ -17,6 +17,7 @@ D3_PLUGIN_MODULE_REG_ENDPOINT = "api/session/python/registermodule" D3_PLUGIN_DEFAULT_PORT = 80 + ############################################################################### # Plugin response types class PluginStatusDetail(BaseModel): @@ -82,7 +83,7 @@ def returnCastValue(self, castType: type[RetCastType]) -> RetCastType: def debug_string(self) -> str: return f""" -{'json ':{'='}<60} +{"json ":{'='}<60} {self.model_dump_json(indent=2)}""" @@ -163,11 +164,12 @@ def is_module_payload(self) -> bool: def debug_string(self) -> str: return f""" -{'json ':{'='}<60} +{"json ":{'='}<60} {self.model_dump_json(indent=2)} -{'script ':{'='}<60} +{"script ":{'='}<60} {self.script}""" + class RegisterPayload(BaseModel): """Payload for registering a Python module with Designer.""" @@ -176,7 +178,7 @@ class RegisterPayload(BaseModel): def debug_string(self) -> str: return f""" -{'json ':{'='}<60} +{"json ":{'='}<60} {self.model_dump_json(indent=2)} -{'contents ':{'='}<60} +{"contents ":{'='}<60} {self.contents}""" From d37cc92086bd5141275d7fec830c170b58d8d19a Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 21:05:03 +0000 Subject: [PATCH 10/37] fix mypy --- pyproject.toml | 1 + src/designer_plugin/api.py | 2 +- src/designer_plugin/d3sdk/ast_utils.py | 19 ++++++++--------- src/designer_plugin/d3sdk/client.py | 28 +++++++++++++------------- src/designer_plugin/d3sdk/function.py | 10 ++++----- src/designer_plugin/d3sdk/session.py | 6 +++--- src/designer_plugin/logger.py | 3 ++- src/designer_plugin/models.py | 4 ++-- uv.lock | 14 +++++++++++++ 9 files changed, 52 insertions(+), 35 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 47c4f64..8e95907 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "aiohttp>=3.13.2", "pydantic>=2.12.4", "requests>=2.32.5", + "types-requests>=2.32.4.20250913", "zeroconf>=0.39.0", ] requires-python = ">=3.11" diff --git a/src/designer_plugin/api.py b/src/designer_plugin/api.py index b2f3d90..23eefca 100644 --- a/src/designer_plugin/api.py +++ b/src/designer_plugin/api.py @@ -71,7 +71,7 @@ def d3_api_request( hostname: str, port: int, url_endpoint: str, - **kwargs, + **kwargs: Any, ) -> Any: """Make a synchronous HTTP request to Designer API. diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py index 9e0618d..d444ca3 100644 --- a/src/designer_plugin/d3sdk/ast_utils.py +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -7,6 +7,7 @@ import inspect import textwrap import types +from typing import Any ############################################################################### @@ -27,7 +28,7 @@ def get_source(frame: types.FrameType) -> str | None: return textwrap.dedent("".join(source_lines)) if source_lines else None -def get_class_node(tree, class_name: str) -> ast.ClassDef | None: +def get_class_node(tree: ast.Module, class_name: str) -> ast.ClassDef | None: """Find a class definition node by name in an AST. Args: @@ -45,7 +46,7 @@ def get_class_node(tree, class_name: str) -> ast.ClassDef | None: ############################################################################### # AST node filtering utilities -def filter_base_classes(class_node: ast.ClassDef): +def filter_base_classes(class_node: ast.ClassDef) -> None: """Remove all base classes from a class definition for Python 2.7 compatibility. This function modifies the class_node in-place by clearing its base class list. @@ -96,7 +97,7 @@ class ConvertToPython27(ast.NodeTransformer): - Converts f-strings to .format() style (f"Hello {name}" → "Hello {}".format(name)) """ - def visit_FunctionDef(self, node: ast.FunctionDef): + def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: """Remove return type annotation from function definitions. Transforms 'def func() -> int:' to 'def func():' for Python 2.7 compatibility. @@ -111,7 +112,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef): self.generic_visit(node) return node - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.FunctionDef: """Convert async function to regular function for Python 2.7 compatibility. Transforms 'async def func() -> int:' to 'def func():' by: @@ -141,7 +142,7 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): # Now run normal FunctionDef logic + recurse return self.visit_FunctionDef(new) - def visit_arg(self, node: ast.arg): + def visit_arg(self, node: ast.arg) -> ast.arg: """Remove type annotation from argument. Args: @@ -153,7 +154,7 @@ def visit_arg(self, node: ast.arg): node.annotation = None return node - def visit_AnnAssign(self, node: ast.AnnAssign): + def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.Assign | None: """Remove type hint. Converts type-annotated variable assignments (e.g., 'x: int = 5') into regular @@ -176,7 +177,7 @@ def visit_AnnAssign(self, node: ast.AnnAssign): col_offset=node.col_offset, ) - def visit_Await(self, node: ast.Await): + def visit_Await(self, node: ast.Await) -> Any: """Remove await keyword. Remove await keyword and return the underlying expression. @@ -190,7 +191,7 @@ def visit_Await(self, node: ast.Await): """ return self.visit(node.value) - def visit_JoinedStr(self, node: ast.JoinedStr): + def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.Call: # Don't use generic_visit here because we need to handle format_spec specially # Process the node values manually to preserve format specs @@ -272,7 +273,7 @@ def convert_function_to_py27( (modified) node. For AsyncFunctionDef input, returns a new FunctionDef node. """ transformer = ConvertToPython27() - return transformer.visit(function_node) + return transformer.visit(function_node) # type: ignore def convert_class_to_py27(class_node: ast.ClassDef) -> None: diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index f72025b..a16096e 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -38,7 +38,7 @@ T = TypeVar("T") -def build_payload(self, method_name: str, args, kwargs) -> PluginPayload[Any]: +def build_payload(self: Any, method_name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> PluginPayload[Any]: """Build plugin payload for remote method execution. Args: @@ -62,7 +62,7 @@ def build_payload(self, method_name: str, args, kwargs) -> PluginPayload[Any]: return PluginPayload[Any](moduleName=self.module_name, script=script) -def create_d3_plugin_method_wrapper(method_name: str, original_method: Callable[P, T]): +def create_d3_plugin_method_wrapper(method_name: str, original_method: Callable[P, T]) -> Callable[..., Any]: """Create a wrapper that executes a method remotely via Designer API calls. This wrapper intercepts method calls and instead of executing locally: @@ -84,7 +84,7 @@ def create_d3_plugin_method_wrapper(method_name: str, original_method: Callable[ if inspect.iscoroutinefunction(original_method): # Create async wrapper that uses async Designer API call @functools.wraps(original_method) - async def async_wrapper(self, *args, **kwargs): + async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: payload = build_payload(self, method_name, args, kwargs) response: PluginResponse[T] = await d3_api_aplugin( self._hostname, self._port, payload @@ -95,7 +95,7 @@ async def async_wrapper(self, *args, **kwargs): else: # Create sync wrapper that uses synchronous Designer API call @functools.wraps(original_method) - def sync_wrapper(self, *args, **kwargs): + def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: payload = build_payload(self, method_name, args, kwargs) response: PluginResponse[T] = d3_api_plugin( self._hostname, self._port, payload @@ -105,7 +105,7 @@ def sync_wrapper(self, *args, **kwargs): return sync_wrapper -def create_d3_payload_wrapper(method_name: str, original_method: Callable[P, T]): +def create_d3_payload_wrapper(method_name: str, original_method: Callable[P, T]) -> Callable[..., PluginPayload[T]]: """Create a wrapper that generates plugin payload without executing. Args: @@ -117,7 +117,7 @@ def create_d3_payload_wrapper(method_name: str, original_method: Callable[P, T]) """ @functools.wraps(original_method) - def sync_wrapper(self, *args, **kwargs) -> PluginPayload[T]: + def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> PluginPayload[T]: return build_payload(self, method_name, args, kwargs) return sync_wrapper @@ -165,7 +165,7 @@ class D3PluginClientMeta(type): instance_code_template: str instance_code: str - def __new__(cls, name, bases, attrs): + def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type: # Skip the base class if name == "D3PluginClient": return super().__new__(cls, name, bases, attrs) @@ -234,7 +234,7 @@ def __new__(cls, name, bases, attrs): return super().__new__(cls, name, bases, attrs) - def __call__(cls, *args, **kwargs): + def __call__(cls, *args: Any, **kwargs: Any) -> Any: """Create an instance and generate its remote instantiation code. This method is called when a class instance is created (e.g., MyPlugin(...)). @@ -318,17 +318,17 @@ def get_surface_uid(self, surface_name: str) -> dict[str, str]: instance_code: The code used to instantiate the plugin remotely (set on init) """ - def __init__(self): + def __init__(self) -> None: self._hostname: str | None = None self._port: int | None = None - def in_session(self): + def in_session(self) -> bool: """Check if the client is currently in an active session. Returns: True if both hostname and port are set, False otherwise. """ - return self._hostname and self._port + return bool(self._hostname) and bool(self._port) @asynccontextmanager async def async_session( @@ -337,7 +337,7 @@ async def async_session( port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None, - ): + ) -> Any: """Async context manager for plugin session with Designer. Args: @@ -372,7 +372,7 @@ def session( port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None, - ): + ) -> Any: """Sync context manager for plugin session with Designer. Args: @@ -435,6 +435,6 @@ def _get_register_module_payload(self) -> RegisterPayload: RegisterPayload containing moduleName and contents for registration. """ return RegisterPayload( - moduleName=self.module_name, # type: ignore[attr-defined] + moduleName=self.module_name, contents=self._get_register_module_content(), ) diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index 260adce..32c68af 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -84,7 +84,7 @@ def extract_function_info(func: Callable[..., Any]) -> FunctionInfo: first_node.decorator_list.clear() # Extract blob in python 3 format - source_code: str = ast.unparse(first_node) + source_code_py3: str = ast.unparse(first_node) # Extract function name function_name: str = first_node.name @@ -111,7 +111,7 @@ def extract_function_info(func: Callable[..., Any]) -> FunctionInfo: body_py27 += ast.unparse(stmt) + "\n" return FunctionInfo( - source_code=source_code, + source_code=source_code_py3, source_code_py27=source_code_py27, name=function_name, body=body.strip(), @@ -178,7 +178,7 @@ def __getattr__(self, name: str) -> Any: """ return getattr(self._function, name) - def _args_to_assign(self, *args, **kwargs) -> str: + def _args_to_assign(self, *args: Any, **kwargs: Any) -> str: """Convert function arguments to assignment statements for standalone execution. Args: @@ -242,7 +242,7 @@ def __init__(self, module_name: str, func: Callable[P, T]): D3Function._available_d3functions[module_name].add(self) - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: """Check equality based on function name for unique registration. Returns: @@ -307,7 +307,7 @@ def module_name(self) -> str: """ return self._module_name - def _args_to_string(self, *args, **kwargs) -> str: + def _args_to_string(self, *args: Any, **kwargs: Any) -> str: """Convert function arguments to a string representation for function call generation. Args: diff --git a/src/designer_plugin/d3sdk/session.py b/src/designer_plugin/d3sdk/session.py index 8536dbf..43d15b2 100644 --- a/src/designer_plugin/d3sdk/session.py +++ b/src/designer_plugin/d3sdk/session.py @@ -81,7 +81,7 @@ def __enter__(self) -> "D3Session": ) return self - def __exit__(self, _type, _value, _traceback) -> None: + def __exit__(self, _type: Any, _value: Any, _traceback: Any) -> None: """Exit context manager.""" pass @@ -119,7 +119,7 @@ def plugin( """ return d3_api_plugin(self.hostname, self.port, payload, timeout_sec) - def request(self, method: Method, url_endpoint: str, **kwargs): + def request(self, method: Method, url_endpoint: str, **kwargs: Any) -> Any: """Make a generic HTTP request to Designer API. Args: @@ -214,7 +214,7 @@ async def __aenter__(self) -> "D3AsyncSession": ) return self - async def __aexit__(self, _exc_type, _exc, _tb) -> None: + async def __aexit__(self, _exc_type: Any, _exc: Any, _tb: Any) -> None: """Exit async context manager.""" pass diff --git a/src/designer_plugin/logger.py b/src/designer_plugin/logger.py index e9e9cd0..a801e52 100644 --- a/src/designer_plugin/logger.py +++ b/src/designer_plugin/logger.py @@ -52,6 +52,7 @@ import logging import sys +from typing import Any # Package root logger name LOGGER_NAME = "designer_plugin" @@ -62,7 +63,7 @@ def enable_debug_logging( level: int = logging.DEBUG, - stream: object | None = None, + stream: Any | None = None, format_string: str | None = None, ) -> None: """ diff --git a/src/designer_plugin/models.py b/src/designer_plugin/models.py index d9337e9..02530c8 100644 --- a/src/designer_plugin/models.py +++ b/src/designer_plugin/models.py @@ -58,7 +58,7 @@ class PluginResponse(BaseModel, Generic[RetType]): @field_validator("returnValue", mode="before") @classmethod - def parse_returnValue(cls, v): + def parse_returnValue(cls, v: Any) -> Any: if isinstance(v, str): if v == "null": return None @@ -121,7 +121,7 @@ class PluginException(Exception): _traceback_str: str | None = None _str: str | None = None - def __post_init__(self): + def __post_init__(self) -> None: # Capture current stack trace if not already provided if self._traceback_str is None: self._traceback_str = "".join(traceback.format_stack()[:-1]) diff --git a/uv.lock b/uv.lock index e1ec7de..f00cc56 100644 --- a/uv.lock +++ b/uv.lock @@ -252,6 +252,7 @@ dependencies = [ { name = "aiohttp" }, { name = "pydantic" }, { name = "requests" }, + { name = "types-requests" }, { name = "zeroconf" }, ] @@ -268,6 +269,7 @@ requires-dist = [ { name = "aiohttp", specifier = ">=3.13.2" }, { name = "pydantic", specifier = ">=2.12.4" }, { name = "requests", specifier = ">=2.32.5" }, + { name = "types-requests", specifier = ">=2.32.4.20250913" }, { name = "zeroconf", specifier = ">=0.39.0" }, ] @@ -995,6 +997,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, ] +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From 64900969d5bb58b37a38de3f3a37236bbab0f9e8 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 21:07:17 +0000 Subject: [PATCH 11/37] update project name for pypi --- pyproject.toml | 4 ++-- uv.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8e95907..156a1d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] -name = "disguise-designer-plugin" -version = "1.1.0" +name = "designer-plugin" +version = "1.2.0" description = "A plugin for the Disguise Designer application." authors = [ { name = "Tom Whittock", email = "tom.whittock@disguise.one" }, diff --git a/uv.lock b/uv.lock index f00cc56..d170129 100644 --- a/uv.lock +++ b/uv.lock @@ -245,8 +245,8 @@ wheels = [ ] [[package]] -name = "disguise-designer-plugin" -version = "1.1.0" +name = "designer-plugin" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From 0c17b62fa9024345172275c87528fcfb51a87676 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 21:10:52 +0000 Subject: [PATCH 12/37] ruff format --- src/designer_plugin/d3sdk/ast_utils.py | 2 +- src/designer_plugin/d3sdk/client.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py index d444ca3..408018a 100644 --- a/src/designer_plugin/d3sdk/ast_utils.py +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -273,7 +273,7 @@ def convert_function_to_py27( (modified) node. For AsyncFunctionDef input, returns a new FunctionDef node. """ transformer = ConvertToPython27() - return transformer.visit(function_node) # type: ignore + return transformer.visit(function_node) # type: ignore def convert_class_to_py27(class_node: ast.ClassDef) -> None: diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index a16096e..36d7cc2 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -38,7 +38,9 @@ T = TypeVar("T") -def build_payload(self: Any, method_name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> PluginPayload[Any]: +def build_payload( + self: Any, method_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] +) -> PluginPayload[Any]: """Build plugin payload for remote method execution. Args: @@ -62,7 +64,9 @@ def build_payload(self: Any, method_name: str, args: tuple[Any, ...], kwargs: di return PluginPayload[Any](moduleName=self.module_name, script=script) -def create_d3_plugin_method_wrapper(method_name: str, original_method: Callable[P, T]) -> Callable[..., Any]: +def create_d3_plugin_method_wrapper( + method_name: str, original_method: Callable[P, T] +) -> Callable[..., Any]: """Create a wrapper that executes a method remotely via Designer API calls. This wrapper intercepts method calls and instead of executing locally: @@ -105,7 +109,9 @@ def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: return sync_wrapper -def create_d3_payload_wrapper(method_name: str, original_method: Callable[P, T]) -> Callable[..., PluginPayload[T]]: +def create_d3_payload_wrapper( + method_name: str, original_method: Callable[P, T] +) -> Callable[..., PluginPayload[T]]: """Create a wrapper that generates plugin payload without executing. Args: From fae40931a5c51ef32d418da41ad7e2c79e897446 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 21:15:05 +0000 Subject: [PATCH 13/37] add twine for pypi --- pyproject.toml | 1 + uv.lock | 371 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 372 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 156a1d4..f535a8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dev = [ "pre-commit>=4.4.0", "pytest>=9.0.1", "ruff>=0.14.5", + "twine>=6.2.0", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index d170129..db809b0 100644 --- a/uv.lock +++ b/uv.lock @@ -144,6 +144,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[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, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -153,6 +162,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -244,6 +298,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, +] + [[package]] name = "designer-plugin" version = "1.2.0" @@ -262,6 +364,7 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "ruff" }, + { name = "twine" }, ] [package.metadata] @@ -279,6 +382,7 @@ dev = [ { name = "pre-commit", specifier = ">=4.4.0" }, { name = "pytest", specifier = ">=9.0.1" }, { name = "ruff", specifier = ">=0.14.5" }, + { name = "twine", specifier = ">=6.2.0" }, ] [[package]] @@ -290,6 +394,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "docutils" +version = "0.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, +] + [[package]] name = "filelock" version = "3.20.0" @@ -404,6 +517,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "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, upload-time = "2024-12-04T19:53:05.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, +] + [[package]] name = "identify" version = "2.6.15" @@ -431,6 +556,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -440,6 +577,99 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[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, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[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, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.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/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[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, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -604,6 +834,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nh3" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" }, + { url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" }, + { url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" }, + { url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" }, + { url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, + { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -764,6 +1027,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + [[package]] name = "pydantic" version = "2.12.4" @@ -901,6 +1173,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] +[[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, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -956,6 +1237,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[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, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -971,6 +1266,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "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, upload-time = "2023-05-01T04:11:33.229Z" } +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, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[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, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + [[package]] name = "ruff" version = "0.14.5" @@ -997,6 +1326,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "twine" +version = "6.2.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/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + [[package]] name = "types-requests" version = "2.32.4.20250913" @@ -1224,3 +1586,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/c0/359bdb3b435d9c573aec1f877f8a63d5e81145deb6c160de89647b237363/zeroconf-0.148.0-cp314-cp314t-win32.whl", hash = "sha256:cdc8083f0b5efa908ab6c8e41687bcb75fd3d23f49ee0f34cbc58422437a456f", size = 2755961, upload-time = "2025-10-05T01:09:24.041Z" }, { url = "https://files.pythonhosted.org/packages/d8/ab/7b487afd5d1fd053c5a018565be734ac6d5e554bce938c7cc126154adcfc/zeroconf-0.148.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f72c1f77a89638e87f243a63979f0fd921ce391f83e18e17ec88f9f453717701", size = 3309977, upload-time = "2025-10-05T01:09:26.039Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From a4d62ece73107cdc6aaaa5bd3cfc2e07d340bdef Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Sat, 29 Nov 2025 21:21:53 +0000 Subject: [PATCH 14/37] update readme for published pypi package --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e8052c..e42efcd 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A Python library for creating and managing plugins for Disguise Designer. This l To install the plugin, use pip: ```bash -pip install git+https://github.com/disguise-one/python-plugin +pip install designer-plugin ```
@@ -217,7 +217,7 @@ async with D3AsyncSession('localhost', 80, ["mymodule"]) as session: rename_surface_get_time.payload("surface 1", "surface 2")) # Use plugin() for full response with logs and status - from designer_plugin.d3sdk import PluginResponse + from designer_plugin import PluginResponse response: PluginResponse = await session.plugin( rename_surface_get_time.payload("surface 1", "surface 2")) print(f"Status: {response.status.code}") From f8c217fe5c82b03363895f2ca7c0426c60c54e19 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Mon, 1 Dec 2025 09:16:06 +0000 Subject: [PATCH 15/37] Rephrase SDK to API --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e42efcd..354b2b7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A Python library for creating and managing plugins for Disguise Designer. This library provides: - DNS-SD service publishing for plugin discovery - Remote Python execution on Designer instances -- Multiple execution patterns (Client SDK, Function SDK) +- Multiple execution patterns (Client API, Functional API) ## Installation @@ -66,9 +66,9 @@ If you would prefer not to use the `d3plugin.json` file, construct the `Designer Python scripts can be executed remotely on Designer via the plugin system. -Direct interaction with the plugin API endpoint requires extensive boilerplate code and JSON parsing. However, the Client SDK and Function SDK simplify this process by providing an RPC (Remote Procedure Call) interface that abstracts away the underlying HTTP communication and payload management. +Direct interaction with the plugin API endpoint requires extensive boilerplate code and JSON parsing. However, the Client API and Functional API simplify this process by providing an RPC (Remote Procedure Call) interface that abstracts away the underlying HTTP communication and payload management. -> **Important:** The Designer plugin API only supports Python 2.7, not Python 3. Both the Client SDK and Function SDK attempt to automatically convert your Python 3 code to Python 2.7 (f-strings and type hints are supported). However, some Python 3 features may not be fully compatible and conversion may fail in certain cases. +> **Important:** The Designer plugin API only supports Python 2.7, not Python 3. Both the Client API and Functional API attempt to automatically convert your Python 3 code to Python 2.7 (f-strings and type hints are supported). However, some Python 3 features may not be fully compatible and conversion may fail in certain cases. ## Stub file @@ -87,9 +87,9 @@ if TYPE_CHECKING: This allows you to get autocomplete for Designer objects like `resourceManager`, `Screen2`, `Path`, etc., while writing your plugin code. -## Client SDK +## Client API -The Client SDK allows you to define a class with methods that execute remotely on Designer by simply inheriting from `D3PluginClient`. The Client SDK supports both async and sync methods. +The Client API allows you to define a class with methods that execute remotely on Designer by simply inheriting from `D3PluginClient`. The Client API supports both async and sync methods. **Example** @@ -148,16 +148,16 @@ import asyncio asyncio.run(main()) ``` -## Function SDK +## Functional API -The Function SDK provides finer control over remote execution compared to the Client SDK. While the Client SDK automatically manages the entire execution lifecycle (registration and execution are transparent), the Function SDK gives you explicit control over: +The Functional API provides finer control over remote execution compared to the Client API. While the Client API automatically manages the entire execution lifecycle (registration and execution are transparent), the Functional API gives you explicit control over: - **Payload generation**: Decorators add a `payload()` method to generate execution payloads - **Session management**: You manually create sessions and control when to register modules - **Function grouping**: Group related functions into modules for efficient reuse - **Response handling**: Choose between `session.plugin()` for full response (status, logs, return value) or `session.rpc()` for just the return value -The Function SDK offers two decorators: `@d3pythonscript` and `@d3function`: +The Functional API offers two decorators: `@d3pythonscript` and `@d3function`: - **`@d3pythonscript`**: - Does not require registration. - Best for simple scripts executed once or infrequently. From 28436c7f2283e8c4543fc6fe9a04ca7743d529e8 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Mon, 1 Dec 2025 16:04:17 +0000 Subject: [PATCH 16/37] change naming from plugin to execute --- README.md | 6 +++--- src/designer_plugin/d3sdk/session.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 354b2b7..03bca58 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The Functional API provides finer control over remote execution compared to the - **Payload generation**: Decorators add a `payload()` method to generate execution payloads - **Session management**: You manually create sessions and control when to register modules - **Function grouping**: Group related functions into modules for efficient reuse -- **Response handling**: Choose between `session.plugin()` for full response (status, logs, return value) or `session.rpc()` for just the return value +- **Response handling**: Choose between `session.execute()` for full response (status, logs, return value) or `session.rpc()` for just the return value The Functional API offers two decorators: `@d3pythonscript` and `@d3function`: - **`@d3pythonscript`**: @@ -171,7 +171,7 @@ The Functional API offers two decorators: `@d3pythonscript` and `@d3function`: Both `D3AsyncSession` and `D3Session` provide two methods for executing functions: - **`session.rpc(payload)`** - Returns only the return value from the function execution. Simpler for most use cases. -- **`session.plugin(payload)`** - Returns a `PluginResponse` object containing: +- **`session.execute(payload)`** - Returns a `PluginResponse` object containing: - `returnValue`: The function's return value - `status`: Execution status (code, message, details) - `d3Log`: Designer console output during execution @@ -218,7 +218,7 @@ async with D3AsyncSession('localhost', 80, ["mymodule"]) as session: # Use plugin() for full response with logs and status from designer_plugin import PluginResponse - response: PluginResponse = await session.plugin( + response: PluginResponse = await session.execute( rename_surface_get_time.payload("surface 1", "surface 2")) print(f"Status: {response.status.code}") print(f"Return value: {response.returnValue}") diff --git a/src/designer_plugin/d3sdk/session.py b/src/designer_plugin/d3sdk/session.py index 43d15b2..d5b818c 100644 --- a/src/designer_plugin/d3sdk/session.py +++ b/src/designer_plugin/d3sdk/session.py @@ -100,7 +100,7 @@ def rpc( Raises: PluginException: If the plugin execution fails. """ - return self.plugin(payload, timeout_sec).returnValue + return self.execute(payload, timeout_sec).returnValue def plugin( self, payload: PluginPayload[RetType], timeout_sec: float | None = None @@ -253,9 +253,9 @@ async def rpc( Raises: PluginException: If the plugin execution fails. """ - return (await self.plugin(payload, timeout_sec)).returnValue + return (await self.execute(payload, timeout_sec)).returnValue - async def plugin( + async def execute( self, payload: PluginPayload[RetType], timeout_sec: float | None = None ) -> PluginResponse[RetType]: """Execute a plugin script on Designer asynchronously. From 95dd216d8b746b9c9b57bc9cf1a7d6d2f28af471 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Mon, 1 Dec 2025 16:29:48 +0000 Subject: [PATCH 17/37] reflect feedbacks --- src/designer_plugin/api.py | 2 +- src/designer_plugin/d3sdk/ast_utils.py | 9 ++------ src/designer_plugin/d3sdk/client.py | 4 ++-- src/designer_plugin/d3sdk/function.py | 3 ++- src/designer_plugin/d3sdk/session.py | 2 +- src/designer_plugin/models.py | 14 +++++++------ tests/test_core.py | 29 ++++++++++++++++++++++++++ 7 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/designer_plugin/api.py b/src/designer_plugin/api.py index 23eefca..edd6b56 100644 --- a/src/designer_plugin/api.py +++ b/src/designer_plugin/api.py @@ -201,7 +201,7 @@ async def d3_api_aregister_module( timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, ) except Exception as e: - raise Exception(f"Failed to register module '{payload.moduleName}") from e + raise Exception(f"Failed to register module: '{payload.moduleName}'") from e plugin_response: PluginRegisterResponse = PluginRegisterResponse.model_validate( response diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py index 408018a..8943644 100644 --- a/src/designer_plugin/d3sdk/ast_utils.py +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -59,18 +59,13 @@ def filter_base_classes(class_node: ast.ClassDef) -> None: def filter_init_args(class_node: ast.ClassDef) -> list[str]: - """Remove excluded arguments from __init__ method and extract parameter names. - - This function modifies the class_node in-place by: - 1. Removing excluded parameters from __init__ signature - 2. Removing excluded arguments from super().__init__() calls - 3. Returning the list of remaining parameter names (excluding 'self') + """Extract parameter names from the __init__ method of a class. Args: class_node: The class definition node to process Returns: - List of parameter names that remain after filtering (excluding 'self') + List of parameter names from __init__ (excluding 'self'), or empty list if no __init__ found """ for node in class_node.body: if not isinstance(node, ast.FunctionDef): diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 36d7cc2..9d921e6 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -231,7 +231,7 @@ def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> t attrs["source_code_py27"] = f"{ast.unparse(class_node)}" # Wrap all user-defined public methods to execute remotely via D3 API - # Skip private methods (_*) and internal framework methods + # Skip internal framework methods for attr_name, attr_value in attrs.items(): if callable(attr_value) and not attr_name.startswith("__"): attrs[attr_name] = create_d3_plugin_method_wrapper( @@ -318,7 +318,7 @@ def get_surface_uid(self, surface_name: str) -> dict[str, str]: # Use as sync context manager with plugin.session("localhost", 80): - result = await plugin.get_surface_uid("surface 1") + result = plugin.get_surface_uid("surface 1") ``` Attributes: instance_code: The code used to instantiate the plugin remotely (set on init) diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index 32c68af..c833325 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -189,7 +189,8 @@ def _args_to_assign(self, *args: Any, **kwargs: Any) -> str: String containing variable assignment statements, one per line. """ args_parts = [ - f"{self._function_info.args[i]}={repr(arg)}" for i, arg in enumerate(args) + f"{param}={repr(arg)}" + for param, arg in zip(self._function_info.args, args, strict=False) ] kwargs_parts = [f"{name}={repr(value)}" for name, value in kwargs.items()] return "\n".join(args_parts + kwargs_parts) diff --git a/src/designer_plugin/d3sdk/session.py b/src/designer_plugin/d3sdk/session.py index d5b818c..63992ab 100644 --- a/src/designer_plugin/d3sdk/session.py +++ b/src/designer_plugin/d3sdk/session.py @@ -102,7 +102,7 @@ def rpc( """ return self.execute(payload, timeout_sec).returnValue - def plugin( + def execute( self, payload: PluginPayload[RetType], timeout_sec: float | None = None ) -> PluginResponse[RetType]: """Execute a plugin script on Designer. diff --git a/src/designer_plugin/models.py b/src/designer_plugin/models.py index 02530c8..c67535c 100644 --- a/src/designer_plugin/models.py +++ b/src/designer_plugin/models.py @@ -59,12 +59,14 @@ class PluginResponse(BaseModel, Generic[RetType]): @field_validator("returnValue", mode="before") @classmethod def parse_returnValue(cls, v: Any) -> Any: - if isinstance(v, str): - if v == "null": - return None - else: - return json.loads(v) - return v + if not isinstance(v, str): + return v + if v == "null": + return None + try: + return json.loads(v) + except json.JSONDecodeError: + return v def returnCastValue(self, castType: type[RetCastType]) -> RetCastType: """Validate and cast the return value to the specified type. diff --git a/tests/test_core.py b/tests/test_core.py index 02d2603..8605400 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,6 +3,8 @@ Copyright (c) 2025 Disguise Technologies ltd """ +import pytest + from designer_plugin.d3sdk.function import ( D3Function, D3PythonScript, @@ -290,3 +292,30 @@ def test_func(a: int, b: int = 10) -> int: assert "a=5" in payload.script assert "b=15" in payload.script assert "return a + b" in payload.script + + def test_d3pythonscript_args_to_assign_with_extra_args(self): + """Test that _args_to_assign handles extra arguments gracefully. + + This test will fail with the enumerate/indexing implementation: + args_parts = [f"{self._function_info.args[i]}={repr(arg)}" for i, arg in enumerate(args)] + + But will pass with the zip implementation: + args_parts = [f"{param}={repr(arg)}" for param, arg in zip(self._function_info.args, args)] + + The enumerate version causes IndexError when len(args) > len(function parameters). + The zip version stops at the shorter sequence length. + """ + @d3pythonscript + def test_func(a: int, b: int) -> int: + return a + b + + # This function has 2 parameters but we're passing 3 arguments + # The first implementation will crash with IndexError + # The second implementation will silently ignore the extra argument + result = test_func._args_to_assign(1, 2, 3) + + # Should only contain assignments for the defined parameters + assert "a=1" in result + assert "b=2" in result + # The extra argument should be silently ignored by the zip implementation + assert result.count("=") == 2 # Only 2 assignments From 17e64e67b5b53f2bd5626e402cf5fc8353316868 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 10:14:44 +0000 Subject: [PATCH 18/37] validate arguments --- src/designer_plugin/api.py | 3 +- src/designer_plugin/d3sdk/ast_utils.py | 73 +++++++ src/designer_plugin/d3sdk/client.py | 27 ++- src/designer_plugin/d3sdk/function.py | 35 +++- tests/test_client.py | 253 +++++++++++++++++++++++++ tests/test_core.py | 88 +++++++-- 6 files changed, 449 insertions(+), 30 deletions(-) create mode 100644 tests/test_client.py diff --git a/src/designer_plugin/api.py b/src/designer_plugin/api.py index edd6b56..a681967 100644 --- a/src/designer_plugin/api.py +++ b/src/designer_plugin/api.py @@ -190,7 +190,6 @@ async def d3_api_aregister_module( PluginException: If module registration fails on Designer side. """ try: - # logger.debug(f"Register module: {payload.moduleName}\n{payload.contents}") logger.debug(f"Register module:{payload.debug_string()}") response: Any = await d3_api_arequest( Method.POST, @@ -201,7 +200,7 @@ async def d3_api_aregister_module( timeout=aiohttp.ClientTimeout(timeout_sec) if timeout_sec else None, ) except Exception as e: - raise Exception(f"Failed to register module: '{payload.moduleName}'") from e + raise Exception(f"Failed to register module: {payload.moduleName}") from e plugin_response: PluginRegisterResponse = PluginRegisterResponse.model_validate( response diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py index 8943644..2581abb 100644 --- a/src/designer_plugin/d3sdk/ast_utils.py +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -288,6 +288,79 @@ def convert_class_to_py27(class_node: ast.ClassDef) -> None: class_node.body[i] = convert_function_to_py27(node) +############################################################################### +# Signature validation utilities +def validate_and_bind_signature( + sig: inspect.Signature, *args: Any, **kwargs: Any +) -> inspect.BoundArguments: + """Validate arguments against a function signature and return bound arguments. + + This is a shared utility used by both D3PluginClient and D3PythonScript + to ensure consistent argument validation across the codebase. + + Args: + sig: The function signature to validate against + *args: Positional arguments to validate + **kwargs: Keyword arguments to validate + + Returns: + BoundArguments object with validated and bound arguments + + Raises: + TypeError: If arguments don't match the signature (too many args, + missing required args, unexpected kwargs, etc.) + """ + bound_args = sig.bind(*args, **kwargs) + bound_args.apply_defaults() + return bound_args + + +def validate_and_extract_args( + sig: inspect.Signature, + exclude_self: bool, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> tuple[tuple[Any, ...], dict[str, Any]]: + """Validate arguments and extract them into positional and keyword arguments. + + This is a shared utility that validates arguments against a signature and + separates them into positional and keyword arguments for remote execution. + + Args: + sig: The function signature to validate against + exclude_self: If True, exclude 'self' parameter from extracted arguments + args: Positional arguments to validate + kwargs: Keyword arguments to validate + + Returns: + Tuple of (positional_args, keyword_args) ready for remote execution + + Raises: + TypeError: If arguments don't match the signature + """ + # Validate arguments using shared validation utility + bound_args = validate_and_bind_signature(sig, *args, **kwargs) + + # Extract arguments + args_dict = dict(bound_args.arguments) + if exclude_self: + args_dict.pop("self", None) + + # Separate back into positional and keyword arguments + positional = [] + keyword = {} + for param_name, param in sig.parameters.items(): + if exclude_self and param_name == "self": + continue + if param_name in args_dict: + if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): + positional.append(args_dict[param_name]) + else: + keyword[param_name] = args_dict[param_name] + + return tuple(positional), keyword + + ############################################################################### # Python package finder utility def find_packages_in_current_file(caller_stack: int = 1) -> list[str]: diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 9d921e6..8b90c5e 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -24,6 +24,7 @@ filter_init_args, get_class_node, get_source, + validate_and_extract_args, ) from designer_plugin.models import ( D3_PLUGIN_DEFAULT_PORT, @@ -70,11 +71,12 @@ def create_d3_plugin_method_wrapper( """Create a wrapper that executes a method remotely via Designer API calls. This wrapper intercepts method calls and instead of executing locally: - 1. Serializes the arguments using repr() - 2. Builds a script string in the form: "return plugin.{method_name}({args})" - 3. Creates a PluginPayload with the script and module information - 4. Sends it to Designer via d3_api_plugin or d3_api_aplugin - 5. Returns the result from the remote execution + 1. Validates arguments against the original method signature + 2. Serializes the arguments using repr() + 3. Builds a script string in the form: "return plugin.{method_name}({args})" + 4. Creates a PluginPayload with the script and module information + 5. Sends it to Designer via d3_api_plugin or d3_api_aplugin + 6. Returns the result from the remote execution Args: method_name: Name of the method to wrap @@ -82,14 +84,20 @@ def create_d3_plugin_method_wrapper( Returns: An async wrapper if the original method is async, otherwise a sync wrapper. - Both wrappers preserve the original method's metadata via functools.wraps. + Both wrappers preserve the original method's metadata and signature validation. """ + # Get the signature for argument validation + sig = inspect.signature(original_method) + # Determine whether to create async or sync wrapper based on original method if inspect.iscoroutinefunction(original_method): # Create async wrapper that uses async Designer API call @functools.wraps(original_method) async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: - payload = build_payload(self, method_name, args, kwargs) + positional, keyword = validate_and_extract_args( + sig, True, (self,) + args, kwargs + ) + payload = build_payload(self, method_name, positional, keyword) response: PluginResponse[T] = await d3_api_aplugin( self._hostname, self._port, payload ) @@ -100,7 +108,10 @@ async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: # Create sync wrapper that uses synchronous Designer API call @functools.wraps(original_method) def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: - payload = build_payload(self, method_name, args, kwargs) + positional, keyword = validate_and_extract_args( + sig, True, (self,) + args, kwargs + ) + payload = build_payload(self, method_name, positional, keyword) response: PluginResponse[T] = d3_api_plugin( self._hostname, self._port, payload ) diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index c833325..662174a 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -16,6 +16,8 @@ from designer_plugin.d3sdk.ast_utils import ( convert_function_to_py27, find_packages_in_current_file, + validate_and_bind_signature, + validate_and_extract_args, ) from designer_plugin.models import ( PluginPayload, @@ -187,13 +189,21 @@ def _args_to_assign(self, *args: Any, **kwargs: Any) -> str: Returns: String containing variable assignment statements, one per line. + + Raises: + TypeError: If arguments don't match the function signature. """ - args_parts = [ - f"{param}={repr(arg)}" - for param, arg in zip(self._function_info.args, args, strict=False) + sig: inspect.Signature = inspect.signature(self._function) + positional, keyword_args = validate_and_extract_args(sig, False, args, kwargs) + + # Create assignment strings for positional arguments + assignments = [ + f"{self._function_info.args[i]}={repr(arg)}" + for i, arg in enumerate(positional) ] - kwargs_parts = [f"{name}={repr(value)}" for name, value in kwargs.items()] - return "\n".join(args_parts + kwargs_parts) + + kwargs_parts = [f"{name}={repr(value)}" for name, value in keyword_args.items()] + return "\n".join(assignments + kwargs_parts) def payload(self, *args: P.args, **kwargs: P.kwargs) -> PluginPayload[T]: """Generate an execution payload for standalone script execution. @@ -206,6 +216,9 @@ def payload(self, *args: P.args, **kwargs: P.kwargs) -> PluginPayload[T]: Returns: PluginPayload containing the script with argument assignments and function body. + + Raises: + TypeError: If arguments don't match the function signature. """ all_args: str = self._args_to_assign(*args, **kwargs) return PluginPayload[T](script=f"{all_args}\n{self._function_info.body_py27}") @@ -317,7 +330,16 @@ def _args_to_string(self, *args: Any, **kwargs: Any) -> str: Returns: Comma-separated string representation of all arguments suitable for function calls. + + Raises: + TypeError: If arguments don't match the function signature. """ + # Validate arguments against signature using shared utility + sig = inspect.signature(self._function) + validate_and_bind_signature( + sig, *args, **kwargs + ) # This validates and raises TypeError if invalid + # Convert positional args args_parts = [repr(arg) for arg in args] # Convert keyword args @@ -337,6 +359,9 @@ def payload(self, *args: P.args, **kwargs: P.kwargs) -> PluginPayload[T]: Returns: PluginPayload containing the module name and script that calls the function by name. + + Raises: + TypeError: If arguments don't match the function signature. """ return PluginPayload[T]( moduleName=self._module_name, diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..7b767d8 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,253 @@ +""" +MIT License +Copyright (c) 2025 Disguise Technologies ltd + +Tests for D3PluginClient signature validation and method wrapping. +""" + +import pytest +from unittest.mock import AsyncMock, Mock, patch + +from designer_plugin.d3sdk.client import D3PluginClient +from designer_plugin.models import PluginResponse, PluginStatus + + +class SignatureValidationPlugin(D3PluginClient): + """Test plugin class for signature validation tests.""" + + def __init__(self, config_value: str): + super().__init__() + self.config_value = config_value + + def simple_method(self, a: int, b: int) -> int: + """Simple method with two required parameters.""" + return a + b + + def method_with_defaults(self, x: int, y: int = 10, z: int = 20) -> int: + """Method with default parameters.""" + return x + y + z + + def method_positional_only(self, a: int, b: int, /) -> int: + """Method with positional-only parameters (Python 3.8+).""" + return a * b + + def method_keyword_only(self, *, name: str, value: int) -> str: + """Method with keyword-only parameters.""" + return f"{name}={value}" + + def method_mixed(self, a: int, b: int = 5, *, c: str) -> str: + """Method with mixed parameter types.""" + return f"a={a}, b={b}, c={c}" + + async def async_method(self, x: int, y: int) -> int: + """Async method for testing async wrapper.""" + return x * y + + +class TestSignatureValidation: + """Test suite for signature validation in wrapped methods.""" + + @pytest.fixture + def plugin(self): + """Create a test plugin instance.""" + return SignatureValidationPlugin("test_config") + + @pytest.fixture + def mock_response(self): + """Create a mock response.""" + return PluginResponse( + status=PluginStatus(code=0, message="Success", details=[]), + returnValue=42 + ) + + def test_correct_arguments_sync(self, plugin, mock_response): + """Test that correct arguments pass through successfully.""" + with patch('designer_plugin.d3sdk.client.d3_api_plugin', return_value=mock_response) as mock_api: + plugin._hostname = "localhost" + plugin._port = 80 + + result = plugin.simple_method(5, 10) + + assert result == 42 + mock_api.assert_called_once() + + def test_too_many_positional_arguments(self, plugin): + """Test that too many positional arguments raise TypeError.""" + plugin._hostname = "localhost" + plugin._port = 80 + + with pytest.raises(TypeError, match="too many positional arguments"): + plugin.simple_method(1, 2, 3) + + def test_multiple_values_for_argument(self, plugin): + """Test that multiple values for same argument raise TypeError.""" + plugin._hostname = "localhost" + plugin._port = 80 + + with pytest.raises(TypeError, match="multiple values for argument"): + plugin.simple_method(1, a=2) + + def test_missing_required_argument(self, plugin): + """Test that missing required arguments raise TypeError.""" + plugin._hostname = "localhost" + plugin._port = 80 + + with pytest.raises(TypeError, match="missing a required argument"): + plugin.simple_method(1) + + def test_unexpected_keyword_argument(self, plugin): + """Test that unexpected keyword arguments raise TypeError.""" + plugin._hostname = "localhost" + plugin._port = 80 + + with pytest.raises(TypeError, match="got an unexpected keyword argument"): + plugin.simple_method(1, 2, unexpected=3) + + def test_method_with_defaults_partial_args(self, plugin, mock_response): + """Test method with default parameters using partial arguments.""" + with patch('designer_plugin.d3sdk.client.d3_api_plugin', return_value=mock_response): + plugin._hostname = "localhost" + plugin._port = 80 + + # Should work with just required argument + result = plugin.method_with_defaults(5) + assert result == 42 + + def test_method_with_defaults_override(self, plugin, mock_response): + """Test method with default parameters overriding defaults.""" + with patch('designer_plugin.d3sdk.client.d3_api_plugin', return_value=mock_response): + plugin._hostname = "localhost" + plugin._port = 80 + + # Should work with overriding defaults + result = plugin.method_with_defaults(5, 15, 25) + assert result == 42 + + def test_method_with_defaults_keyword(self, plugin, mock_response): + """Test method with default parameters using keyword arguments.""" + with patch('designer_plugin.d3sdk.client.d3_api_plugin', return_value=mock_response): + plugin._hostname = "localhost" + plugin._port = 80 + + # Should work with keyword arguments + result = plugin.method_with_defaults(5, z=30) + assert result == 42 + + def test_keyword_only_parameters(self, plugin, mock_response): + """Test method with keyword-only parameters.""" + with patch('designer_plugin.d3sdk.client.d3_api_plugin', return_value=mock_response): + plugin._hostname = "localhost" + plugin._port = 80 + + # Should work with keyword arguments + result = plugin.method_keyword_only(name="test", value=100) + assert result == 42 + + def test_keyword_only_parameters_as_positional_fails(self, plugin): + """Test that keyword-only parameters cannot be passed as positional.""" + plugin._hostname = "localhost" + plugin._port = 80 + + with pytest.raises(TypeError, match="too many positional arguments"): + plugin.method_keyword_only("test", 100) + + def test_mixed_parameters(self, plugin, mock_response): + """Test method with mixed parameter types.""" + with patch('designer_plugin.d3sdk.client.d3_api_plugin', return_value=mock_response): + plugin._hostname = "localhost" + plugin._port = 80 + + result = plugin.method_mixed(1, 2, c="test") + assert result == 42 + + def test_mixed_parameters_missing_keyword_only(self, plugin): + """Test that missing keyword-only parameter raises TypeError.""" + plugin._hostname = "localhost" + plugin._port = 80 + + with pytest.raises(TypeError, match="missing a required keyword-only argument"): + plugin.method_mixed(1, 2) + + def test_async_method_signature_validation(self, plugin): + """Test that async methods have signature validation (check without running).""" + import inspect + + # Verify the async_method is wrapped + assert callable(plugin.async_method) + + # The wrapper should preserve the function metadata + assert plugin.async_method.__name__ == "async_method" + + +class TestValidateAndExtractArgs: + """Test suite for validate_and_extract_args helper function.""" + + def test_positional_arguments_extraction(self): + """Test that positional arguments are correctly extracted.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b, c): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None, 1, 2, 3), {}) + + assert positional == (1, 2, 3) + assert keyword == {} + + def test_keyword_arguments_extraction(self): + """Test that keyword arguments are correctly extracted.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, *, a, b): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None,), {'a': 1, 'b': 2}) + + assert positional == () + assert keyword == {'a': 1, 'b': 2} + + def test_mixed_arguments_extraction(self): + """Test that mixed arguments are correctly extracted.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b=5, *, c): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None, 1), {'b': 10, 'c': 'test'}) + + assert positional == (1, 10) + assert keyword == {'c': 'test'} + + def test_defaults_applied(self): + """Test that default values are applied correctly.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b=10, c=20): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None, 1), {}) + + # Should include defaults + assert positional == (1, 10, 20) + assert keyword == {} + + def test_invalid_signature_raises_type_error(self): + """Test that invalid signatures raise TypeError.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b): + pass + + sig = inspect.signature(test_func) + + with pytest.raises(TypeError): + validate_and_extract_args(sig, True, (None, 1, 2, 3), {}) diff --git a/tests/test_core.py b/tests/test_core.py index 8605400..2a264af 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -239,6 +239,47 @@ def test_get_all_d3functions(self): assert "standalone_function" in function_names +class TestD3FunctionSignatureValidation: + """Test suite for D3Function signature validation.""" + + def test_d3function_payload_too_many_args(self): + """Test that D3Function.payload raises TypeError for too many arguments.""" + @d3function("test_module") + def test_func(a: int, b: int) -> int: + return a + b + + with pytest.raises(TypeError, match="too many positional arguments"): + test_func.payload(1, 2, 3) + + def test_d3function_payload_missing_args(self): + """Test that D3Function.payload raises TypeError for missing required arguments.""" + @d3function("test_module") + def test_func(a: int, b: int) -> int: + return a + b + + with pytest.raises(TypeError, match="missing a required argument"): + test_func.payload(1) + + def test_d3function_payload_multiple_values(self): + """Test that D3Function.payload raises TypeError for multiple values for same argument.""" + @d3function("test_module") + def test_func(a: int, b: int) -> int: + return a + b + + with pytest.raises(TypeError, match="multiple values for argument"): + test_func.payload(1, a=2) + + def test_d3function_payload_correct_args(self): + """Test that D3Function.payload works correctly with valid arguments.""" + @d3function("test_module") + def test_func(a: int, b: int) -> int: + return a + b + + payload = test_func.payload(5, 10) + assert payload.moduleName == "test_module" + assert "test_func(5, 10)" in payload.script + + class TestD3FunctionEquality: def test_hash_and_equality(self): func1 = D3Function("module1", example_function) @@ -294,28 +335,45 @@ def test_func(a: int, b: int = 10) -> int: assert "return a + b" in payload.script def test_d3pythonscript_args_to_assign_with_extra_args(self): - """Test that _args_to_assign handles extra arguments gracefully. - - This test will fail with the enumerate/indexing implementation: - args_parts = [f"{self._function_info.args[i]}={repr(arg)}" for i, arg in enumerate(args)] - - But will pass with the zip implementation: - args_parts = [f"{param}={repr(arg)}" for param, arg in zip(self._function_info.args, args)] + """Test that _args_to_assign validates signature and raises TypeError for extra arguments. - The enumerate version causes IndexError when len(args) > len(function parameters). - The zip version stops at the shorter sequence length. + With signature validation, passing too many arguments should raise TypeError + just like calling a regular Python function would. """ @d3pythonscript def test_func(a: int, b: int) -> int: return a + b # This function has 2 parameters but we're passing 3 arguments - # The first implementation will crash with IndexError - # The second implementation will silently ignore the extra argument - result = test_func._args_to_assign(1, 2, 3) + # Should raise TypeError due to signature validation + with pytest.raises(TypeError, match="too many positional arguments"): + test_func._args_to_assign(1, 2, 3) - # Should only contain assignments for the defined parameters + def test_d3pythonscript_args_to_assign_correct_args(self): + """Test that _args_to_assign works correctly with valid arguments.""" + @d3pythonscript + def test_func(a: int, b: int) -> int: + return a + b + + # Test with correct number of arguments + result = test_func._args_to_assign(1, 2) assert "a=1" in result assert "b=2" in result - # The extra argument should be silently ignored by the zip implementation - assert result.count("=") == 2 # Only 2 assignments + + def test_d3pythonscript_payload_missing_args(self): + """Test that payload() raises TypeError for missing required arguments.""" + @d3pythonscript + def test_func(a: int, b: int) -> int: + return a + b + + with pytest.raises(TypeError, match="missing a required argument"): + test_func.payload(1) + + def test_d3pythonscript_payload_multiple_values(self): + """Test that payload() raises TypeError for multiple values for same argument.""" + @d3pythonscript + def test_func(a: int, b: int) -> int: + return a + b + + with pytest.raises(TypeError, match="multiple values for argument"): + test_func.payload(1, a=2) From 3959a4ed96b1b2268ced4688d6d3defc0cce67a1 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 10:44:41 +0000 Subject: [PATCH 19/37] add github badge --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 03bca58..3d9aeda 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # designer-plugin +[![CI](https://github.com/disguise-one/python-plugin/workflows/CI/badge.svg)](https://github.com/disguise-one/python-plugin/actions) +[![PyPI version](https://badge.fury.io/py/designer-plugin.svg)](https://pypi.org/project/designer-plugin/) +[![Python Version](https://img.shields.io/pypi/pyversions/designer-plugin.svg)](https://pypi.org/project/designer-plugin/) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + A Python library for creating and managing plugins for Disguise Designer. This library provides: - DNS-SD service publishing for plugin discovery - Remote Python execution on Designer instances From 8fa83d920f11ad53042039f0385163aa24dd3ac9 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 10:52:13 +0000 Subject: [PATCH 20/37] Add changelog and contributing --- CHANGELOG.md | 36 ++++++ CONTRIBUTING.md | 291 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8110396 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.2.0] - 2024-12-02 + +### Added +- **Client API**: Metaclass-based remote method execution with `D3PluginClient` + - Support for both sync and async methods + - Automatic Python 2.7 code conversion for Designer compatibility + - Session management with context managers +- **Functional API**: Decorator-based remote execution + - `@d3pythonscript` decorator for one-off script execution + - `@d3function(module_name)` decorator for reusable module-based functions + - Function chaining support within modules +- **Session Management**: `D3Session` and `D3AsyncSession` classes + - `rpc()` method for simple return value retrieval + - `execute()` method for full response with logs and status + - Automatic module registration via context managers +- **Type Safety**: Full Pydantic models for all API interactions + - `PluginPayload`, `PluginResponse`, `RegisterPayload` + - Generic type support for type-safe return values +- **Logging**: Configurable logging with `enable_debug_logging()` + - NullHandler by default (library best practice) + - Granular module-level control +- **AST Utilities**: Python 3 to Python 2.7 code transformation + - F-string to `.format()` conversion + - Type annotation removal + - Async/await removal + - Automatic package import detection +- Comprehensive test suite with 99 tests covering all major functionality +- CI/CD with GitHub Actions (ruff, mypy, pytest) +- PEP 561 type hints marker (`py.typed`) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..113f899 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,291 @@ +# Contributing to designer-plugin + +Thank you for your interest in contributing to designer-plugin! This document provides guidelines and instructions for contributing. + +## Development Setup + +### Prerequisites + +- Python 3.11 or higher +- [uv](https://github.com/astral-sh/uv) package manager + +### Getting Started + +1. **Clone the repository:** + ```bash + git clone https://github.com/disguise-one/python-plugin.git + cd python-plugin + ``` + +2. **Install dependencies:** + ```bash + uv sync --dev + ``` + +3. **Install pre-commit hooks:** + ```bash + uv run pre-commit install + ``` + +## Development Workflow + +### Running Tests + +Run the full test suite: +```bash +uv run pytest +``` + +Run tests with verbose output: +```bash +uv run pytest -v +``` + +Run specific test file: +```bash +uv run pytest tests/test_core.py +``` + +### Code Quality Checks + +**Linting:** +```bash +uv run ruff check +``` + +**Auto-fix linting issues:** +```bash +uv run ruff check --fix +``` + +**Formatting:** +```bash +uv run ruff format +``` + +**Type checking:** +```bash +uv run mypy +``` + +**Run all checks:** +```bash +uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest +``` + +### Pre-commit Hooks + +Pre-commit hooks are configured to automatically run: +- `ruff check --fix` - Linting with auto-fix +- `ruff format` - Code formatting + +These run automatically on `git commit`. To run manually: +```bash +uv run pre-commit run --all-files +``` + +## Code Style + +### General Guidelines + +- Follow PEP 8 style guidelines +- Use type hints for all function signatures +- Write docstrings for all public APIs +- Prefer explicit over implicit + +### Type Hints + +All code must include type hints: +```python +def my_function(name: str, count: int = 0) -> dict[str, int]: + """Do something useful. + + Args: + name: The name parameter. + count: The count parameter. + + Returns: + A dictionary mapping name to count. + """ + return {name: count} +``` + +### Docstrings + +Use Google-style docstrings: +```python +def example_function(param1: str, param2: int) -> bool: + """Brief description of the function. + + More detailed explanation if needed. Can span + multiple lines. + + Args: + param1: Description of param1. + param2: Description of param2. + + Returns: + Description of return value. + + Raises: + ValueError: When param2 is negative. + """ + if param2 < 0: + raise ValueError("param2 must be non-negative") + return True +``` + +### Code Organization + +- Group related functionality together +- Use clear, descriptive names +- Avoid circular imports + +## Testing Guidelines + +### Test Structure + +- Place tests in the `tests/` directory +- Name test files `test_*.py` +- Name test classes `Test*` +- Name test functions `test_*` + +### Writing Tests + +```python +class TestMyFeature: + """Test suite for my feature.""" + + def test_basic_functionality(self) -> None: + """Test basic functionality works correctly.""" + result = my_function("test", 42) + assert result == {"test": 42} + + def test_error_handling(self) -> None: + """Test error handling for invalid input.""" + with pytest.raises(ValueError): + my_function("test", -1) +``` + +### Test Coverage + +- Aim for high test coverage +- Test both success and error cases +- Test edge cases and boundary conditions +- Use mocks for external dependencies + +## Pull Request Process + +### Before Submitting + +1. **Create a feature branch:** + ```bash + git checkout -b feature/my-new-feature + ``` + +2. **Make your changes:** + - Write code + - Add tests + - Update documentation + +3. **Run all checks:** + ```bash + uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest + ``` + +4. **Commit your changes:** + ```bash + git add . + git commit -m "Add my new feature" + ``` + + Commit messages should: + - Use present tense ("Add feature" not "Added feature") + - Be clear and descriptive + - Reference issue numbers if applicable + +5. **Push to your fork:** + ```bash + git push origin feature/my-new-feature + ``` + +### Submitting the PR + +1. Open a Pull Request on GitHub +2. Fill out the PR template +3. Link any related issues +4. Wait for CI checks to pass +5. Address review feedback + +### PR Requirements + +- All tests must pass +- Code must pass linting and type checking +- New features must include tests +- Documentation must be updated + +## Reporting Issues + +### Bug Reports + +When reporting bugs, please include: +- Description of the issue +- Steps to reproduce +- Expected behavior +- Actual behavior +- Python version +- Package version +- Minimal code example (if applicable) + +### Feature Requests + +When requesting features, please include: +- Clear description of the feature +- Use case and motivation +- Example API or usage (if applicable) +- Any alternatives you've considered + +## Documentation + +### Updating Documentation + +- Update README.md for user facing changes +- Update docstrings for API changes +- Update CHANGELOG.md following Keep a Changelog format +- Add examples for new features + +### Documentation Style + +- Write clear, concise documentation +- Include code examples +- Explain the "why" not just the "what" +- Keep documentation up to date with code + +## Release Process + +Releases are managed by project maintainers: + +1. Update version in `pyproject.toml` +2. Update `CHANGELOG.md` +3. Create git tag: `git tag -a v1.2.0 -m "Release v1.2.0"` +4. Push tag: `git push origin v1.2.0` +5. Build distributions: `uv build` +6. Upload to PyPI: `twine upload dist/*` + +## Code of Conduct + +- Be respectful and inclusive +- Focus on constructive feedback +- Help others learn and grow +- Assume good intentions + +## Questions? + +If you have questions about contributing: +- Open a [GitHub Issue](https://github.com/disguise-one/python-plugin/issues) +- Check existing issues and PRs +- Review the documentation + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. From 3ca2601479d14a9ca18f1ebda13289ab77206959 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 10:54:08 +0000 Subject: [PATCH 21/37] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 28ec18c..fb2d6c5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 disguise +Copyright (c) 2025 Disguise Technologies ltd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From d99bb28bb97b892c7c6ae6e4cf704be110eca303 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 10:58:04 +0000 Subject: [PATCH 22/37] Update description of pyproject --- .vscode/settings.json | 12 ++++-------- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 184c4eb..bb23db1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,7 @@ { - "python.testing.unittestArgs": [ - "-v", - "-s", - ".", - "-p", - "test_*.py" + "python.testing.pytestArgs": [ + "tests" ], - "python.testing.pytestEnabled": false, - "python.testing.unittestEnabled": true + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f535a8b..c7bb151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "designer-plugin" version = "1.2.0" -description = "A plugin for the Disguise Designer application." +description = "Python library for creating Disguise Designer plugins with DNS-SD discovery and remote Python execution" authors = [ { name = "Tom Whittock", email = "tom.whittock@disguise.one" }, { name = "Taegyun Ha", email = "taegyun.ha@disguise.one" } From 8f70087118b469696fd27129c096c55dc18c2f6f Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 11:08:21 +0000 Subject: [PATCH 23/37] fix test --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 7b767d8..71e6d14 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -165,7 +165,7 @@ def test_mixed_parameters_missing_keyword_only(self, plugin): plugin._hostname = "localhost" plugin._port = 80 - with pytest.raises(TypeError, match="missing a required keyword-only argument"): + with pytest.raises(TypeError, match="missing a required*"): plugin.method_mixed(1, 2) def test_async_method_signature_validation(self, plugin): From cd8f8808aa09fdddb6589ba6620aeb69dce22fa0 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 11:28:34 +0000 Subject: [PATCH 24/37] fix type information being removed --- src/designer_plugin/d3sdk/client.py | 16 ++++++++-------- src/designer_plugin/d3sdk/function.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 8b90c5e..4cbec2b 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -93,7 +93,7 @@ def create_d3_plugin_method_wrapper( if inspect.iscoroutinefunction(original_method): # Create async wrapper that uses async Designer API call @functools.wraps(original_method) - async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + async def async_wrapper(self, *args, **kwargs): # type: ignore positional, keyword = validate_and_extract_args( sig, True, (self,) + args, kwargs ) @@ -107,7 +107,7 @@ async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: else: # Create sync wrapper that uses synchronous Designer API call @functools.wraps(original_method) - def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + def sync_wrapper(self, *args, **kwargs): # type: ignore positional, keyword = validate_and_extract_args( sig, True, (self,) + args, kwargs ) @@ -134,7 +134,7 @@ def create_d3_payload_wrapper( """ @functools.wraps(original_method) - def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> PluginPayload[T]: + def sync_wrapper(self, *args, **kwargs) -> PluginPayload[T]: # type: ignore return build_payload(self, method_name, args, kwargs) return sync_wrapper @@ -251,7 +251,7 @@ def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> t return super().__new__(cls, name, bases, attrs) - def __call__(cls, *args: Any, **kwargs: Any) -> Any: + def __call__(cls, *args, **kwargs): # type: ignore """Create an instance and generate its remote instantiation code. This method is called when a class instance is created (e.g., MyPlugin(...)). @@ -348,13 +348,13 @@ def in_session(self) -> bool: return bool(self._hostname) and bool(self._port) @asynccontextmanager - async def async_session( + async def async_session( # type: ignore self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None, - ) -> Any: + ): """Async context manager for plugin session with Designer. Args: @@ -383,13 +383,13 @@ async def async_session( logger.debug("Exiting D3PluginModule context") @contextmanager - def session( + def session( # type: ignore self, hostname: str, port: int = D3_PLUGIN_DEFAULT_PORT, register_module: bool = True, module_name: str | None = None, - ) -> Any: + ): """Sync context manager for plugin session with Designer. Args: diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index 662174a..5180682 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -321,7 +321,7 @@ def module_name(self) -> str: """ return self._module_name - def _args_to_string(self, *args: Any, **kwargs: Any) -> str: + def _args_to_string(self, *args, **kwargs) -> str: # type: ignore """Convert function arguments to a string representation for function call generation. Args: From 97419e484ae28906e7c553719dd041a2826c978d Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 11:58:23 +0000 Subject: [PATCH 25/37] fix args and kargs arguments --- src/designer_plugin/d3sdk/function.py | 11 +++++----- tests/test_core.py | 31 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index 5180682..8d775a7 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -100,9 +100,8 @@ def extract_function_info(func: Callable[..., Any]) -> FunctionInfo: body += ast.unparse(stmt) + "\n" # Extract function arguments - args: list[str] = [] - for arg in first_node.args.args: - args.append(arg.arg) + sig: inspect.Signature = inspect.signature(func) + args: list[str] = list(sig.parameters.keys()) first_node_py27 = convert_function_to_py27(first_node) source_code_py27: str = ast.unparse(first_node_py27) @@ -196,10 +195,10 @@ def _args_to_assign(self, *args: Any, **kwargs: Any) -> str: sig: inspect.Signature = inspect.signature(self._function) positional, keyword_args = validate_and_extract_args(sig, False, args, kwargs) - # Create assignment strings for positional arguments + # Create assignment strings for positional arguments using parameter names from signature + param_names = list(sig.parameters.keys()) assignments = [ - f"{self._function_info.args[i]}={repr(arg)}" - for i, arg in enumerate(positional) + f"{param_names[i]}={repr(arg)}" for i, arg in enumerate(positional) ] kwargs_parts = [f"{name}={repr(value)}" for name, value in keyword_args.items()] diff --git a/tests/test_core.py b/tests/test_core.py index 2a264af..e71ea9d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -133,6 +133,37 @@ def test_extract_info_complex_typed_function(self): # But should contain the variable assignment without type hint assert "data =" in info.source_code_py27 + def test_extract_info_function_with_varargs(self): + """Test that *args and **kwargs are correctly extracted. + + This test detects the issue where using AST (first_node.args.args) + only extracts regular positional arguments, missing *args and **kwargs. + The fix uses inspect.signature() to capture all parameter names. + """ + def function_with_varargs(a, b, *args, **kwargs): + return (a, b, args, kwargs) + + info = extract_function_info(function_with_varargs) + + assert info.name == "function_with_varargs" + # All parameters including *args and **kwargs should be captured + assert info.args == ["a", "b", "args", "kwargs"] + + def test_extract_info_function_with_keyword_only(self): + """Test that keyword-only arguments are correctly extracted. + + This test ensures that keyword-only arguments (after *) are included + in the extracted argument list. + """ + def function_with_kwonly(a, b, *, c, d=10): + return a + b + c + d + + info = extract_function_info(function_with_kwonly) + + assert info.name == "function_with_kwonly" + # All parameters including keyword-only arguments should be captured + assert info.args == ["a", "b", "c", "d"] + class TestD3Function: def test_d3_function_creation(self): From 117ba40caba5e7a015c104d7f21f2f30248a545a Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 12:28:47 +0000 Subject: [PATCH 26/37] add override_module_name --- src/designer_plugin/d3sdk/client.py | 10 +++-- tests/test_client.py | 59 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 4cbec2b..6c2a78e 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -338,6 +338,7 @@ def get_surface_uid(self, surface_name: str) -> dict[str, str]: def __init__(self) -> None: self._hostname: str | None = None self._port: int | None = None + self._override_module_name: str | None = None def in_session(self) -> bool: """Check if the client is currently in an active session. @@ -369,7 +370,7 @@ async def async_session( # type: ignore """ try: if module_name: - self.module_name = module_name + self._override_module_name = module_name self._hostname = hostname self._port = port @@ -380,6 +381,7 @@ async def async_session( # type: ignore finally: self._hostname = None self._port = None + self._override_module_name = None logger.debug("Exiting D3PluginModule context") @contextmanager @@ -404,7 +406,7 @@ def session( # type: ignore """ try: if module_name: - self.module_name = module_name + self._override_module_name = module_name self._hostname = hostname self._port = port @@ -415,6 +417,7 @@ def session( # type: ignore finally: self._hostname = None self._port = None + self._override_module_name = None logger.debug("Exiting D3PluginModule context") async def _aregister(self, hostname: str, port: int) -> None: @@ -451,7 +454,8 @@ def _get_register_module_payload(self) -> RegisterPayload: Returns: RegisterPayload containing moduleName and contents for registration. """ + module_name: str = self._override_module_name or self.module_name return RegisterPayload( - moduleName=self.module_name, + moduleName=module_name, contents=self._get_register_module_content(), ) diff --git a/tests/test_client.py b/tests/test_client.py index 71e6d14..fd9f8bd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -251,3 +251,62 @@ def test_func(self, a, b): with pytest.raises(TypeError): validate_and_extract_args(sig, True, (None, 1, 2, 3), {}) + + +class TestModuleNameOverride: + """Test suite for module_name override functionality.""" + + @pytest.fixture + def plugin(self): + """Create a test plugin instance.""" + return SignatureValidationPlugin("test_config") + + def test_override_module_name_in_session(self, plugin): + """Test that module_name parameter overrides the default in session.""" + with patch('designer_plugin.d3sdk.client.d3_api_register_module') as mock_register: + # Get the original module_name + original_module_name = plugin.module_name + + # Use session with a custom module name + with plugin.session("localhost", 80, register_module=True, module_name="CustomModule"): + # Verify the register was called with the overridden name + mock_register.assert_called_once() + call_args = mock_register.call_args + payload = call_args[0][2] # Third positional argument is the payload + assert payload.moduleName == "CustomModule" + assert payload.moduleName != original_module_name + + # After session ends, verify the override is cleared + assert plugin._override_module_name is None + # Verify the class module_name is unchanged + assert plugin.module_name == original_module_name + + def test_no_override_uses_default_module_name(self, plugin): + """Test that without module_name parameter, default module_name is used.""" + with patch('designer_plugin.d3sdk.client.d3_api_register_module') as mock_register: + original_module_name = plugin.module_name + + # Use session without custom module name + with plugin.session("localhost", 80, register_module=True): + mock_register.assert_called_once() + call_args = mock_register.call_args + payload = call_args[0][2] + assert payload.moduleName == original_module_name + + # Verify no override was set + assert plugin._override_module_name is None + + def test_override_cleared_on_exception(self, plugin): + """Test that module_name override is cleared even if an exception occurs.""" + with patch('designer_plugin.d3sdk.client.d3_api_register_module', side_effect=Exception("Test error")): + original_module_name = plugin.module_name + + # Use session with custom module name, expect exception + with pytest.raises(Exception, match="Test error"): + with plugin.session("localhost", 80, register_module=True, module_name="CustomModule"): + pass + + # Verify the override is cleared despite the exception + assert plugin._override_module_name is None + # Verify the class module_name is unchanged + assert plugin.module_name == original_module_name From 51289b229aec63198347a02dd93d39707fba8654 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Tue, 2 Dec 2025 12:43:15 +0000 Subject: [PATCH 27/37] raise RuntimeError if not in session --- src/designer_plugin/d3sdk/client.py | 20 +++++++++++++++++++- tests/test_client.py | 9 +++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 6c2a78e..afc8dbf 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -65,6 +65,16 @@ def build_payload( return PluginPayload[Any](moduleName=self.module_name, script=script) +def session_runtime_error_message(class_name: str) -> str: + return f"""\ +{class_name} is not in an active session. + +Usage: + with plugin.session('localhost', 80): + plugin.your_method() +""" + + def create_d3_plugin_method_wrapper( method_name: str, original_method: Callable[P, T] ) -> Callable[..., Any]: @@ -97,6 +107,10 @@ async def async_wrapper(self, *args, **kwargs): # type: ignore positional, keyword = validate_and_extract_args( sig, True, (self,) + args, kwargs ) + if not self.in_session(): + raise RuntimeError( + session_runtime_error_message(self.__class__.__name__) + ) payload = build_payload(self, method_name, positional, keyword) response: PluginResponse[T] = await d3_api_aplugin( self._hostname, self._port, payload @@ -111,6 +125,10 @@ def sync_wrapper(self, *args, **kwargs): # type: ignore positional, keyword = validate_and_extract_args( sig, True, (self,) + args, kwargs ) + if not self.in_session(): + raise RuntimeError( + session_runtime_error_message(self.__class__.__name__) + ) payload = build_payload(self, method_name, positional, keyword) response: PluginResponse[T] = d3_api_plugin( self._hostname, self._port, payload @@ -454,7 +472,7 @@ def _get_register_module_payload(self) -> RegisterPayload: Returns: RegisterPayload containing moduleName and contents for registration. """ - module_name: str = self._override_module_name or self.module_name + module_name: str = self._override_module_name or self.module_name # type: ignore[attr-defined] return RegisterPayload( moduleName=module_name, contents=self._get_register_module_content(), diff --git a/tests/test_client.py b/tests/test_client.py index fd9f8bd..b93738a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -60,6 +60,15 @@ def mock_response(self): returnValue=42 ) + def test_method_call_without_session_raises_error(self, plugin): + """Test that calling a method outside of a session raises RuntimeError.""" + # Verify plugin is not in session + assert not plugin.in_session() + + # Attempt to call a method without being in a session + with pytest.raises(RuntimeError, match="is not in.*session"): + plugin.simple_method(1, 2) + def test_correct_arguments_sync(self, plugin, mock_response): """Test that correct arguments pass through successfully.""" with patch('designer_plugin.d3sdk.client.d3_api_plugin', return_value=mock_response) as mock_api: From 985d887d6c37c91c5442d1da75e79f8c733202f3 Mon Sep 17 00:00:00 2001 From: Taegyun Ha <110908525+DevTGHa@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:24:14 +0000 Subject: [PATCH 28/37] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Peter Schübel --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8110396..b4df8d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.2.0] - 2024-12-02 +## [1.2.0] - 2025-12-02 ### Added - **Client API**: Metaclass-based remote method execution with `D3PluginClient` From 6674d831eb42f068cec91673902e7ef3f40f13fa Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 3 Dec 2025 09:21:29 +0000 Subject: [PATCH 29/37] remove all d3 implication in docstring --- README.md | 2 +- pyproject.toml | 3 ++- src/designer_plugin/d3sdk/ast_utils.py | 4 ++-- src/designer_plugin/d3sdk/client.py | 2 +- src/designer_plugin/d3sdk/function.py | 24 ++++++++++++------------ src/designer_plugin/models.py | 6 +++--- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3d9aeda..7cbaa5e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Python Version](https://img.shields.io/pypi/pyversions/designer-plugin.svg)](https://pypi.org/project/designer-plugin/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -A Python library for creating and managing plugins for Disguise Designer. This library provides: +A Python library for creating and managing plugins for [Disguise Designer]( https://www.disguise.one/en/products/designer). This library provides: - DNS-SD service publishing for plugin discovery - Remote Python execution on Designer instances - Multiple execution patterns (Client API, Functional API) diff --git a/pyproject.toml b/pyproject.toml index c7bb151..876ae82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,10 +20,10 @@ dependencies = [ requires-python = ">=3.11" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent" ] readme = "README.md" +license = "MIT" [tool.setuptools.package-data] designer_plugin = ["py.typed"] @@ -33,6 +33,7 @@ where = ["src"] [project.urls] Homepage = "https://github.com/disguise-one/python-plugin" +Documentation = "https://developer.disguise.one/plugins/python-execution-api/" Issues = "https://github.com/disguise-one/python-plugin/issues" # Package development diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py index 2581abb..50eb75a 100644 --- a/src/designer_plugin/d3sdk/ast_utils.py +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -50,7 +50,7 @@ def filter_base_classes(class_node: ast.ClassDef) -> None: """Remove all base classes from a class definition for Python 2.7 compatibility. This function modifies the class_node in-place by clearing its base class list. - Inheritance is not supported in the current D3 Designer plugin system. + Inheritance is not supported in the current Designer plugin system. Args: class_node: The class definition node to process @@ -368,7 +368,7 @@ def find_packages_in_current_file(caller_stack: int = 1) -> list[str]: This function walks up the call stack to find the module where it was called from, then parses that module's source code to extract all import statements that are - compatible with Python 2.7 and safe to send to D3 Designer. + compatible with Python 2.7 and safe to send to Designer. Args: caller_stack: Number of frames to go up the call stack. Default is 1 (immediate caller). diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index afc8dbf..9d9dea2 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -259,7 +259,7 @@ def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> t convert_class_to_py27(class_node) attrs["source_code_py27"] = f"{ast.unparse(class_node)}" - # Wrap all user-defined public methods to execute remotely via D3 API + # Wrap all user-defined public methods to execute remotely via Designer API # Skip internal framework methods for attr_name, attr_value in attrs.items(): if callable(attr_value) and not attr_name.startswith("__"): diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index 8d775a7..c417755 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -130,7 +130,7 @@ def __init__(self, func: Callable[P, T]): """Initialise a D3PythonScript wrapper around a Python function. Args: - func: The Python function to wrap for D3 execution. + func: The Python function to wrap for execution within Designer. """ self._function: Callable[P, T] = func self._function_info: FunctionInfo = extract_function_info(func) @@ -246,7 +246,7 @@ def __init__(self, module_name: str, func: Callable[P, T]): Args: module_name: Name of the module to register this function under. - func: The Python function to wrap for D3 execution. + func: The Python function to wrap for execution in Designer. """ # Add this function into available_d3_functions self._module_name: str = module_name @@ -371,10 +371,10 @@ def payload(self, *args: P.args, **kwargs: P.kwargs) -> PluginPayload[T]: ############################################################################### # d3function API def d3pythonscript(func: Callable[P, T]) -> D3PythonScript[P, T]: - """Decorator to wrap a Python function for standalone D3 Designer script execution. + """Decorator to wrap a Python function for standalone Designer script execution. This decorator transforms a regular Python function into a D3PythonScript that generates - execution payloads for direct script execution in D3 Designer. Unlike @d3function, this + execution payloads for direct script execution in Designer. Unlike @d3function, this decorator does not register the function as a module and is intended for one-off script execution where the function body is inlined with the arguments. @@ -405,10 +405,10 @@ def my_add(a: int, b: int) -> int: # Actual implementation def d3function(module_name: str = "") -> Callable[[Callable[P, T]], D3Function[P, T]]: - """Decorator to wrap a Python function for D3 Designer module registration and execution. + """Decorator to wrap a Python function for Designer module registration and execution. This decorator transforms a regular Python function into a D3Function that can be registered - as a reusable module in D3 Designer and executed remotely. The decorated function is added to + as a reusable module in Designer and executed remotely. The decorated function is added to the module's function registry and can be called by name after module registration. Unlike @d3pythonscript which inlines the function body, @d3function registers the complete @@ -448,15 +448,15 @@ def decorator(func: Callable[P, T]) -> D3Function[P, T]: def add_packages_in_current_file(module_name: str) -> None: - """Add all import statements from the caller's file to a D3 module's package list. + """Add all import statements from the caller's file to a d3function module's package list. This function scans the calling file's import statements and registers them with the specified module name, making those imports available when the module is registered with Designer. This is useful for ensuring all dependencies are included - when deploying Python functions to D3 Designer. + when deploying Python functions to Designer. Args: - module_name: The name of the D3 module to associate the packages with. + module_name: The name of the d3function module to associate the packages with. Must match the module_name used in @d3function decorator. Example: @@ -483,7 +483,7 @@ def get_register_payload(module_name: str) -> RegisterPayload | None: module_name: The name of the module to get the payload for. Returns: - RegisterPayload for the module, or None if the module has no registered functions. + RegisterPayload for the module, or None if the module has no registered d3function. """ return D3Function.get_module_register_payload(module_name) @@ -492,7 +492,7 @@ def get_all_d3functions() -> list[tuple[str, str]]: """Retrieve all available d3function as module_name-function_name pairs. Returns: - List of tuples containing (module_name, function_name) for all registered D3 functions. + List of tuples containing (module_name, function_name) for all registered d3function. """ module_function_pairs: list[tuple[str, str]] = [] for module_name, funcs in D3Function._available_d3functions.items(): @@ -506,6 +506,6 @@ def get_all_modules() -> list[str]: """Retrieve names of all modules registered with @d3function decorator. Returns: - List of module names that have registered D3 functions. + List of module names that have registered d3function. """ return list(D3Function._available_d3functions.keys()) diff --git a/src/designer_plugin/models.py b/src/designer_plugin/models.py index c67535c..2b608ed 100644 --- a/src/designer_plugin/models.py +++ b/src/designer_plugin/models.py @@ -48,11 +48,11 @@ class PluginResponse(BaseModel, Generic[RetType]): status: PluginStatus = Field(description="Status of plugin API call.") d3Log: str | None = Field( default=None, - description="The D3 log field captures the console log for the time the Python script is executing, recording any output generated by running a Python command, including the time taken to execute the Python command. As this captures the Designer console output, it is possible other threads may write to this output during this period causing additional irrelevant output.", + description="The d3Log field captures the Designer console log for the time the Python script is executing, recording any output generated by running a Python command, including the time taken to execute the Python command. As this captures the Designer console output, it is possible other threads may write to this output during this period causing additional irrelevant output.", ) pythonLog: str | None = Field( default=None, - description="This output field is the pure Python output, recording any print statements or warnings occurring during the execution of the script. This output will also appear in the d3Log field, however, it may be mixed with other application output.", + description="This output field is the pure Python output, recording any print statements or warnings that occur during the execution of the script. This output will also appear in the d3Log field. However, in the d3Log field it will be mixed with other Designer log output.", ) returnValue: RetType = Field(description="Return value of python plugin execution") @@ -112,7 +112,7 @@ class PluginException(Exception): Attributes: status: The status information from the failed plugin call - d3Log: D3 Designer console log output + d3Log: Designer console log output pythonLog: Python-specific log output """ From 53342f188ad7b364ca62450e581df4b8da849e15 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 3 Dec 2025 10:36:03 +0000 Subject: [PATCH 30/37] add support for args and kargs --- README.md | 2 +- src/designer_plugin/d3sdk/ast_utils.py | 7 ++ tests/test_client.py | 138 +++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cbaa5e..710472c 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ async with D3AsyncSession('localhost', 80, ["mymodule"]) as session: time: str = await session.rpc( rename_surface_get_time.payload("surface 1", "surface 2")) - # Use plugin() for full response with logs and status + # Use execute() for full response with logs and status from designer_plugin import PluginResponse response: PluginResponse = await session.execute( rename_surface_get_time.payload("surface 1", "surface 2")) diff --git a/src/designer_plugin/d3sdk/ast_utils.py b/src/designer_plugin/d3sdk/ast_utils.py index 50eb75a..696cad1 100644 --- a/src/designer_plugin/d3sdk/ast_utils.py +++ b/src/designer_plugin/d3sdk/ast_utils.py @@ -355,7 +355,14 @@ def validate_and_extract_args( if param_name in args_dict: if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): positional.append(args_dict[param_name]) + elif param.kind == param.VAR_POSITIONAL: + # Unpack *args into positional list + positional.extend(args_dict[param_name]) + elif param.kind == param.VAR_KEYWORD: + # Unpack **kwargs into keyword dict + keyword.update(args_dict[param_name]) else: + # KEYWORD_ONLY parameters keyword[param_name] = args_dict[param_name] return tuple(positional), keyword diff --git a/tests/test_client.py b/tests/test_client.py index b93738a..a195c2e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -261,6 +261,144 @@ def test_func(self, a, b): with pytest.raises(TypeError): validate_and_extract_args(sig, True, (None, 1, 2, 3), {}) + def test_var_positional_args_extraction(self): + """Test that *args are correctly unpacked into positional arguments.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b, *args): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None, 1, 2, 3, 4, 5), {}) + + assert positional == (1, 2, 3, 4, 5) + assert keyword == {} + + def test_var_positional_empty_args(self): + """Test that empty *args are handled correctly.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b, *args): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None, 1, 2), {}) + + assert positional == (1, 2) + assert keyword == {} + + def test_var_keyword_kwargs_extraction(self): + """Test that **kwargs are correctly unpacked into keyword arguments.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, **kwargs): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args( + sig, True, (None, 1), {'x': 10, 'y': 20, 'z': 30} + ) + + assert positional == (1,) + assert keyword == {'x': 10, 'y': 20, 'z': 30} + + def test_var_keyword_empty_kwargs(self): + """Test that empty **kwargs are handled correctly.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, **kwargs): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None, 1), {}) + + assert positional == (1,) + assert keyword == {} + + def test_mixed_args_and_kwargs(self): + """Test function with both *args and **kwargs.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b, *args, **kwargs): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args( + sig, True, (None, 1, 2, 3, 4), {'x': 10, 'y': 20} + ) + + assert positional == (1, 2, 3, 4) + assert keyword == {'x': 10, 'y': 20} + + def test_complex_signature_with_all_parameter_types(self): + """Test function with all parameter types: positional, *args, keyword-only, **kwargs.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b, *args, c, d=10, **kwargs): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args( + sig, True, (None, 1, 2, 3, 4), {'c': 5, 'd': 15, 'x': 100, 'y': 200} + ) + + # Positional should include a, b, and *args + assert positional == (1, 2, 3, 4) + # Keyword should include c, d (keyword-only), and **kwargs + assert keyword == {'c': 5, 'd': 15, 'x': 100, 'y': 200} + + def test_positional_only_with_var_positional(self): + """Test function with positional-only parameters and *args.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, b, /, *args): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args(sig, True, (None, 1, 2, 3, 4, 5), {}) + + assert positional == (1, 2, 3, 4, 5) + assert keyword == {} + + def test_var_positional_preserves_order(self): + """Test that *args values maintain their order in the positional list.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, *args): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args( + sig, True, (None, 'a', 'b', 'c', 'd', 'e'), {} + ) + + assert positional == ('a', 'b', 'c', 'd', 'e') + assert keyword == {} + + def test_var_keyword_with_keyword_only_params(self): + """Test that **kwargs works correctly with keyword-only parameters.""" + from designer_plugin.d3sdk.ast_utils import validate_and_extract_args + import inspect + + def test_func(self, a, *, b, c, **kwargs): + pass + + sig = inspect.signature(test_func) + positional, keyword = validate_and_extract_args( + sig, True, (None, 1), {'b': 2, 'c': 3, 'x': 10, 'y': 20} + ) + + assert positional == (1,) + assert keyword == {'b': 2, 'c': 3, 'x': 10, 'y': 20} + class TestModuleNameOverride: """Test suite for module_name override functionality.""" From ac3b3155a6ed8172ef4428af11eb2263823067aa Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 3 Dec 2025 10:45:08 +0000 Subject: [PATCH 31/37] fix module_name in build_payload --- src/designer_plugin/d3sdk/client.py | 16 ++- tests/test_client.py | 171 ++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 3 deletions(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 9d9dea2..e04777f 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -62,7 +62,7 @@ def build_payload( script = f"return plugin.{method_name}({all_args})" # Create payload containing script and module info - return PluginPayload[Any](moduleName=self.module_name, script=script) + return PluginPayload[Any](moduleName=self._get_module_name(), script=script) def session_runtime_error_message(class_name: str) -> str: @@ -458,6 +458,17 @@ def _register(self, hostname: str, port: int) -> None: """ d3_api_register_module(hostname, port, self._get_register_module_payload()) + def _get_module_name(self) -> str: + """Get the effective module name, considering session overrides. + + Returns the override module name if set during a session context, + otherwise returns the class's default module_name property. + + Returns: + The module name to use for registration and API calls. + """ + return self._override_module_name or self.module_name # type: ignore[attr-defined] + def _get_register_module_content(self) -> str: """Generate the complete module content to register with Designer. @@ -472,8 +483,7 @@ def _get_register_module_payload(self) -> RegisterPayload: Returns: RegisterPayload containing moduleName and contents for registration. """ - module_name: str = self._override_module_name or self.module_name # type: ignore[attr-defined] return RegisterPayload( - moduleName=module_name, + moduleName=self._get_module_name(), contents=self._get_register_module_content(), ) diff --git a/tests/test_client.py b/tests/test_client.py index a195c2e..a950bec 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -457,3 +457,174 @@ def test_override_cleared_on_exception(self, plugin): assert plugin._override_module_name is None # Verify the class module_name is unchanged assert plugin.module_name == original_module_name + + def test_get_module_name_returns_default_when_no_override(self, plugin): + """Test that _get_module_name returns default module_name when no override is set.""" + # Ensure no override is set + assert plugin._override_module_name is None + + # Get the original module_name + original_module_name = plugin.module_name + + # _get_module_name should return the default + assert plugin._get_module_name() == original_module_name + + def test_get_module_name_returns_override_when_set(self, plugin): + """Test that _get_module_name returns override when set.""" + original_module_name = plugin.module_name + override_name = "OverriddenModule" + + # Set an override + plugin._override_module_name = override_name + + # _get_module_name should return the override + assert plugin._get_module_name() == override_name + assert plugin._get_module_name() != original_module_name + + # Clean up + plugin._override_module_name = None + + def test_get_module_name_during_session_with_override(self, plugin): + """Test that _get_module_name returns override during session context.""" + with patch('designer_plugin.d3sdk.client.d3_api_register_module'): + original_module_name = plugin.module_name + override_name = "SessionModule" + + with plugin.session("localhost", 80, register_module=True, module_name=override_name): + # During session, _get_module_name should return the override + assert plugin._get_module_name() == override_name + assert plugin._get_module_name() != original_module_name + + # After session, should return to default + assert plugin._get_module_name() == original_module_name + + def test_get_module_name_during_session_without_override(self, plugin): + """Test that _get_module_name returns default during session without override.""" + with patch('designer_plugin.d3sdk.client.d3_api_register_module'): + original_module_name = plugin.module_name + + with plugin.session("localhost", 80, register_module=True): + # Without override, should return default module_name + assert plugin._get_module_name() == original_module_name + + # After session, should still return default + assert plugin._get_module_name() == original_module_name + + +class TestBuildPayload: + """Test suite for build_payload function.""" + + @pytest.fixture + def plugin(self): + """Create a test plugin instance.""" + return SignatureValidationPlugin("test_config") + + def test_build_payload_with_no_arguments(self, plugin): + """Test build_payload with method that takes no arguments.""" + from designer_plugin.d3sdk.client import build_payload + + payload = build_payload(plugin, "simple_method", (), {}) + + assert payload.moduleName == plugin.module_name + assert payload.script == "return plugin.simple_method()" + + def test_build_payload_with_positional_arguments(self, plugin): + """Test build_payload with positional arguments.""" + from designer_plugin.d3sdk.client import build_payload + + payload = build_payload(plugin, "simple_method", (1, 2), {}) + + assert payload.moduleName == plugin.module_name + assert payload.script == "return plugin.simple_method(1, 2)" + + def test_build_payload_with_keyword_arguments(self, plugin): + """Test build_payload with keyword arguments.""" + from designer_plugin.d3sdk.client import build_payload + + payload = build_payload(plugin, "method_keyword_only", (), {"name": "test", "value": 100}) + + assert payload.moduleName == plugin.module_name + assert payload.script == "return plugin.method_keyword_only(name='test', value=100)" + + def test_build_payload_with_mixed_arguments(self, plugin): + """Test build_payload with both positional and keyword arguments.""" + from designer_plugin.d3sdk.client import build_payload + + payload = build_payload(plugin, "method_mixed", (1, 2), {"c": "test"}) + + assert payload.moduleName == plugin.module_name + assert payload.script == "return plugin.method_mixed(1, 2, c='test')" + + def test_build_payload_with_string_arguments(self, plugin): + """Test build_payload correctly escapes string arguments.""" + from designer_plugin.d3sdk.client import build_payload + + payload = build_payload(plugin, "some_method", ("hello world",), {"key": "value with spaces"}) + + assert payload.moduleName == plugin.module_name + # repr() should properly quote strings + assert "plugin.some_method('hello world', key='value with spaces')" in payload.script + + def test_build_payload_with_various_types(self, plugin): + """Test build_payload handles various argument types.""" + from designer_plugin.d3sdk.client import build_payload + + payload = build_payload(plugin, "some_method", (42, 3.14, True, None, [1, 2, 3]), {}) + + assert payload.moduleName == plugin.module_name + assert payload.script == "return plugin.some_method(42, 3.14, True, None, [1, 2, 3])" + + def test_build_payload_uses_override_module_name(self, plugin): + """Test that build_payload uses override module name when set.""" + from designer_plugin.d3sdk.client import build_payload + + original_module_name = plugin.module_name + override_name = "OverriddenModule" + + # Set an override + plugin._override_module_name = override_name + + payload = build_payload(plugin, "simple_method", (), {}) + + # Should use the override, not the default + assert payload.moduleName == override_name + assert payload.moduleName != original_module_name + + # Clean up + plugin._override_module_name = None + + def test_build_payload_during_session_with_override(self, plugin): + """Test build_payload uses override module name during session.""" + from designer_plugin.d3sdk.client import build_payload + + with patch('designer_plugin.d3sdk.client.d3_api_register_module'): + override_name = "SessionModule" + + with plugin.session("localhost", 80, register_module=True, module_name=override_name): + payload = build_payload(plugin, "simple_method", (1, 2), {}) + + # Should use the session override + assert payload.moduleName == override_name + assert payload.script == "return plugin.simple_method(1, 2)" + + def test_build_payload_with_complex_nested_structures(self, plugin): + """Test build_payload handles nested data structures.""" + from designer_plugin.d3sdk.client import build_payload + + nested_data = {"key": [1, 2, {"inner": "value"}]} + payload = build_payload(plugin, "some_method", (nested_data,), {}) + + assert payload.moduleName == plugin.module_name + # repr() should handle nested structures + assert "plugin.some_method(" in payload.script + assert "'key': [1, 2, {'inner': 'value'}]" in payload.script + + def test_build_payload_method_name_preserved(self, plugin): + """Test that method names are correctly preserved in the script.""" + from designer_plugin.d3sdk.client import build_payload + + method_names = ["method1", "method_with_underscores", "methodCamelCase"] + + for method_name in method_names: + payload = build_payload(plugin, method_name, (), {}) + assert f"plugin.{method_name}()" in payload.script From 6725873a285daac1ba8b705d6497e363f094620f Mon Sep 17 00:00:00 2001 From: Taegyun Ha <110908525+DevTGHa@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:21:52 +0000 Subject: [PATCH 32/37] Update pyproject.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Peter Schübel --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 876ae82..4a53600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ where = ["src"] Homepage = "https://github.com/disguise-one/python-plugin" Documentation = "https://developer.disguise.one/plugins/python-execution-api/" Issues = "https://github.com/disguise-one/python-plugin/issues" +Sponsor = "https://www.disguise.one" # Package development [dependency-groups] From 12da4bbfe16c357eac7462fd10f946d8e873843e Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 3 Dec 2025 23:01:07 +0000 Subject: [PATCH 33/37] hide expensive logging under isEnableFor --- src/designer_plugin/api.py | 18 ++++-- src/designer_plugin/d3sdk/client.py | 12 ++-- src/designer_plugin/logger.py | 91 ++--------------------------- 3 files changed, 25 insertions(+), 96 deletions(-) diff --git a/src/designer_plugin/api.py b/src/designer_plugin/api.py index a681967..672d014 100644 --- a/src/designer_plugin/api.py +++ b/src/designer_plugin/api.py @@ -145,7 +145,8 @@ async def d3_api_aplugin( Raises: PluginException: If the plugin execution fails. """ - logger.debug(f"Send plugin api:{payload.debug_string()}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"Send plugin api:{payload.debug_string()}") response: Any = await d3_api_arequest( Method.POST, hostname, @@ -159,7 +160,8 @@ async def d3_api_aplugin( if plugin_response.pythonLog: print(plugin_response.pythonLog) - logger.debug(f"PluginResponse:{plugin_response.debug_string()}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"PluginResponse:{plugin_response.debug_string()}") return plugin_response except ValidationError: @@ -190,7 +192,8 @@ async def d3_api_aregister_module( PluginException: If module registration fails on Designer side. """ try: - logger.debug(f"Register module:{payload.debug_string()}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"Register module:{payload.debug_string()}") response: Any = await d3_api_arequest( Method.POST, hostname, @@ -237,7 +240,8 @@ def d3_api_plugin( PluginException: If the plugin execution fails. """ - logger.debug(f"Send plugin api:{payload.debug_string()}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"Send plugin api:{payload.debug_string()}") response = d3_api_request( Method.POST, hostname, @@ -252,7 +256,8 @@ def d3_api_plugin( if plugin_response.pythonLog: print(plugin_response.pythonLog) - logger.debug(f"PluginResponse:{plugin_response.debug_string()}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"PluginResponse:{plugin_response.debug_string()}") return plugin_response except ValidationError: @@ -286,7 +291,8 @@ def d3_api_register_module( PluginException: If module registration fails on Designer side. """ try: - logger.debug(f"Register module:{payload.debug_string()}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"Register module:{payload.debug_string()}") response: Any = d3_api_request( Method.POST, hostname, diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index e04777f..dcc2f09 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -394,13 +394,15 @@ async def async_session( # type: ignore self._port = port if register_module: await self._aregister(hostname, port) - logger.debug("Entering D3PluginModule context") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Entering D3PluginModule context") yield self finally: self._hostname = None self._port = None self._override_module_name = None - logger.debug("Exiting D3PluginModule context") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Exiting D3PluginModule context") @contextmanager def session( # type: ignore @@ -430,13 +432,15 @@ def session( # type: ignore self._port = port if register_module: self._register(hostname, port) - logger.debug("Entering D3PluginModule context") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Entering D3PluginModule context") yield self finally: self._hostname = None self._port = None self._override_module_name = None - logger.debug("Exiting D3PluginModule context") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Exiting D3PluginModule context") async def _aregister(self, hostname: str, port: int) -> None: """Register the plugin module with Designer asynchronously. diff --git a/src/designer_plugin/logger.py b/src/designer_plugin/logger.py index a801e52..2bd7a33 100644 --- a/src/designer_plugin/logger.py +++ b/src/designer_plugin/logger.py @@ -12,12 +12,8 @@ Or configure the designer_plugin logger specifically: - logging.getLogger('designer_plugin').setLevel(logging.DEBUG) - -For quick debugging, you can use the convenience function: - - from designer_plugin.logger import enable_debug_logging - enable_debug_logging() + from designer_plugin.logger import get_logger + get_logger().setLevel(logging.DEBUG) Internal Usage (for library developers): @@ -51,87 +47,10 @@ """ import logging -import sys -from typing import Any # Package root logger name LOGGER_NAME = "designer_plugin" -# Add NullHandler by default to prevent "No handler found" warnings -logging.getLogger(LOGGER_NAME).addHandler(logging.NullHandler()) - - -def enable_debug_logging( - level: int = logging.DEBUG, - stream: Any | None = None, - format_string: str | None = None, -) -> None: - """ - Enable debug logging for designer_plugin. - - For the production environments, it is advised to configure logging through - the application's logging configuration: - - i.e. `logging.basicConfig()` or `dictConfig()` - - Note: This will remove any existing handlers on the designer_plugin logger - to avoid duplicate log messages. - - Args: - level: Logging level (default: `logging.DEBUG`) - stream: Output stream (default: `sys.stderr`) - format_string: Custom format string for log messages - (default: `'%(asctime)s [%(name)s:%(levelname)s] %(message)s'`) - - Example: - ```python - from designer_plugin.logger import enable_debug_logging - enable_debug_logging() - - # With custom level - enable_debug_logging(level=logging.INFO) - - # With custom format - enable_debug_logging(format_string='[%(levelname)s] %(message)s') - ``` - """ - logger = logging.getLogger(LOGGER_NAME) - - # Remove existing handlers to avoid duplicates - # Keep the NullHandler removal to prevent accumulation - logger.handlers.clear() - - # Create and configure handler - handler = logging.StreamHandler(stream or sys.stderr) - handler.setLevel(level) - - # Create and set formatter - fmt = format_string or "%(asctime)s [%(name)s:%(levelname)s] %(message)s" - formatter = logging.Formatter(fmt) - handler.setFormatter(formatter) - - # Configure logger - logger.addHandler(handler) - logger.setLevel(level) - - # Prevent propagation to avoid duplicate logs if root logger is also configured - logger.propagate = False - - -def disable_logging() -> None: - """ - Disable all logging from designer_plugin. - - This removes all handlers and adds back the NullHandler. - Useful for silencing logs during testing or in production. - - Example: - ```python - from designer_plugin.logger import disable_logging - disable_logging() - ``` - """ - logger = logging.getLogger(LOGGER_NAME) - logger.handlers.clear() - logger.addHandler(logging.NullHandler()) - logger.setLevel(logging.NOTSET) - logger.propagate = True + +def get_logger() -> logging.Logger: + return logging.getLogger(LOGGER_NAME) From f458cf3a0dbb4ec71ddd83fe38f2a0085b993f88 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 3 Dec 2025 23:35:47 +0000 Subject: [PATCH 34/37] fix default argument for constructor --- src/designer_plugin/d3sdk/client.py | 43 ++--- tests/test_client.py | 265 ++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 26 deletions(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index dcc2f09..1317231 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -188,7 +188,6 @@ class D3PluginClientMeta(type): source_code: Python 3 source code with filtered variables source_code_py27: Python 2.7 compatible source code module_name: Name used to register the module with Designer - instance_code_template: Template string for instantiating the plugin remotely instance_code: Actual instantiation code with concrete argument values """ @@ -197,7 +196,6 @@ class D3PluginClientMeta(type): source_code: str source_code_py27: str module_name: str - instance_code_template: str instance_code: str def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type: @@ -243,16 +241,9 @@ def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> t # Filter out client-side-only __init__ arguments and get remaining params filtered_init_args: list[str] = filter_init_args(class_node) - formated_filtered_init_args: list[str] = [ - f"{{{arg}}}" for arg in filtered_init_args - ] # Unparse modified AST back to Python 3 source code (clean, no comments) attrs["source_code"] = f"{ast.unparse(class_node)}" - # Create template for instantiating the plugin remotely with placeholders - attrs["instance_code_template"] = ( - f"plugin = {name}({','.join(formated_filtered_init_args)})" - ) attrs["filtered_init_args"] = filtered_init_args # Convert async methods to Python 2.7 compatible sync methods @@ -273,8 +264,7 @@ def __call__(cls, *args, **kwargs): # type: ignore """Create an instance and generate its remote instantiation code. This method is called when a class instance is created (e.g., MyPlugin(...)). - It maps the provided arguments to the filtered parameter names and generates - the instance_code that will be used to instantiate the plugin remotely. + It builds an argument string for remote instantiation, respecting defaults. Args: *args: Positional arguments for the plugin __init__ @@ -283,23 +273,22 @@ def __call__(cls, *args, **kwargs): # type: ignore Returns: An instance of the plugin class with instance_code attribute set """ - # Build mapping from parameter names to their repr() values for remote instantiation + # Build argument string for remote instantiation, respecting defaults. param_names: list[str] = cls.filtered_init_args - arg_mapping: dict[str, str] = {} + arg_strings: list[str] = [] - # Map positional arguments for i, param_name in enumerate(param_names): - filtered_idx = i # Account for excluded client-side args - if filtered_idx < len(args): - arg_mapping[param_name] = repr(args[filtered_idx]) - - # Map keyword arguments that match filtered parameter names - for key, value in kwargs.items(): - if key in param_names: - arg_mapping[key] = repr(value) - - # Replace placeholders in template with actual serialized argument values - instance_code: str = cls.instance_code_template.format(**arg_mapping) + if i < len(args): + # Positional argument provided + arg_strings.append(repr(args[i])) + elif param_name in kwargs: + # Keyword argument provided + arg_strings.append(f"{param_name}={repr(kwargs[param_name])}") + else: + # Argument not provided -> rely on __init__ default on the remote side + continue + + instance_code: str = f"plugin = {cls.__name__}({', '.join(arg_strings)})" # Create the actual client instance with all original arguments instance = super().__call__(*args, **kwargs) @@ -471,7 +460,9 @@ def _get_module_name(self) -> str: Returns: The module name to use for registration and API calls. """ - return self._override_module_name or self.module_name # type: ignore[attr-defined] + if hasattr(self, "_override_module_name") and self._override_module_name: + return self._override_module_name + return self.module_name # type: ignore def _get_register_module_content(self) -> str: """Generate the complete module content to register with Designer. diff --git a/tests/test_client.py b/tests/test_client.py index a950bec..7289996 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -628,3 +628,268 @@ def test_build_payload_method_name_preserved(self, plugin): for method_name in method_names: payload = build_payload(plugin, method_name, (), {}) assert f"plugin.{method_name}()" in payload.script + + +class TestInstanceCodeGenerationWithDefaults: + """Test suite for instance_code generation with default parameters in __init__. + + This addresses the issue where D3PluginClientMeta.__call__ fails when __init__ + uses defaults or omitted filtered args, causing KeyError during .format(). + + The current implementation uses: + instance_code_template = "plugin = ClassName({arg1},{arg2})" + instance_code = instance_code_template.format(**arg_mapping) + + This raises KeyError when arg_mapping doesn't contain all placeholders. + + The fix should build the argument string directly from provided args/kwargs: + arg_strings = [repr(arg) for arg in provided_args] + instance_code = f"plugin = ClassName({', '.join(arg_strings)})" + """ + + def test_plugin_with_no_defaults_all_args_provided(self): + """Test plugin with no defaults when all arguments are provided.""" + class NoDefaultsPlugin(D3PluginClient): + def __init__(self, a: int, b: int): + super().__init__() + self.a = a + self.b = b + + def test_method(self) -> int: + return self.a + self.b + + # Create instance with all args - should work + plugin = NoDefaultsPlugin(1, 2) + + # Verify instance_code is generated correctly + assert hasattr(plugin, 'instance_code') + assert 'NoDefaultsPlugin' in plugin.instance_code + assert '1' in plugin.instance_code + assert '2' in plugin.instance_code + + def test_plugin_with_defaults_all_args_provided(self): + """Test plugin with defaults when all arguments are explicitly provided.""" + class DefaultsPlugin(D3PluginClient): + def __init__(self, a: int, b: int = 10, c: int = 20): + super().__init__() + self.a = a + self.b = b + self.c = c + + def test_method(self) -> int: + return self.a + self.b + self.c + + # Create instance with all args - should work + plugin = DefaultsPlugin(1, 2, 3) + + # Verify instance_code is generated correctly with all args + assert hasattr(plugin, 'instance_code') + assert 'DefaultsPlugin' in plugin.instance_code + # Should contain all provided values + assert '1' in plugin.instance_code + assert '2' in plugin.instance_code + assert '3' in plugin.instance_code + + def test_plugin_with_defaults_omitting_optional_args(self): + """Test plugin with defaults when optional arguments are omitted. + + This is the main issue: when a parameter with a default is omitted, + the current implementation raises KeyError during format(). + """ + class DefaultsPlugin(D3PluginClient): + def __init__(self, a: int, b: int = 10): + super().__init__() + self.a = a + self.b = b + + def test_method(self) -> int: + return self.a + self.b + + # Create instance with only required arg - should work with the fix + plugin = DefaultsPlugin(5) + + # Verify instance_code is generated correctly + assert hasattr(plugin, 'instance_code') + assert 'DefaultsPlugin' in plugin.instance_code + # Should only contain the provided argument + assert '5' in plugin.instance_code + # The instance_code should be valid Python that relies on the default + # With the fix, it should be something like: plugin = DefaultsPlugin(5) + # NOT: plugin = DefaultsPlugin({a}) which would fail .format() + + def test_plugin_with_multiple_defaults_partial_override(self): + """Test plugin with multiple defaults, overriding only some.""" + class MultiDefaultsPlugin(D3PluginClient): + def __init__(self, a: int, b: int = 10, c: int = 20, d: int = 30): + super().__init__() + self.a = a + self.b = b + self.c = c + self.d = d + + def test_method(self) -> int: + return self.a + self.b + self.c + self.d + + # Create instance with required + some optional args + plugin = MultiDefaultsPlugin(1, 15) + + # Verify instance_code is generated correctly + assert hasattr(plugin, 'instance_code') + assert 'MultiDefaultsPlugin' in plugin.instance_code + assert '1' in plugin.instance_code + assert '15' in plugin.instance_code + # c and d should rely on defaults and not appear in instance_code + + def test_plugin_with_defaults_using_keyword_args(self): + """Test plugin with defaults using keyword arguments.""" + class DefaultsPlugin(D3PluginClient): + def __init__(self, a: int, b: int = 10, c: int = 20): + super().__init__() + self.a = a + self.b = b + self.c = c + + def test_method(self) -> int: + return self.a + self.b + self.c + + # Create instance with keyword args, skipping middle default + plugin = DefaultsPlugin(1, c=30) + + # Verify instance_code is generated correctly + assert hasattr(plugin, 'instance_code') + assert 'DefaultsPlugin' in plugin.instance_code + assert '1' in plugin.instance_code + # Should include c as keyword arg + assert 'c=30' in plugin.instance_code or '30' in plugin.instance_code + + def test_plugin_with_all_defaults_using_positional(self): + """Test plugin where all parameters have defaults, using positional args.""" + class AllDefaultsPlugin(D3PluginClient): + def __init__(self, a: int = 1, b: int = 2, c: int = 3): + super().__init__() + self.a = a + self.b = b + self.c = c + + def test_method(self) -> int: + return self.a + self.b + self.c + + # Create instance with no args - all defaults + plugin = AllDefaultsPlugin() + + # Verify instance_code is generated correctly (should be empty args) + assert hasattr(plugin, 'instance_code') + assert 'AllDefaultsPlugin()' in plugin.instance_code + + def test_plugin_with_all_defaults_partial_override(self): + """Test plugin where all parameters have defaults, overriding some.""" + class AllDefaultsPlugin(D3PluginClient): + def __init__(self, a: int = 1, b: int = 2, c: int = 3): + super().__init__() + self.a = a + self.b = b + self.c = c + + def test_method(self) -> int: + return self.a + self.b + self.c + + # Create instance with one arg + plugin = AllDefaultsPlugin(10) + + # Verify instance_code is generated correctly + assert hasattr(plugin, 'instance_code') + assert 'AllDefaultsPlugin' in plugin.instance_code + assert '10' in plugin.instance_code + + def test_plugin_defaults_with_different_types(self): + """Test plugin with defaults of different types.""" + class TypedDefaultsPlugin(D3PluginClient): + def __init__(self, name: str, count: int = 5, active: bool = True): + super().__init__() + self.name = name + self.count = count + self.active = active + + def test_method(self) -> str: + return f"{self.name}: {self.count}" + + # Create instance with only required arg + plugin = TypedDefaultsPlugin("test") + + # Verify instance_code is generated correctly + assert hasattr(plugin, 'instance_code') + assert 'TypedDefaultsPlugin' in plugin.instance_code + assert "'test'" in plugin.instance_code + + def test_instance_code_execution_semantics(self): + """Test that generated instance_code has correct execution semantics. + + The generated code should let the remote side apply defaults, + not try to substitute them client-side. + """ + class SemanticsPlugin(D3PluginClient): + def __init__(self, x: int, y: int = 100): + super().__init__() + self.x = x + self.y = y + + def test_method(self) -> int: + return self.x + self.y + + # Test case 1: Only required arg + plugin1 = SemanticsPlugin(5) + # The instance_code should be: plugin = SemanticsPlugin(5) + # NOT: plugin = SemanticsPlugin(5, 100) - that would hardcode the default + assert 'SemanticsPlugin(5)' in plugin1.instance_code + assert '100' not in plugin1.instance_code # Default should not appear + + # Test case 2: Override the default + plugin2 = SemanticsPlugin(5, 200) + # The instance_code should include the override + assert 'SemanticsPlugin' in plugin2.instance_code + assert '5' in plugin2.instance_code + assert '200' in plugin2.instance_code + + def test_complex_defaults_scenario(self): + """Test complex scenario with mixed required and optional args.""" + class ComplexPlugin(D3PluginClient): + def __init__( + self, + required1: str, + required2: int, + optional1: str = "default1", + optional2: int = 42, + optional3: bool = False + ): + super().__init__() + self.required1 = required1 + self.required2 = required2 + self.optional1 = optional1 + self.optional2 = optional2 + self.optional3 = optional3 + + def test_method(self) -> str: + return f"{self.required1}_{self.required2}" + + # Test various combinations + + # All required only + plugin1 = ComplexPlugin("test", 10) + assert 'ComplexPlugin' in plugin1.instance_code + assert "'test'" in plugin1.instance_code + assert '10' in plugin1.instance_code + + # Required + some optional (positional) + plugin2 = ComplexPlugin("test", 10, "custom") + assert 'ComplexPlugin' in plugin2.instance_code + assert "'test'" in plugin2.instance_code + assert '10' in plugin2.instance_code + assert "'custom'" in plugin2.instance_code + + # Required + some optional (keyword) + plugin3 = ComplexPlugin("test", 10, optional2=99) + assert 'ComplexPlugin' in plugin3.instance_code + assert "'test'" in plugin3.instance_code + assert '10' in plugin3.instance_code + # Should include the keyword argument + assert '99' in plugin3.instance_code From d9a39ee03c94b20186e9e358242a449336ab35b9 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Fri, 5 Dec 2025 09:53:54 +0000 Subject: [PATCH 35/37] always intialise D3PluginClient --- src/designer_plugin/d3sdk/client.py | 33 +++++++- tests/test_client.py | 118 ++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 1317231..7c14d9e 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -158,6 +158,30 @@ def sync_wrapper(self, *args, **kwargs) -> PluginPayload[T]: # type: ignore return sync_wrapper +def create_init_wrapper(original_init: Callable[..., Any]) -> Callable[..., None]: + """Wrap user's __init__ to ensure parent initialisation is called. + + This ensures that even if the user forgets to call super().__init__(), + the required attributes (_hostname, _port, _override_module_name) are + still initialized, preventing AttributeError in methods like in_session(). + + Args: + original_init: The user-defined __init__ method. + + Returns: + Wrapped __init__ that calls parent initialisation first. + """ + + @functools.wraps(original_init) + def wrapper(self: Any, *args: Any, **kwargs: Any) -> None: + # Call parent's __init__ first to initialize required attributes + D3PluginClient.__init__(self) + # Then call user's __init__ + original_init(self, *args, **kwargs) + + return wrapper + + class D3PluginClientMeta(type): """Metaclass for Designer plugin clients that enables remote method execution. @@ -250,6 +274,11 @@ def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> t convert_class_to_py27(class_node) attrs["source_code_py27"] = f"{ast.unparse(class_node)}" + # Handle __init__ specially to ensure parent initialisation + # This prevents AttributeError when users forget to call super().__init__() + if "__init__" in attrs and callable(attrs["__init__"]): + attrs["__init__"] = create_init_wrapper(attrs["__init__"]) + # Wrap all user-defined public methods to execute remotely via Designer API # Skip internal framework methods for attr_name, attr_value in attrs.items(): @@ -460,9 +489,7 @@ def _get_module_name(self) -> str: Returns: The module name to use for registration and API calls. """ - if hasattr(self, "_override_module_name") and self._override_module_name: - return self._override_module_name - return self.module_name # type: ignore + return self._override_module_name or self.module_name # type: ignore def _get_register_module_content(self) -> str: """Generate the complete module content to register with Designer. diff --git a/tests/test_client.py b/tests/test_client.py index 7289996..2564b1d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -893,3 +893,121 @@ def test_method(self) -> str: assert '10' in plugin3.instance_code # Should include the keyword argument assert '99' in plugin3.instance_code + + +class TestInitWrapperSuperCall: + """Test suite for automatic parent __init__ calling when user forgets super().__init__().""" + + def test_plugin_without_super_init_has_required_attributes(self): + """Test that plugin works even without calling super().__init__().""" + class PluginWithoutSuper(D3PluginClient): + def __init__(self): + # Deliberately NOT calling super().__init__() + self.custom_attr = "test" + + def test_method(self) -> str: + return self.custom_attr + + plugin = PluginWithoutSuper() + + # These attributes should exist even without super().__init__() + assert hasattr(plugin, '_hostname') + assert hasattr(plugin, '_port') + assert hasattr(plugin, '_override_module_name') + assert hasattr(plugin, 'custom_attr') + + # Verify attribute values are properly initialized + assert plugin._hostname is None + assert plugin._port is None + assert plugin._override_module_name is None + assert plugin.custom_attr == "test" + + def test_in_session_works_without_super_init(self): + """Test that in_session() method works without super().__init__().""" + class PluginWithoutSuper(D3PluginClient): + def __init__(self): + self.a = "hey" + + def fn(self, a: int) -> int: + return a + + plugin = PluginWithoutSuper() + + # in_session() should not raise AttributeError + assert plugin.in_session() is False + + # After setting hostname and port + plugin._hostname = "localhost" + plugin._port = 80 + assert plugin.in_session() is True + + def test_plugin_with_super_init_still_works(self): + """Test that plugin still works when properly calling super().__init__().""" + class PluginWithSuper(D3PluginClient): + def __init__(self): + super().__init__() + self.custom_attr = "test" + + def test_method(self) -> str: + return self.custom_attr + + plugin = PluginWithSuper() + + # All attributes should be present + assert hasattr(plugin, '_hostname') + assert hasattr(plugin, '_port') + assert hasattr(plugin, '_override_module_name') + assert hasattr(plugin, 'custom_attr') + + # Verify attribute values + assert plugin._hostname is None + assert plugin._port is None + assert plugin._override_module_name is None + assert plugin.custom_attr == "test" + + def test_no_double_initialization_with_super_call(self): + """Test that calling super().__init__() doesn't cause double initialization.""" + initialization_count = [] + + class PluginWithSuper(D3PluginClient): + def __init__(self): + super().__init__() + initialization_count.append(1) + + def test_method(self) -> str: + return "test" + + plugin = PluginWithSuper() + + # Should have been initialized exactly once (by the user's __init__) + assert len(initialization_count) == 1 + + # Attributes should still be properly set + assert plugin._hostname is None + assert plugin._port is None + assert plugin._override_module_name is None + + def test_plugin_with_args_without_super_init(self): + """Test plugin with constructor arguments but no super().__init__() call.""" + class PluginWithArgs(D3PluginClient): + def __init__(self, value: int, name: str): + # Not calling super().__init__() + self.value = value + self.name = name + + def test_method(self) -> str: + return f"{self.name}={self.value}" + + plugin = PluginWithArgs(42, "test") + + # Required attributes should exist + assert hasattr(plugin, '_hostname') + assert hasattr(plugin, '_port') + assert hasattr(plugin, '_override_module_name') + + # Custom attributes should be set + assert plugin.value == 42 + assert plugin.name == "test" + + # in_session() should work + assert plugin.in_session() is False From 720a7bbb219833bae6481af11ab14794a5720701 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Fri, 5 Dec 2025 10:17:10 +0000 Subject: [PATCH 36/37] british english --- CONTRIBUTING.md | 4 ++-- src/designer_plugin/d3sdk/client.py | 6 +++--- src/designer_plugin/logger.py | 2 +- tests/test_client.py | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 113f899..9f5e34d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -231,8 +231,8 @@ class TestMyFeature: When reporting bugs, please include: - Description of the issue - Steps to reproduce -- Expected behavior -- Actual behavior +- Expected behaviour +- Actual behaviour - Python version - Package version - Minimal code example (if applicable) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 7c14d9e..800763c 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -163,7 +163,7 @@ def create_init_wrapper(original_init: Callable[..., Any]) -> Callable[..., None This ensures that even if the user forgets to call super().__init__(), the required attributes (_hostname, _port, _override_module_name) are - still initialized, preventing AttributeError in methods like in_session(). + still initialised, preventing AttributeError in methods like in_session(). Args: original_init: The user-defined __init__ method. @@ -174,7 +174,7 @@ def create_init_wrapper(original_init: Callable[..., Any]) -> Callable[..., None @functools.wraps(original_init) def wrapper(self: Any, *args: Any, **kwargs: Any) -> None: - # Call parent's __init__ first to initialize required attributes + # Call parent's __init__ first to initialise required attributes D3PluginClient.__init__(self) # Then call user's __init__ original_init(self, *args, **kwargs) @@ -489,7 +489,7 @@ def _get_module_name(self) -> str: Returns: The module name to use for registration and API calls. """ - return self._override_module_name or self.module_name # type: ignore + return self._override_module_name or self.module_name # type: ignore def _get_register_module_content(self) -> str: """Generate the complete module content to register with Designer. diff --git a/src/designer_plugin/logger.py b/src/designer_plugin/logger.py index 2bd7a33..186c596 100644 --- a/src/designer_plugin/logger.py +++ b/src/designer_plugin/logger.py @@ -38,7 +38,7 @@ Log messages will show their source module: - 2025-11-29 10:15:23 [designer_plugin.api:INFO] API initialized + 2025-11-29 10:15:23 [designer_plugin.api:INFO] API initialised 2025-11-29 10:15:24 [designer_plugin.d3sdk.client:DEBUG] Connecting to server 2025-11-29 10:15:25 [designer_plugin.models:INFO] Model loaded diff --git a/tests/test_client.py b/tests/test_client.py index 2564b1d..91f8ee3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -916,7 +916,7 @@ def test_method(self) -> str: assert hasattr(plugin, '_override_module_name') assert hasattr(plugin, 'custom_attr') - # Verify attribute values are properly initialized + # Verify attribute values are properly initialised assert plugin._hostname is None assert plugin._port is None assert plugin._override_module_name is None @@ -965,22 +965,22 @@ def test_method(self) -> str: assert plugin._override_module_name is None assert plugin.custom_attr == "test" - def test_no_double_initialization_with_super_call(self): - """Test that calling super().__init__() doesn't cause double initialization.""" - initialization_count = [] + def test_no_double_initialisation_with_super_call(self): + """Test that calling super().__init__() doesn't cause double initialisation.""" + initialisation_count = [] class PluginWithSuper(D3PluginClient): def __init__(self): super().__init__() - initialization_count.append(1) + initialisation_count.append(1) def test_method(self) -> str: return "test" plugin = PluginWithSuper() - # Should have been initialized exactly once (by the user's __init__) - assert len(initialization_count) == 1 + # Should have been initialised exactly once (by the user's __init__) + assert len(initialisation_count) == 1 # Attributes should still be properly set assert plugin._hostname is None From 9a9c3a2c8f1d0448317e5f774afe2feb4a0d5fd8 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Fri, 5 Dec 2025 10:46:30 +0000 Subject: [PATCH 37/37] fix for constructingn D3PluginClient --- src/designer_plugin/d3sdk/client.py | 5 ++ tests/test_client.py | 83 +++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/designer_plugin/d3sdk/client.py b/src/designer_plugin/d3sdk/client.py index 800763c..fcf8cc4 100644 --- a/src/designer_plugin/d3sdk/client.py +++ b/src/designer_plugin/d3sdk/client.py @@ -302,6 +302,11 @@ def __call__(cls, *args, **kwargs): # type: ignore Returns: An instance of the plugin class with instance_code attribute set """ + # Base class (and any non-instrumented subclasses) don't carry + # remote-instantiation metadata; fall back to normal construction. + if not hasattr(cls, "filtered_init_args"): + return super().__call__(*args, **kwargs) + # Build argument string for remote instantiation, respecting defaults. param_names: list[str] = cls.filtered_init_args arg_strings: list[str] = [] diff --git a/tests/test_client.py b/tests/test_client.py index 91f8ee3..b7c73f1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -895,6 +895,89 @@ def test_method(self) -> str: assert '99' in plugin3.instance_code +class TestMetaclassCallGuard: + """Test suite for metaclass __call__ guard for base/non-instrumented classes. + + This addresses the issue where D3PluginClientMeta.__new__ skips instrumentation + when name == "D3PluginClient", so the base class never gets filtered_init_args/ + instance_code. However, __call__ unconditionally reads cls.filtered_init_args, + causing AttributeError when instantiating D3PluginClient() directly or any + non-instrumented class. + """ + + def test_base_class_instantiation_without_args(self): + """Test that base D3PluginClient can be instantiated without arguments. + + This would raise AttributeError without the guard in __call__. + """ + # This should not raise AttributeError + plugin = D3PluginClient() + + # Verify the instance is created properly + assert isinstance(plugin, D3PluginClient) + assert hasattr(plugin, '_hostname') + assert hasattr(plugin, '_port') + assert hasattr(plugin, '_override_module_name') + + def test_base_class_does_not_have_instrumentation_attributes(self): + """Test that base D3PluginClient class lacks instrumentation attributes.""" + # The base class should not have these attributes + assert not hasattr(D3PluginClient, 'filtered_init_args') + assert not hasattr(D3PluginClient, 'source_code') + assert not hasattr(D3PluginClient, 'source_code_py27') + + def test_instrumented_subclass_has_attributes(self): + """Test that instrumented subclasses do have instrumentation attributes.""" + class InstrumentedPlugin(D3PluginClient): + def __init__(self, a: int): + super().__init__() + self.a = a + + def test_method(self) -> int: + return self.a + + # Instrumented subclasses should have these attributes + assert hasattr(InstrumentedPlugin, 'filtered_init_args') + assert hasattr(InstrumentedPlugin, 'source_code') + assert hasattr(InstrumentedPlugin, 'source_code_py27') + + def test_instrumented_subclass_instantiation_works(self): + """Test that instrumented subclasses can still be instantiated normally.""" + class InstrumentedPlugin(D3PluginClient): + def __init__(self, a: int, b: str = "default"): + super().__init__() + self.a = a + self.b = b + + def test_method(self) -> str: + return f"{self.a}:{self.b}" + + # This should work as before + plugin = InstrumentedPlugin(42, "test") + + assert isinstance(plugin, InstrumentedPlugin) + assert plugin.a == 42 + assert plugin.b == "test" + + # Should have instance_code generated + assert hasattr(plugin, 'instance_code') + assert 'InstrumentedPlugin' in plugin.instance_code + assert '42' in plugin.instance_code + + def test_base_class_instance_no_instance_code(self): + """Test that base D3PluginClient instance doesn't get instance_code. + + Since the base class bypasses instrumentation, instances shouldn't + have instance_code attribute (or it should be handled gracefully). + """ + plugin = D3PluginClient() + + # Base class instances shouldn't have instance_code + # (or if they do, it should be handled gracefully) + # The important thing is no AttributeError during instantiation + assert isinstance(plugin, D3PluginClient) + + class TestInitWrapperSuperCall: """Test suite for automatic parent __init__ calling when user forgets super().__init__()."""