From 24bea5b1762b3bf64fec727f65cdc0b8ae1bb91a Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Fri, 14 Nov 2025 16:55:45 +0000 Subject: [PATCH 01/11] Remove dependency on uv and numpy --- pyproject.toml | 14 +-- uv.lock | 226 ------------------------------------------------- 2 files changed, 8 insertions(+), 232 deletions(-) delete mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index 048ed1f..bdbdbb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,14 @@ authors = [ requires-python = ">=3.10" dependencies = [ "anyio>=4.10.0", - "numpy>=2.2.6", ] -[project.scripts] -rclpy-async = "rclpy_async:main" - [build-system] -requires = ["uv_build>=0.8.18,<0.9.0"] -build-backend = "uv_build" +requires = ["setuptools>=70", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 0944d60..0000000 --- a/uv.lock +++ /dev/null @@ -1,226 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.10" -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version < '3.11'", -] - -[[package]] -name = "anyio" -version = "4.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - -[[package]] -name = "numpy" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/45/e80d203ef6b267aa29b22714fb558930b27960a0c5ce3c19c999232bb3eb/numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", size = 21259253, upload-time = "2025-09-09T15:56:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/52/18/cf2c648fccf339e59302e00e5f2bc87725a3ce1992f30f3f78c9044d7c43/numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", size = 14450980, upload-time = "2025-09-09T15:56:05.926Z" }, - { url = "https://files.pythonhosted.org/packages/93/fb/9af1082bec870188c42a1c239839915b74a5099c392389ff04215dcee812/numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", size = 5379709, upload-time = "2025-09-09T15:56:07.95Z" }, - { url = "https://files.pythonhosted.org/packages/75/0f/bfd7abca52bcbf9a4a65abc83fe18ef01ccdeb37bfb28bbd6ad613447c79/numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", size = 6913923, upload-time = "2025-09-09T15:56:09.443Z" }, - { url = "https://files.pythonhosted.org/packages/79/55/d69adad255e87ab7afda1caf93ca997859092afeb697703e2f010f7c2e55/numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", size = 14589591, upload-time = "2025-09-09T15:56:11.234Z" }, - { url = "https://files.pythonhosted.org/packages/10/a2/010b0e27ddeacab7839957d7a8f00e91206e0c2c47abbb5f35a2630e5387/numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", size = 16938714, upload-time = "2025-09-09T15:56:14.637Z" }, - { url = "https://files.pythonhosted.org/packages/1c/6b/12ce8ede632c7126eb2762b9e15e18e204b81725b81f35176eac14dc5b82/numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", size = 16370592, upload-time = "2025-09-09T15:56:17.285Z" }, - { url = "https://files.pythonhosted.org/packages/b4/35/aba8568b2593067bb6a8fe4c52babb23b4c3b9c80e1b49dff03a09925e4a/numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", size = 18884474, upload-time = "2025-09-09T15:56:20.943Z" }, - { url = "https://files.pythonhosted.org/packages/45/fa/7f43ba10c77575e8be7b0138d107e4f44ca4a1ef322cd16980ea3e8b8222/numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57", size = 6599794, upload-time = "2025-09-09T15:56:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a2/a4f78cb2241fe5664a22a10332f2be886dcdea8784c9f6a01c272da9b426/numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa", size = 13088104, upload-time = "2025-09-09T15:56:25.476Z" }, - { url = "https://files.pythonhosted.org/packages/79/64/e424e975adbd38282ebcd4891661965b78783de893b381cbc4832fb9beb2/numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7", size = 10460772, upload-time = "2025-09-09T15:56:27.679Z" }, - { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, - { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, - { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, - { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, - { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload-time = "2025-09-09T15:56:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload-time = "2025-09-09T15:56:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload-time = "2025-09-09T15:56:56.541Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, - { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, - { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, - { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" }, - { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" }, - { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, - { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, - { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, - { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, - { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, - { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" }, - { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" }, - { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, - { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, - { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, - { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" }, - { url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, - { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, - { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, - { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, - { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" }, - { url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" }, - { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f2/7e0a37cfced2644c9563c529f29fa28acbd0960dde32ece683aafa6f4949/numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", size = 21131019, upload-time = "2025-09-09T15:58:42.838Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/3291f505297ed63831135a6cc0f474da0c868a1f31b0dd9a9f03a7a0d2ed/numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", size = 14376288, upload-time = "2025-09-09T15:58:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4b/ae02e985bdeee73d7b5abdefeb98aef1207e96d4c0621ee0cf228ddfac3c/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", size = 5305425, upload-time = "2025-09-09T15:58:48.6Z" }, - { url = "https://files.pythonhosted.org/packages/8b/eb/9df215d6d7250db32007941500dc51c48190be25f2401d5b2b564e467247/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", size = 6819053, upload-time = "2025-09-09T15:58:50.401Z" }, - { url = "https://files.pythonhosted.org/packages/57/62/208293d7d6b2a8998a4a1f23ac758648c3c32182d4ce4346062018362e29/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", size = 14420354, upload-time = "2025-09-09T15:58:52.704Z" }, - { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/af/11/0cc63f9f321ccf63886ac203336777140011fb669e739da36d8db3c53b98/numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", size = 12971844, upload-time = "2025-09-09T15:58:57.359Z" }, -] - -[[package]] -name = "rclpy-async" -version = "0.10" -source = { editable = "." } -dependencies = [ - { name = "anyio" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] - -[package.metadata] -requires-dist = [ - { name = "anyio", specifier = ">=4.10.0" }, - { name = "numpy", specifier = ">=2.2.6" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -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" }, -] From d18b699f2404a24b709606ba7b8606d69bae3be5 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 17 Nov 2025 15:14:47 +0000 Subject: [PATCH 02/11] Add async node with decorators for timer and subscription --- src/rclpy_async/_node_proto.py | 372 +++++++++++++++++++++++++++++++++ src/rclpy_async/async_node.py | 329 +++++++++++++++++++++++++++++ 2 files changed, 701 insertions(+) create mode 100644 src/rclpy_async/_node_proto.py create mode 100644 src/rclpy_async/async_node.py diff --git a/src/rclpy_async/_node_proto.py b/src/rclpy_async/_node_proto.py new file mode 100644 index 0000000..9003b59 --- /dev/null +++ b/src/rclpy_async/_node_proto.py @@ -0,0 +1,372 @@ +"""Abstract prototype for a ROS 2 Node. + +This file intentionally strips all implementation details from the original +`rclpy.node.Node` class and leaves only the public interface shape so it can be +used as a base (InnerProto) for delegation or inheritance without bringing in +ROS graph side-effects. Each method raises NotImplementedError and MUST be +implemented by a concrete subclass or provided via composition. + +Only a subset of the original interface is preserved here (the commonly used +APIs). Extend as needed by adding further stubs mirroring upstream signatures. +""" + +from __future__ import annotations + +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Protocol, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) + +from rcl_interfaces.msg import ParameterDescriptor, ParameterValue, SetParametersResult +from rclpy.callback_groups import CallbackGroup +from rclpy.client import Client +from rclpy.clock import Clock +from rclpy.context import Context +from rclpy.executors import Executor +from rclpy.guard_condition import GuardCondition +from rclpy.impl.rcutils_logger import RcutilsLogger +from rclpy.parameter import Parameter + +# ROS 2 interface types (mirroring rclpy.node.Node signatures) +from rclpy.publisher import Publisher +from rclpy.qos import QoSProfile +from rclpy.qos_event import PublisherEventCallbacks, SubscriptionEventCallbacks +from rclpy.qos_overriding_options import QoSOverridingOptions +from rclpy.service import Service +from rclpy.subscription import Subscription +from rclpy.timer import Rate, Timer +from rclpy.topic_endpoint_info import TopicEndpointInfo +from rclpy.waitable import Waitable + +# Type variables (kept for signature parity; concrete types resolved in subclasses) +MsgType = TypeVar("MsgType") +SrvType = TypeVar("SrvType") +SrvTypeRequest = TypeVar("SrvTypeRequest") +SrvTypeResponse = TypeVar("SrvTypeResponse") + + +class NodeProto(Protocol): + """Structural interface of a ROS 2 node (typing.Protocol). + + This protocol mirrors the public surface actually consumed by wrappers. + Concrete implementations (e.g. `rclpy.node.Node`) satisfy it implicitly. + Using a Protocol avoids inheritance side-effects and enables lightweight + delegation where unimplemented members simply fall through to the inner node. + """ + + # ---- Lifecycle / basic introspection ------------------------------------------------- + def __init__( + self, + node_name: str, + *, + context: Optional[Any] = None, + cli_args: Optional[List[str]] = None, + namespace: Optional[str] = None, + use_global_arguments: bool = True, + enable_rosout: bool = True, + start_parameter_services: bool = True, + parameter_overrides: Optional[List[Any]] = None, + allow_undeclared_parameters: bool = False, + automatically_declare_parameters_from_overrides: bool = False, + ) -> None: ... + + # ---- Entity collections --------------------------------------------------------------- + @property + def publishers(self) -> Iterator[Publisher]: ... + + @property + def subscriptions(self) -> Iterator[Subscription]: ... + + @property + def clients(self) -> Iterator[Client]: ... + + @property + def services(self) -> Iterator[Service]: ... + + @property + def timers(self) -> Iterator[Timer]: ... + + @property + def guards(self) -> Iterator[GuardCondition]: ... + + @property + def waitables(self) -> Iterator[Waitable]: ... + + # ---- Executor linkage ----------------------------------------------------------------- + @property + def executor(self) -> Optional[Executor]: ... + + @executor.setter + def executor(self, new_executor: Any) -> None: # type: ignore[override] + ... + + # ---- Core properties ------------------------------------------------------------------ + @property + def context(self) -> Context: ... + + @property + def default_callback_group(self) -> CallbackGroup: ... + + @property + def handle(self) -> Any: ... + + @handle.setter + def handle(self, value: Any) -> None: # type: ignore[override] + ... + + # ---- Introspection -------------------------------------------------------------------- + def get_name(self) -> str: ... + + def get_namespace(self) -> str: ... + + def get_clock(self) -> Clock: ... + + def get_logger(self) -> RcutilsLogger: ... + + # ---- Parameters ----------------------------------------------------------------------- + def declare_parameter( + self, + name: str, + value: Any = None, + descriptor: Optional[ParameterDescriptor] = None, + ignore_override: bool = False, + ) -> Parameter: ... + + def declare_parameters( + self, + namespace: str, + parameters: List[ + Union[ + Tuple[str], + Tuple[str, Parameter.Type], + Tuple[str, Any, ParameterDescriptor], + ] + ], + ignore_override: bool = False, + ) -> List[Parameter]: ... + + def undeclare_parameter(self, name: str) -> None: ... + + def has_parameter(self, name: str) -> bool: ... + + def get_parameter_types(self, names: List[str]) -> List[Parameter.Type]: ... + + def get_parameter_type(self, name: str) -> Parameter.Type: ... + + def get_parameters(self, names: List[str]) -> List[Parameter]: ... + + def get_parameter(self, name: str) -> Parameter: ... + + def get_parameter_or( + self, name: str, alternative_value: Optional[Parameter] = None + ) -> Parameter: ... + + def get_parameters_by_prefix( + self, + prefix: str, + ) -> Dict[ + str, + Optional[ + Union[ + bool, + int, + float, + str, + bytes, + Sequence[bool], + Sequence[int], + Sequence[float], + Sequence[str], + ] + ], + ]: ... + + def set_parameters( + self, parameter_list: List[Parameter] + ) -> List[SetParametersResult]: ... + + def set_parameters_atomically( + self, parameter_list: List[Parameter] + ) -> SetParametersResult: ... + + def add_on_set_parameters_callback( + self, callback: Callable[[List[Parameter]], SetParametersResult] + ) -> None: ... + + def remove_on_set_parameters_callback( + self, callback: Callable[[List[Parameter]], SetParametersResult] + ) -> None: ... + + def describe_parameter(self, name: str) -> ParameterDescriptor: ... + + def describe_parameters(self, names: List[str]) -> List[ParameterDescriptor]: ... + + def set_descriptor( + self, + name: str, + descriptor: ParameterDescriptor, + alternative_value: Optional[ParameterValue] = None, + ) -> ParameterValue: ... + + # ---- Name resolution ------------------------------------------------------------------ + def resolve_topic_name(self, topic: str, *, only_expand: bool = False) -> str: ... + + def resolve_service_name( + self, service: str, *, only_expand: bool = False + ) -> str: ... + + # ---- Creation of entities ------------------------------------------------------------- + def create_publisher( + self, + msg_type: Type[MsgType], + topic: str, + qos_profile: Union[QoSProfile, int], + *, + callback_group: Optional[CallbackGroup] = None, + event_callbacks: Optional[PublisherEventCallbacks] = None, + qos_overriding_options: Optional[QoSOverridingOptions] = None, + publisher_class: Type[Publisher] = Publisher, + ) -> Publisher: ... + + def create_subscription( + self, + msg_type: Type[MsgType], + topic: str, + callback: Callable[[MsgType], None], + qos_profile: Union[QoSProfile, int], + *, + callback_group: Optional[CallbackGroup] = None, + event_callbacks: Optional[SubscriptionEventCallbacks] = None, + qos_overriding_options: Optional[QoSOverridingOptions] = None, + raw: bool = False, + ) -> Subscription: ... + + def create_client( + self, + srv_type: Type[SrvType], + srv_name: str, + *, + qos_profile: QoSProfile = QoSProfile(depth=10), + callback_group: Optional[CallbackGroup] = None, + ) -> Client: ... + + def create_service( + self, + srv_type: Type[SrvType], + srv_name: str, + callback: Callable[[SrvTypeRequest, SrvTypeResponse], SrvTypeResponse], + *, + qos_profile: QoSProfile = QoSProfile(depth=10), + callback_group: Optional[CallbackGroup] = None, + ) -> Service: ... + + def create_timer( + self, + timer_period_sec: float, + callback: Callable, + callback_group: Optional[CallbackGroup] = None, + clock: Optional[Clock] = None, + ) -> Timer: ... + + def create_guard_condition( + self, + callback: Callable, + callback_group: Optional[CallbackGroup] = None, + ) -> GuardCondition: ... + + def create_rate( + self, + frequency: float, + clock: Optional[Clock] = None, + ) -> Rate: ... + + # ---- Destruction of entities ---------------------------------------------------------- + def destroy_publisher(self, publisher: Any) -> bool: ... + + def destroy_subscription(self, subscription: Any) -> bool: ... + + def destroy_client(self, client: Any) -> bool: ... + + def destroy_service(self, service: Any) -> bool: ... + + def destroy_timer(self, timer: Any) -> bool: ... + + def destroy_guard_condition(self, guard: Any) -> bool: ... + + def destroy_rate(self, rate: Any) -> bool: ... + + def destroy_node(self) -> None: ... + + # ---- Discovery / graph introspection -------------------------------------------------- + def get_publisher_names_and_types_by_node( + self, + node_name: str, + node_namespace: str, + no_demangle: bool = False, + ) -> List[Tuple[str, List[str]]]: ... + + def get_subscriber_names_and_types_by_node( + self, + node_name: str, + node_namespace: str, + no_demangle: bool = False, + ) -> List[Tuple[str, List[str]]]: ... + + def get_service_names_and_types_by_node( + self, + node_name: str, + node_namespace: str, + ) -> List[Tuple[str, List[str]]]: ... + + def get_client_names_and_types_by_node( + self, + node_name: str, + node_namespace: str, + ) -> List[Tuple[str, List[str]]]: ... + + def get_topic_names_and_types( + self, no_demangle: bool = False + ) -> List[Tuple[str, List[str]]]: ... + + def get_service_names_and_types(self) -> List[Tuple[str, List[str]]]: ... + + def get_node_names(self) -> List[str]: ... + + def get_node_names_and_namespaces(self) -> List[Tuple[str, str]]: ... + + def get_node_names_and_namespaces_with_enclaves( + self, + ) -> List[Tuple[str, str, str]]: ... + + def get_fully_qualified_name(self) -> str: ... + + # ---- Counting / endpoint info --------------------------------------------------------- + def count_publishers(self, topic_name: str) -> int: ... + + def count_subscribers(self, topic_name: str) -> int: ... + + def get_publishers_info_by_topic( + self, + topic_name: str, + no_mangle: bool = False, + ) -> List[TopicEndpointInfo]: ... + + def get_subscriptions_info_by_topic( + self, + topic_name: str, + no_mangle: bool = False, + ) -> List[TopicEndpointInfo]: ... + + # ---- Utility -------------------------------------------------------------------------- + def __repr__(self) -> str: # Helpful debug hook + return f"<{self.__class__.__name__} (Protocol)>" diff --git a/src/rclpy_async/async_node.py b/src/rclpy_async/async_node.py new file mode 100644 index 0000000..5258793 --- /dev/null +++ b/src/rclpy_async/async_node.py @@ -0,0 +1,329 @@ +import inspect +from dataclasses import dataclass +from typing import ( + Any, + Awaitable, + Callable, + Generic, + List, + Optional, + Type, + TypeVar, + Union, +) + +import anyio +import rclpy +from rclpy.node import Node +from rclpy.qos import QoSProfile + +import rclpy_async + +from ._node_proto import NodeProto + +# Public interface member names gathered from the Protocol; used for delegation. +_DELEGATE_NAMES = { + name + for name, member in vars(NodeProto).items() + if not name.startswith("_") + and (inspect.isfunction(member) or isinstance(member, property)) +} + + +def _is_overridden(cls: Type[Any], name: str) -> bool: + if not hasattr(NodeProto, name): + return True # Not part of proto; treat as overridden/local. + member_cls = getattr(cls, name, None) + member_proto = getattr(NodeProto, name, None) + if inspect.isfunction(member_cls) and inspect.isfunction(member_proto): + return member_cls is not member_proto + if isinstance(member_proto, property) and isinstance(member_cls, property): + return member_cls.fget is not member_proto.fget + return True + + +@dataclass +class TopicHandlerSpec: + """Specification for a subscription handler. + + async_fn signature: (message: MsgType) -> Awaitable[None] + """ + + msg_type: type + topic_name: str + qos_profile: QoSProfile + max_queue_size: int + drop_oldest: bool + async_fn: Callable[[Any], Awaitable[None]] + + +TParams = TypeVar("TParams") + + +@dataclass +class TimerHandlerSpec(Generic[TParams]): + """Specification for a timer handler. + + async_fn signature: () -> Awaitable[None] + timer_period_sec may be float or a callable taking params -> float. + """ + + timer_period_sec: Union[float, Callable[[TParams], float]] + max_queue_size: int + drop_oldest: bool + async_fn: Callable[[], Awaitable[None]] + + +class State: + """ + An object that can be used to store arbitrary state. + + Used for `request.state` and `app.state`. + """ + + _state: dict[str, Any] + + def __init__(self, state: dict[str, Any] | None = None): + if state is None: + state = {} + super().__setattr__("_state", state) + + def __setattr__(self, key: Any, value: Any) -> None: + self._state[key] = value + + def __getattr__(self, key: Any) -> Any: + try: + return self._state[key] + except KeyError: + message = "'{}' object has no attribute '{}'" + raise AttributeError(message.format(self.__class__.__name__, key)) + + def __delattr__(self, key: Any) -> None: + del self._state[key] + + +class AsyncNode(NodeProto): + """Asynchronous wrapper around a ROS 2 `Node` using dynamic delegation. + + Delegates all protocol-defined attributes/methods to `__inner` unless overridden. + Provides decorators for subscription and timer handlers executed with anyio. + """ + + __inner: Optional[Node] = None # Underlying rclpy Node instance + __node_name: str + __params_type: Optional[Type[TParams]] + __timer_handler_specs: List[TimerHandlerSpec[TParams]] = [] + __topic_handler_specs: List[TopicHandlerSpec] = [] + params: Optional[TParams] = None + state = State() # Arbitrary user state container + + def __init__(self, node_name: str, params_type: Type[TParams] = None): + self.__node_name = node_name + self.__params_type = params_type # Parameter schema type or None + + @property + def inner(self) -> Optional[Node]: + """Return underlying `Node` (read-only reference).""" + return self.__inner + + def initialize(self, **kwargs) -> None: + """Create underlying rclpy Node and declare parameters if a schema is provided.""" + if self.__inner is not None: + raise RuntimeError("AsyncNode already initialized") + self.__inner = rclpy.create_node(self.__node_name) + + if self.__params_type is not None: + self.__inner.declare_parameters( + namespace=( + kwargs.get("namespace") + if kwargs.get("namespace", None) is not None + else "" + ), + parameters=self.__params_type.as_parameters(), + ) + + self.params = self.__params_type.from_parameters(self.__inner.get_parameter) + self.__inner.get_logger().info(f"PubSub parameters: {self.params}") + + def __getattr__(self, name: str) -> Any: + """Delegate protocol members to inner node when not overridden locally.""" + inner = self.__inner + if ( + inner is not None + and name in _DELEGATE_NAMES + and not _is_overridden(type(self), name) + ): + return getattr(inner, name) + if inner is not None and hasattr(inner, name): + return getattr(inner, name) + raise AttributeError(name) + + def __setattr__(self, name: str, value: Any) -> None: + # Allow normal setting for our private / known attributes. + if ( + name.startswith("_AsyncNode__") + or name in {"params", "state"} + or hasattr(type(self), name) + ): + super().__setattr__(name, value) + return + inner = getattr(self, "_AsyncNode__inner", None) + if inner is not None and hasattr(inner, name): + setattr(inner, name, value) + else: + super().__setattr__(name, value) + + def __dir__(self) -> List[str]: + inner = self.__inner + names = set(super().__dir__()) + if inner is not None: + names.update(dir(inner)) + return sorted(names) + + def __repr__(self) -> str: + return f"" + + def subscription( + self, + msg_type: type, + topic_name: str, + qos_profile: QoSProfile = 10, + max_queue_size: int = 0, + drop_oldest: bool = False, + ) -> Callable[[Callable[[Any], Awaitable[None]]], Callable[[Any], Awaitable[None]]]: + """Decorator registering an async subscription handler.""" + + def _decorator(async_fn: Callable[[Any], Awaitable[None]]): + spec = TopicHandlerSpec( + msg_type=msg_type, + topic_name=topic_name, + qos_profile=qos_profile, + max_queue_size=max_queue_size, + drop_oldest=drop_oldest, + async_fn=async_fn, + ) + self.__topic_handler_specs.append(spec) + return async_fn + + return _decorator + + def timer( + self, + timer_period_sec: Union[float, Callable[[TParams], float]], + max_queue_size: int = 0, + drop_oldest: bool = False, + ) -> Callable[[Callable[[], Awaitable[None]]], Callable[[], Awaitable[None]]]: + """Decorator registering an async timer handler.""" + + def _decorator(async_fn: Callable[[], Awaitable[None]]): + spec = TimerHandlerSpec( + timer_period_sec=timer_period_sec, + max_queue_size=max_queue_size, + drop_oldest=drop_oldest, + async_fn=async_fn, + ) + self.__timer_handler_specs.append(spec) + return async_fn + + return _decorator + + async def spin(self) -> None: + """Start processing subscription and timer handlers until cancelled.""" + if self.__inner is None: + raise RuntimeError("AsyncNode not initialized; call initialize() first") + + async with anyio.create_task_group() as tg: + _attached_consumers = [] + + for spec in self.__topic_handler_specs: + async_fn = spec.async_fn # expects (ctx, msg) + msg_type = spec.msg_type + topic_name = spec.topic_name + qos_profile = spec.qos_profile + max_queue_size = spec.max_queue_size + drop_oldest = spec.drop_oldest + + send_stream, receive_stream = anyio.create_memory_object_stream( + max_queue_size + ) + + def _sub_callback(msg, *, _send=send_stream, _recv=receive_stream): + try: + _send.send_nowait(msg) + except anyio.WouldBlock: + if max_queue_size == 0: + return + if drop_oldest: + try: + _ = _recv.receive_nowait() + except anyio.WouldBlock: + return + try: + _send.send_nowait(msg) + except anyio.WouldBlock: + pass + + self.__inner.create_subscription( + msg_type, topic_name, _sub_callback, qos_profile=qos_profile + ) + + async def _consumer_task(fn=async_fn, _recv=receive_stream): + async for _msg in _recv: + await fn(_msg) + + _attached_consumers.append(_consumer_task) + + for spec in self.__timer_handler_specs: + async_fn = spec.async_fn # expects (ctx) + timer_period_sec = spec.timer_period_sec + if callable(timer_period_sec): + try: + period_value = float(timer_period_sec(self.params)) + except Exception: + period_value = 1.0 + else: + period_value = float(timer_period_sec) + max_queue_size = spec.max_queue_size + drop_oldest = spec.drop_oldest + + send_stream, receive_stream = anyio.create_memory_object_stream( + max_queue_size + ) + + def _timer_callback(_send=send_stream, _recv=receive_stream): + try: + _send.send_nowait(None) + except anyio.WouldBlock: + if max_queue_size == 0: + return + if drop_oldest: + try: + _ = _recv.receive_nowait() + except anyio.WouldBlock: + return + try: + _send.send_nowait(None) + except anyio.WouldBlock: + pass + + self.__inner.create_timer(period_value, _timer_callback) + + async def _consumer_task(fn=async_fn, _recv=receive_stream): + async for _ in _recv: + await fn() + + _attached_consumers.append(_consumer_task) + + for consumer_task in _attached_consumers: + tg.start_soon(consumer_task) + + # Sleep forever; cancellation of the task group stops processing. + await anyio.sleep(float("inf")) + + async def spin_one(self) -> None: + """Run a single executor managing this node and spin handlers concurrently.""" + if self.__inner is None: + raise RuntimeError("AsyncNode not initialized; call initialize() first") + async with rclpy_async.start_executor() as xtor: + xtor.add_node(self.__inner) + await self.spin() From fcc1f616ff741241e0955eaae95453b1ecab132c Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 17 Nov 2025 15:29:52 +0000 Subject: [PATCH 03/11] Fix delegation --- src/rclpy_async/async_node.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/rclpy_async/async_node.py b/src/rclpy_async/async_node.py index 5258793..636e17d 100644 --- a/src/rclpy_async/async_node.py +++ b/src/rclpy_async/async_node.py @@ -158,6 +158,22 @@ def __getattr__(self, name: str) -> Any: return getattr(inner, name) raise AttributeError(name) + def __getattribute__(self, name: str) -> Any: + """Attribute access with delegation for Protocol stub members.""" + # Fast-path for private/internal attributes to avoid recursion. + if name.startswith("_AsyncNode__"): + return super().__getattribute__(name) + + inner = super().__getattribute__("_AsyncNode__inner") + # Delegate protocol-defined members unless overridden in AsyncNode. + if ( + inner is not None + and name in _DELEGATE_NAMES + and not _is_overridden(type(self), name) + ): + return getattr(inner, name) + return super().__getattribute__(name) + def __setattr__(self, name: str, value: Any) -> None: # Allow normal setting for our private / known attributes. if ( From abaa1ea6743a311b7ae869c8610aaab9d02cbf0e Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Tue, 18 Nov 2025 13:50:22 +0000 Subject: [PATCH 04/11] Add example showing how to use async node --- .devcontainer/devcontainer.json | 3 +- examples/async_node.py | 92 +++++++++++++++++++++++++++++++++ pyproject.toml | 1 + src/rclpy_async/async_node.py | 26 +++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 examples/async_node.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 23a94af..8204072 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,9 @@ { - "name": "ROS2 with uv", + "name": "ROS2", "build": { "dockerfile": "Dockerfile" }, "features": { - "ghcr.io/devcontainers-extra/features/uv:latest": {}, "ghcr.io/devcontainers/features/git-lfs:latest": {} } } \ No newline at end of file diff --git a/examples/async_node.py b/examples/async_node.py new file mode 100644 index 0000000..86c7fbd --- /dev/null +++ b/examples/async_node.py @@ -0,0 +1,92 @@ +"""AsyncNode example demonstrating parameter schema, subscription, and timer. + +This example shows how to: +1. Define a `ParameterSchema` (here `NodeCLIArgs`) to declare and retrieve ROS 2 + parameters via the `AsyncNode` abstraction. +2. Register an asynchronous subscription handler using `@node.subscription` that + logs incoming `std_msgs/msg/String` messages on the `chatter` topic. +3. Register an asynchronous timer via `@node.timer`, whose period is computed + dynamically from declared parameters, publishing incrementing counter messages + on the `timer_event` topic. +4. Use `node.state` for mutable runtime state shared across handlers (counter and + publisher reference). + +Run this file and in another shell publish test data: + ros2 topic pub /chatter std_msgs/msg/String '{data: "Hello World"}' --once +Observe timer events: + ros2 topic echo /timer_event std_msgs/msg/String +""" + +from dataclasses import dataclass +from typing import Callable + +import anyio +import rclpy +from rclpy.parameter import Parameter +from std_msgs.msg import String + +from rclpy_async.async_node import AsyncNode, ParameterSchema + + +@dataclass +class NodeCLIArgs(ParameterSchema): + timed_event_counter: int = 0 + timed_event_periodicity: float = 2.0 + + def as_parameters(self): + return [ + ("timed_event_counter", self.timed_event_counter), + ("timed_event_periodicity", self.timed_event_periodicity), + ] + + @classmethod + def from_parameters(cls, get_parameter: Callable[[str], Parameter]): + print( + get_parameter("timed_event_counter").get_parameter_value().integer_value, + get_parameter("timed_event_periodicity").get_parameter_value().double_value, + ) + return cls( + timed_event_counter=get_parameter("timed_event_counter") + .get_parameter_value() + .integer_value, + timed_event_periodicity=get_parameter("timed_event_periodicity") + .get_parameter_value() + .double_value, + ) + + +node = AsyncNode("mynode", NodeCLIArgs) + + +@node.subscription(String, "chatter") +async def chatter_callback(msg): + node.get_logger().info(f"I heard: {msg.data}") + + +@node.timer(lambda params: params.timed_event_periodicity) +async def timer_callback(): + msg = String(data=f"Timer event {node.state.timed_event_counter}") + node.state.timed_event_counter += 1 + node.state.publisher_.publish(msg) + + +async def main(): + rclpy.init() + node.initialize() + + node.state.timed_event_counter = node.params.timed_event_counter + node.state.publisher_ = node.create_publisher(String, "timer_event", 10) + + print( + "Node started. Listening on 'chatter/in' and publishing to 'chatter/out'.\n" + + "To test, you can publish messages using:\n" + + "\tros2 topic pub /chatter std_msgs/msg/String '{data: \"Hello World\"}' --once\n" + + "You should see the messages being echoed back on 'chatter/out'.\n" + + "To see the published messages, you can subscribe using:\n" + + "\tros2 topic echo /timer_event std_msgs/msg/String\n" + + "Press Ctrl+C to stop the node.\n" + ) + await node.spin_one() + + +anyio.run(main) diff --git a/pyproject.toml b/pyproject.toml index bdbdbb8..1893ae2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ authors = [ requires-python = ">=3.10" dependencies = [ "anyio>=4.10.0", + "numpy>=2.2.6", ] [build-system] diff --git a/src/rclpy_async/async_node.py b/src/rclpy_async/async_node.py index 636e17d..601e950 100644 --- a/src/rclpy_async/async_node.py +++ b/src/rclpy_async/async_node.py @@ -1,5 +1,6 @@ import inspect from dataclasses import dataclass +from abc import ABC, abstractmethod from typing import ( Any, Awaitable, @@ -57,7 +58,28 @@ class TopicHandlerSpec: async_fn: Callable[[Any], Awaitable[None]] -TParams = TypeVar("TParams") +class ParameterSchema(ABC): + """Abstract base class for node parameter schema used by `AsyncNode`. + + Subclasses must provide a method `as_parameters` returning an iterable + of parameter declarations accepted by `Node.declare_parameters`, and a + classmethod `from_parameters` that constructs and returns an instance of + the schema populated from a getter callable (e.g. `node.get_parameter`). + """ + + @abstractmethod + def as_parameters(cls) -> list[tuple[str, Any]]: + """Return list of (name, value) tuples for declaration.""" + raise NotImplementedError + + @classmethod + @abstractmethod + def from_parameters(cls, get_parameter: Callable[[str], Any]) -> "ParameterSchema": + """Construct instance from declared parameters.""" + raise NotImplementedError + + +TParams = TypeVar("TParams", bound=ParameterSchema) @dataclass @@ -139,7 +161,7 @@ def initialize(self, **kwargs) -> None: if kwargs.get("namespace", None) is not None else "" ), - parameters=self.__params_type.as_parameters(), + parameters=self.__params_type().as_parameters(), ) self.params = self.__params_type.from_parameters(self.__inner.get_parameter) From fe61ac0eb37df9459f4615d230a9f9dabe4ec096 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Wed, 19 Nov 2025 11:31:44 +0000 Subject: [PATCH 05/11] Pushing default tutorial implementations for comparison --- examples/async_node/README.md | 45 ++++++++++++++++++++++++++ examples/async_node/action_client.py | 33 +++++++++++++++++++ examples/async_node/action_server.py | 47 ++++++++++++++++++++++++++++ examples/async_node/client.py | 40 +++++++++++++++++++++++ examples/async_node/param.py | 32 +++++++++++++++++++ examples/async_node/publisher.py | 38 ++++++++++++++++++++++ examples/async_node/service.py | 32 +++++++++++++++++++ examples/async_node/subscriber.py | 36 +++++++++++++++++++++ 8 files changed, 303 insertions(+) create mode 100644 examples/async_node/README.md create mode 100644 examples/async_node/action_client.py create mode 100644 examples/async_node/action_server.py create mode 100644 examples/async_node/client.py create mode 100644 examples/async_node/param.py create mode 100644 examples/async_node/publisher.py create mode 100644 examples/async_node/service.py create mode 100644 examples/async_node/subscriber.py diff --git a/examples/async_node/README.md b/examples/async_node/README.md new file mode 100644 index 0000000..2941878 --- /dev/null +++ b/examples/async_node/README.md @@ -0,0 +1,45 @@ +# Async Node samples + +The following samples are based off the ROS2 documented [tutorials](https://docs.ros.org/en/humble/Tutorials.html). + +## Usage + +*All scripts assume terminals are open in the root of this repository and have installed `rclpy_async`* + +A simple publisher and subscriber: +```sh +# Terminal 1: +python3 ./examples/async_node/subscriber.py + +# Terminal 2: +python3 ./examples/async_node/publisher.py +``` + +A simple service and client: +```sh +# Terminal 1: +python3 ./examples/async_node/service.py + +# Terminal 2: +python3 ./examples/async_node/client.py 1 2 +``` + +Using parameters in a class: +```sh +# Start the node +python3 ./examples/async_node/param.py +# [Optionally] start the node with the parameter as something other that "world" by adding: +python3 ./examples/async_node/param.py --ros-args -p my_parameter:=world2 + +# In another terminal set the parameter again +ros2 param set /minimal_param_node my_parameter earth +``` + +An action server and client: +```sh +# Terminal 1: +python3 ./examples/async_node/action_server.py + +# Terminal 2: +python3 ./examples/async_node/action_client.py +``` diff --git a/examples/async_node/action_client.py b/examples/async_node/action_client.py new file mode 100644 index 0000000..19c9292 --- /dev/null +++ b/examples/async_node/action_client.py @@ -0,0 +1,33 @@ +import rclpy +from example_interfaces.action import Fibonacci +from rclpy.action import ActionClient +from rclpy.node import Node + + +class FibonacciActionClient(Node): + + def __init__(self): + super().__init__("fibonacci_action_client") + self._action_client = ActionClient(self, Fibonacci, "fibonacci") + + def send_goal(self, order): + goal_msg = Fibonacci.Goal() + goal_msg.order = order + + self._action_client.wait_for_server() + + return self._action_client.send_goal_async(goal_msg) + + +def main(args=None): + rclpy.init(args=args) + + action_client = FibonacciActionClient() + + future = action_client.send_goal(10) + + rclpy.spin_until_future_complete(action_client, future) + + +if __name__ == "__main__": + main() diff --git a/examples/async_node/action_server.py b/examples/async_node/action_server.py new file mode 100644 index 0000000..e096652 --- /dev/null +++ b/examples/async_node/action_server.py @@ -0,0 +1,47 @@ +import time + +import rclpy +from example_interfaces.action import Fibonacci +from rclpy.action import ActionServer +from rclpy.node import Node + + +class FibonacciActionServer(Node): + + def __init__(self): + super().__init__("fibonacci_action_server") + self._action_server = ActionServer( + self, Fibonacci, "fibonacci", self.execute_callback + ) + + def execute_callback(self, goal_handle): + self.get_logger().info("Executing goal...") + + feedback_msg = Fibonacci.Feedback() + feedback_msg.sequence = [0, 1] + + for i in range(1, goal_handle.request.order): + feedback_msg.sequence.append( + feedback_msg.sequence[i] + feedback_msg.sequence[i - 1] + ) + self.get_logger().info("Feedback: {0}".format(feedback_msg.sequence)) + goal_handle.publish_feedback(feedback_msg) + time.sleep(1) + + goal_handle.succeed() + + result = Fibonacci.Result() + result.sequence = feedback_msg.sequence + return result + + +def main(args=None): + rclpy.init(args=args) + + fibonacci_action_server = FibonacciActionServer() + + rclpy.spin(fibonacci_action_server) + + +if __name__ == "__main__": + main() diff --git a/examples/async_node/client.py b/examples/async_node/client.py new file mode 100644 index 0000000..f4ef0f2 --- /dev/null +++ b/examples/async_node/client.py @@ -0,0 +1,40 @@ +import sys + +import rclpy +from example_interfaces.srv import AddTwoInts +from rclpy.node import Node + + +class MinimalClientAsync(Node): + + def __init__(self): + super().__init__("minimal_client_async") + self.cli = self.create_client(AddTwoInts, "add_two_ints") + while not self.cli.wait_for_service(timeout_sec=1.0): + self.get_logger().info("service not available, waiting again...") + self.req = AddTwoInts.Request() + + def send_request(self, a, b): + self.req.a = a + self.req.b = b + return self.cli.call_async(self.req) + + +def main(): + rclpy.init() + + minimal_client = MinimalClientAsync() + future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2])) + rclpy.spin_until_future_complete(minimal_client, future) + response = future.result() + minimal_client.get_logger().info( + "Result of add_two_ints: for %d + %d = %d" + % (int(sys.argv[1]), int(sys.argv[2]), response.sum) + ) + + minimal_client.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/examples/async_node/param.py b/examples/async_node/param.py new file mode 100644 index 0000000..9afe31b --- /dev/null +++ b/examples/async_node/param.py @@ -0,0 +1,32 @@ +import rclpy +import rclpy.node + + +class MinimalParam(rclpy.node.Node): + def __init__(self): + super().__init__("minimal_param_node") + + self.declare_parameter("my_parameter", "world") + + self.timer = self.create_timer(1, self.timer_callback) + + def timer_callback(self): + my_param = self.get_parameter("my_parameter").get_parameter_value().string_value + + self.get_logger().info("Hello %s!" % my_param) + + my_new_param = rclpy.parameter.Parameter( + "my_parameter", rclpy.Parameter.Type.STRING, "world" + ) + all_new_parameters = [my_new_param] + self.set_parameters(all_new_parameters) + + +def main(): + rclpy.init() + node = MinimalParam() + rclpy.spin(node) + + +if __name__ == "__main__": + main() diff --git a/examples/async_node/publisher.py b/examples/async_node/publisher.py new file mode 100644 index 0000000..686579e --- /dev/null +++ b/examples/async_node/publisher.py @@ -0,0 +1,38 @@ +import rclpy +from rclpy.node import Node +from std_msgs.msg import String + + +class MinimalPublisher(Node): + + def __init__(self): + super().__init__("minimal_publisher") + self.publisher_ = self.create_publisher(String, "topic", 10) + timer_period = 0.5 # seconds + self.timer = self.create_timer(timer_period, self.timer_callback) + self.i = 0 + + def timer_callback(self): + msg = String() + msg.data = "Hello World: %d" % self.i + self.publisher_.publish(msg) + self.get_logger().info('Publishing: "%s"' % msg.data) + self.i += 1 + + +def main(args=None): + rclpy.init(args=args) + + minimal_publisher = MinimalPublisher() + + rclpy.spin(minimal_publisher) + + # Destroy the node explicitly + # (optional - otherwise it will be done automatically + # when the garbage collector destroys the node object) + minimal_publisher.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/examples/async_node/service.py b/examples/async_node/service.py new file mode 100644 index 0000000..eeb4887 --- /dev/null +++ b/examples/async_node/service.py @@ -0,0 +1,32 @@ +import rclpy +from example_interfaces.srv import AddTwoInts +from rclpy.node import Node + + +class MinimalService(Node): + + def __init__(self): + super().__init__("minimal_service") + self.srv = self.create_service( + AddTwoInts, "add_two_ints", self.add_two_ints_callback + ) + + def add_two_ints_callback(self, request, response): + response.sum = request.a + request.b + self.get_logger().info("Incoming request\na: %d b: %d" % (request.a, request.b)) + + return response + + +def main(): + rclpy.init() + + minimal_service = MinimalService() + + rclpy.spin(minimal_service) + + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/examples/async_node/subscriber.py b/examples/async_node/subscriber.py new file mode 100644 index 0000000..e576c53 --- /dev/null +++ b/examples/async_node/subscriber.py @@ -0,0 +1,36 @@ +import rclpy +from rclpy.node import Node +from std_msgs.msg import String + + +class MinimalSubscriber(Node): + + def __init__(self): + super().__init__('minimal_subscriber') + self.subscription = self.create_subscription( + String, + 'topic', + self.listener_callback, + 10) + self.subscription # prevent unused variable warning + + def listener_callback(self, msg): + self.get_logger().info('I heard: "%s"' % msg.data) + + +def main(args=None): + rclpy.init(args=args) + + minimal_subscriber = MinimalSubscriber() + + rclpy.spin(minimal_subscriber) + + # Destroy the node explicitly + # (optional - otherwise it will be done automatically + # when the garbage collector destroys the node object) + minimal_subscriber.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() \ No newline at end of file From e70c2186c7655cd7490ed9f9185963e35995715b Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Wed, 19 Nov 2025 11:46:12 +0000 Subject: [PATCH 06/11] Update ROS2 examples to use decorator syntax --- examples/async_node.py | 33 +-- examples/async_node/action_client.py | 38 ++-- examples/async_node/action_server.py | 56 +++-- examples/async_node/client.py | 51 ++--- examples/async_node/param.py | 35 ++-- examples/async_node/publisher.py | 42 ++-- examples/async_node/service.py | 30 ++- examples/async_node/subscriber.py | 37 ++-- src/rclpy_async/_async_node/__init__.py | 17 ++ .../_async_node/action_handler_spec.py | 16 ++ .../node_proto.py} | 0 .../_async_node/parameter_schema.py | 132 ++++++++++++ .../_async_node/service_handler_spec.py | 15 ++ src/rclpy_async/_async_node/state.py | 29 +++ .../_async_node/timer_handler_spec.py | 21 ++ .../_async_node/topic_handler_spec.py | 20 ++ src/rclpy_async/async_node.py | 198 +++++++++--------- 17 files changed, 475 insertions(+), 295 deletions(-) create mode 100644 src/rclpy_async/_async_node/__init__.py create mode 100644 src/rclpy_async/_async_node/action_handler_spec.py rename src/rclpy_async/{_node_proto.py => _async_node/node_proto.py} (100%) create mode 100644 src/rclpy_async/_async_node/parameter_schema.py create mode 100644 src/rclpy_async/_async_node/service_handler_spec.py create mode 100644 src/rclpy_async/_async_node/state.py create mode 100644 src/rclpy_async/_async_node/timer_handler_spec.py create mode 100644 src/rclpy_async/_async_node/topic_handler_spec.py diff --git a/examples/async_node.py b/examples/async_node.py index 86c7fbd..5479fb3 100644 --- a/examples/async_node.py +++ b/examples/async_node.py @@ -1,7 +1,7 @@ """AsyncNode example demonstrating parameter schema, subscription, and timer. This example shows how to: -1. Define a `ParameterSchema` (here `NodeCLIArgs`) to declare and retrieve ROS 2 +1. Define a `ParameterSchema` (here `NodeParameters`) to declare and retrieve ROS 2 parameters via the `AsyncNode` abstraction. 2. Register an asynchronous subscription handler using `@node.subscription` that logs incoming `std_msgs/msg/String` messages on the `chatter` topic. @@ -18,44 +18,21 @@ """ from dataclasses import dataclass -from typing import Callable import anyio import rclpy -from rclpy.parameter import Parameter from std_msgs.msg import String from rclpy_async.async_node import AsyncNode, ParameterSchema @dataclass -class NodeCLIArgs(ParameterSchema): +class NodeParameters(ParameterSchema): timed_event_counter: int = 0 timed_event_periodicity: float = 2.0 - def as_parameters(self): - return [ - ("timed_event_counter", self.timed_event_counter), - ("timed_event_periodicity", self.timed_event_periodicity), - ] - @classmethod - def from_parameters(cls, get_parameter: Callable[[str], Parameter]): - print( - get_parameter("timed_event_counter").get_parameter_value().integer_value, - get_parameter("timed_event_periodicity").get_parameter_value().double_value, - ) - return cls( - timed_event_counter=get_parameter("timed_event_counter") - .get_parameter_value() - .integer_value, - timed_event_periodicity=get_parameter("timed_event_periodicity") - .get_parameter_value() - .double_value, - ) - - -node = AsyncNode("mynode", NodeCLIArgs) +node = AsyncNode("mynode", NodeParameters) @node.subscription(String, "chatter") @@ -78,10 +55,10 @@ async def main(): node.state.publisher_ = node.create_publisher(String, "timer_event", 10) print( - "Node started. Listening on 'chatter/in' and publishing to 'chatter/out'.\n" + "Node started. Listening on 'chatter' and publishing to 'timer_event'.\n" + "To test, you can publish messages using:\n" + "\tros2 topic pub /chatter std_msgs/msg/String '{data: \"Hello World\"}' --once\n" - + "You should see the messages being echoed back on 'chatter/out'.\n" + + "You should see the messages printed.\n" + "To see the published messages, you can subscribe using:\n" + "\tros2 topic echo /timer_event std_msgs/msg/String\n" + "Press Ctrl+C to stop the node.\n" diff --git a/examples/async_node/action_client.py b/examples/async_node/action_client.py index 19c9292..4aa4d88 100644 --- a/examples/async_node/action_client.py +++ b/examples/async_node/action_client.py @@ -1,33 +1,27 @@ +import anyio import rclpy from example_interfaces.action import Fibonacci -from rclpy.action import ActionClient -from rclpy.node import Node +import rclpy_async +from rclpy_async.async_node import AsyncNode -class FibonacciActionClient(Node): +node = AsyncNode("fibonacci_action_client") - def __init__(self): - super().__init__("fibonacci_action_client") - self._action_client = ActionClient(self, Fibonacci, "fibonacci") - def send_goal(self, order): - goal_msg = Fibonacci.Goal() - goal_msg.order = order +async def main(): + rclpy.init() + node.initialize() - self._action_client.wait_for_server() + async with rclpy_async.start_executor() as xtor: + xtor.add_node(node) - return self._action_client.send_goal_async(goal_msg) - - -def main(args=None): - rclpy.init(args=args) - - action_client = FibonacciActionClient() - - future = action_client.send_goal(10) - - rclpy.spin_until_future_complete(action_client, future) + with rclpy_async.action_client(node, Fibonacci, "fibonacci") as action_client: + result = await action_client( + Fibonacci.Goal(order=10), + lambda msg: node.get_logger().info(f"Fibonacci feedback: {msg.feedback}"), # type: ignore + ) + node.get_logger().info(f"Fibonacci result: {result}") if __name__ == "__main__": - main() + anyio.run(main) diff --git a/examples/async_node/action_server.py b/examples/async_node/action_server.py index e096652..3710ec6 100644 --- a/examples/async_node/action_server.py +++ b/examples/async_node/action_server.py @@ -1,47 +1,41 @@ -import time - +import anyio import rclpy from example_interfaces.action import Fibonacci -from rclpy.action import ActionServer -from rclpy.node import Node - +from rclpy.action.server import ServerGoalHandle -class FibonacciActionServer(Node): +from rclpy_async.async_node import AsyncNode - def __init__(self): - super().__init__("fibonacci_action_server") - self._action_server = ActionServer( - self, Fibonacci, "fibonacci", self.execute_callback - ) +node = AsyncNode("fibonacci_action_server_node") - def execute_callback(self, goal_handle): - self.get_logger().info("Executing goal...") - feedback_msg = Fibonacci.Feedback() - feedback_msg.sequence = [0, 1] +@node.action(Fibonacci, "fibonacci") +async def execute_callback(goal_handle: ServerGoalHandle): + node.get_logger().info("Executing goal...") - for i in range(1, goal_handle.request.order): - feedback_msg.sequence.append( - feedback_msg.sequence[i] + feedback_msg.sequence[i - 1] - ) - self.get_logger().info("Feedback: {0}".format(feedback_msg.sequence)) - goal_handle.publish_feedback(feedback_msg) - time.sleep(1) + feedback_msg = Fibonacci.Feedback() + feedback_msg.sequence = [0, 1] - goal_handle.succeed() + for i in range(1, goal_handle.request.order): + feedback_msg.sequence.append( + feedback_msg.sequence[i] + feedback_msg.sequence[i - 1] + ) + node.get_logger().info("Feedback: {0}".format(feedback_msg.sequence)) + goal_handle.publish_feedback(feedback_msg) + await anyio.sleep(1) - result = Fibonacci.Result() - result.sequence = feedback_msg.sequence - return result + goal_handle.succeed() + result = Fibonacci.Result() + result.sequence = feedback_msg.sequence + return result -def main(args=None): - rclpy.init(args=args) - fibonacci_action_server = FibonacciActionServer() +async def main(): + rclpy.init() + node.initialize() - rclpy.spin(fibonacci_action_server) + await node.spin_one() if __name__ == "__main__": - main() + anyio.run(main) diff --git a/examples/async_node/client.py b/examples/async_node/client.py index f4ef0f2..e09963c 100644 --- a/examples/async_node/client.py +++ b/examples/async_node/client.py @@ -1,40 +1,35 @@ import sys +import anyio import rclpy from example_interfaces.srv import AddTwoInts -from rclpy.node import Node +import rclpy_async +from rclpy_async.async_node import AsyncNode -class MinimalClientAsync(Node): +node = AsyncNode("minimal_client_async") - def __init__(self): - super().__init__("minimal_client_async") - self.cli = self.create_client(AddTwoInts, "add_two_ints") - while not self.cli.wait_for_service(timeout_sec=1.0): - self.get_logger().info("service not available, waiting again...") - self.req = AddTwoInts.Request() - def send_request(self, a, b): - self.req.a = a - self.req.b = b - return self.cli.call_async(self.req) - - -def main(): +async def main(): rclpy.init() - - minimal_client = MinimalClientAsync() - future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2])) - rclpy.spin_until_future_complete(minimal_client, future) - response = future.result() - minimal_client.get_logger().info( - "Result of add_two_ints: for %d + %d = %d" - % (int(sys.argv[1]), int(sys.argv[2]), response.sum) - ) - - minimal_client.destroy_node() - rclpy.shutdown() + node.initialize() + + async with rclpy_async.start_executor() as executor: + executor.add_node(node) + with rclpy_async.service_client( + node, + AddTwoInts, + "add_two_ints", + ) as cli: + req = AddTwoInts.Request() + req.a = int(sys.argv[1]) + req.b = int(sys.argv[2]) + response = await cli(req) + node.get_logger().info( + "Result of add_two_ints: for %d + %d = %d" + % (int(sys.argv[1]), int(sys.argv[2]), response.sum) + ) if __name__ == "__main__": - main() + anyio.run(main) diff --git a/examples/async_node/param.py b/examples/async_node/param.py index 9afe31b..a04256c 100644 --- a/examples/async_node/param.py +++ b/examples/async_node/param.py @@ -1,32 +1,31 @@ +import anyio import rclpy -import rclpy.node +from dataclasses import dataclass +from rclpy_async.async_node import AsyncNode, ParameterSchema -class MinimalParam(rclpy.node.Node): - def __init__(self): - super().__init__("minimal_param_node") - self.declare_parameter("my_parameter", "world") +@dataclass +class NodeParameters(ParameterSchema): + my_parameter: str = "world" - self.timer = self.create_timer(1, self.timer_callback) - def timer_callback(self): - my_param = self.get_parameter("my_parameter").get_parameter_value().string_value +node = AsyncNode("minimal_param_node", NodeParameters) - self.get_logger().info("Hello %s!" % my_param) - my_new_param = rclpy.parameter.Parameter( - "my_parameter", rclpy.Parameter.Type.STRING, "world" - ) - all_new_parameters = [my_new_param] - self.set_parameters(all_new_parameters) +@node.timer(0.5) +async def timer_callback(): + # Read current value from ROS parameter server (dynamic) and then update it. + node.get_logger().info(f"Hello {node.params.my_parameter}!") + node.params.my_parameter = "world" -def main(): +async def main(): rclpy.init() - node = MinimalParam() - rclpy.spin(node) + node.initialize() + + await node.spin_one() if __name__ == "__main__": - main() + anyio.run(main) diff --git a/examples/async_node/publisher.py b/examples/async_node/publisher.py index 686579e..fc1f2f3 100644 --- a/examples/async_node/publisher.py +++ b/examples/async_node/publisher.py @@ -1,38 +1,30 @@ +import anyio import rclpy -from rclpy.node import Node from std_msgs.msg import String +from rclpy_async.async_node import AsyncNode -class MinimalPublisher(Node): +node = AsyncNode("minimal_publisher") - def __init__(self): - super().__init__("minimal_publisher") - self.publisher_ = self.create_publisher(String, "topic", 10) - timer_period = 0.5 # seconds - self.timer = self.create_timer(timer_period, self.timer_callback) - self.i = 0 - def timer_callback(self): - msg = String() - msg.data = "Hello World: %d" % self.i - self.publisher_.publish(msg) - self.get_logger().info('Publishing: "%s"' % msg.data) - self.i += 1 +@node.timer(0.5) +async def timer_callback(): + msg = String() + msg.data = "Hello World: %d" % node.state.i + node.state.publisher_.publish(msg) + node.get_logger().info('Publishing: "%s"' % msg.data) + node.state.i += 1 -def main(args=None): - rclpy.init(args=args) +async def main(): + rclpy.init() + node.initialize() - minimal_publisher = MinimalPublisher() + node.state.i = 0 + node.state.publisher_ = node.create_publisher(String, "topic", 10) - rclpy.spin(minimal_publisher) - - # Destroy the node explicitly - # (optional - otherwise it will be done automatically - # when the garbage collector destroys the node object) - minimal_publisher.destroy_node() - rclpy.shutdown() + await node.spin_one() if __name__ == "__main__": - main() + anyio.run(main) diff --git a/examples/async_node/service.py b/examples/async_node/service.py index eeb4887..eff55f0 100644 --- a/examples/async_node/service.py +++ b/examples/async_node/service.py @@ -1,32 +1,26 @@ +import anyio import rclpy from example_interfaces.srv import AddTwoInts -from rclpy.node import Node +from rclpy_async.async_node import AsyncNode -class MinimalService(Node): +node = AsyncNode("minimal_service") - def __init__(self): - super().__init__("minimal_service") - self.srv = self.create_service( - AddTwoInts, "add_two_ints", self.add_two_ints_callback - ) - def add_two_ints_callback(self, request, response): - response.sum = request.a + request.b - self.get_logger().info("Incoming request\na: %d b: %d" % (request.a, request.b)) +@node.service(AddTwoInts, "add_two_ints") +async def add_two_ints_callback(request, response): + response.sum = request.a + request.b + node.get_logger().info("Incoming request \ta: %d b: %d" % (request.a, request.b)) - return response + return response -def main(): +async def main(): rclpy.init() + node.initialize() - minimal_service = MinimalService() - - rclpy.spin(minimal_service) - - rclpy.shutdown() + await node.spin_one() if __name__ == "__main__": - main() + anyio.run(main) diff --git a/examples/async_node/subscriber.py b/examples/async_node/subscriber.py index e576c53..ce2c9fb 100644 --- a/examples/async_node/subscriber.py +++ b/examples/async_node/subscriber.py @@ -1,36 +1,23 @@ +import anyio import rclpy -from rclpy.node import Node from std_msgs.msg import String +from rclpy_async.async_node import AsyncNode -class MinimalSubscriber(Node): +node = AsyncNode("minimal_subscriber") - def __init__(self): - super().__init__('minimal_subscriber') - self.subscription = self.create_subscription( - String, - 'topic', - self.listener_callback, - 10) - self.subscription # prevent unused variable warning - def listener_callback(self, msg): - self.get_logger().info('I heard: "%s"' % msg.data) +@node.subscription(String, "topic") +async def listener_callback(msg: String): + node.get_logger().info('I heard: "%s"' % msg.data) -def main(args=None): - rclpy.init(args=args) +async def main(): + rclpy.init() + node.initialize() - minimal_subscriber = MinimalSubscriber() + await node.spin_one() - rclpy.spin(minimal_subscriber) - # Destroy the node explicitly - # (optional - otherwise it will be done automatically - # when the garbage collector destroys the node object) - minimal_subscriber.destroy_node() - rclpy.shutdown() - - -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + anyio.run(main) diff --git a/src/rclpy_async/_async_node/__init__.py b/src/rclpy_async/_async_node/__init__.py new file mode 100644 index 0000000..b5d3a2a --- /dev/null +++ b/src/rclpy_async/_async_node/__init__.py @@ -0,0 +1,17 @@ +from .node_proto import NodeProto +from .parameter_schema import ParameterSchema +from .state import State +from .timer_handler_spec import TimerHandlerSpec +from .topic_handler_spec import TopicHandlerSpec +from .action_handler_spec import ActionHandlerSpec +from .service_handler_spec import ServiceHandlerSpec + +__all__ = [ + "ActionHandlerSpec", + "NodeProto", + "ParameterSchema", + "ServiceHandlerSpec", + "State", + "TimerHandlerSpec", + "TopicHandlerSpec", +] diff --git a/src/rclpy_async/_async_node/action_handler_spec.py b/src/rclpy_async/_async_node/action_handler_spec.py new file mode 100644 index 0000000..aa7809c --- /dev/null +++ b/src/rclpy_async/_async_node/action_handler_spec.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Any, Awaitable, Callable +from rclpy.action.server import ServerGoalHandle + + +@dataclass +class ActionHandlerSpec: + """Specification for an action handler. + + async_fn signature: (goal_handle: ServerGoalHandle) -> Awaitable[Any] + """ + + action_type: type + action_name: str + kwargs: dict[str, Any] + async_fn: Callable[[ServerGoalHandle], Awaitable[Any]] diff --git a/src/rclpy_async/_node_proto.py b/src/rclpy_async/_async_node/node_proto.py similarity index 100% rename from src/rclpy_async/_node_proto.py rename to src/rclpy_async/_async_node/node_proto.py diff --git a/src/rclpy_async/_async_node/parameter_schema.py b/src/rclpy_async/_async_node/parameter_schema.py new file mode 100644 index 0000000..04f02bf --- /dev/null +++ b/src/rclpy_async/_async_node/parameter_schema.py @@ -0,0 +1,132 @@ +from abc import ABC +from dataclasses import fields, is_dataclass +from typing import Any, List, Optional, Tuple + +from rcl_interfaces.msg import ParameterValue +from rclpy.node import Node +from rclpy.parameter import Parameter + + +class ParameterSchema(ABC): + """Base class enabling dynamic ROS 2 parameter get/set with strong typing. + + Subclasses should be declared as dataclasses. Field names and default values are + used to declare parameters. When attached to a node, attribute reads reflect the + current parameter value in the node and writes propagate through `set_parameters`. + """ + + _node: Optional[Node] = None + _dynamic: bool = False + + # --- Declaration helpers ------------------------------------------------- + @classmethod + def as_parameters(cls) -> List[Tuple[str, Any]]: + if not is_dataclass(cls): + raise TypeError("ParameterSchema subclasses must be dataclasses") + params: List[Tuple[str, Any]] = [] + for f in fields(cls): + params.append((f.name, getattr(cls, f.name))) + return params + + @classmethod + def from_node(cls, node: Node) -> "ParameterSchema": + if not is_dataclass(cls): + raise TypeError("ParameterSchema subclasses must be dataclasses") + values: dict[str, Any] = {} + for f in fields(cls): + try: + values[f.name] = _parameter_value_to_python( + node.get_parameter(f.name).get_parameter_value() + ) + except Exception: + values[f.name] = getattr(cls, f.name) + obj = cls(**values) + obj._dynamic = True + obj._node = node + return obj + + # --- Internal helpers ---------------------------------------------------- + def _get_parameter(self, name: str) -> ParameterValue: + if not self._dynamic or self._node is None: + raise RuntimeError("Schema not attached to node") + return self._node.get_parameter(name).get_parameter_value() + + def _set_parameters(self, parameters: List[Parameter]) -> None: + if not self._dynamic or self._node is None: + raise RuntimeError("Schema not attached to node") + self._node.set_parameters(parameters) + + def __getattribute__(self, name: str) -> Any: + # Intercept dataclass field reads when dynamic. + dynamic: bool = object.__getattribute__(self, "_dynamic") + node: Node = object.__getattribute__(self, "_node") + + if ( + dynamic + and node is not None + and name in getattr(type(self), "__dataclass_fields__", {}) + ): + try: + pv = node.get_parameter(name).get_parameter_value() + return _parameter_value_to_python(pv) + except Exception: + pass + return object.__getattribute__(self, name) + + def __setattr__(self, name: str, value: Any) -> None: + if name.startswith("_") or name in {"_node", "_dynamic"}: + object.__setattr__(self, name, value) + return + if ( + self._dynamic + and self._node is not None + and name in getattr(type(self), "__dataclass_fields__", {}) + ): + self._node.set_parameters( + [Parameter(name, _python_value_to_param_type(value), value)] + ) + object.__setattr__(self, name, value) + + +def _parameter_value_to_python(pv: ParameterValue) -> Any: + t = Parameter.Type(pv.type) + if t == Parameter.Type.NOT_SET: + return None + if t == Parameter.Type.BOOL: + return pv.bool_value + if t == Parameter.Type.INTEGER: + return pv.integer_value + if t == Parameter.Type.DOUBLE: + return pv.double_value + if t == Parameter.Type.STRING: + return pv.string_value + if t == Parameter.Type.BOOL_ARRAY: + return list(pv.bool_array_value) + if t == Parameter.Type.INTEGER_ARRAY: + return list(pv.integer_array_value) + if t == Parameter.Type.DOUBLE_ARRAY: + return list(pv.double_array_value) + if t == Parameter.Type.STRING_ARRAY: + return list(pv.string_array_value) + return None + + +def _python_value_to_param_type(value: Any) -> int: + if isinstance(value, bool): + return Parameter.Type.BOOL + if isinstance(value, int) and not isinstance(value, bool): + return Parameter.Type.INTEGER + if isinstance(value, float): + return Parameter.Type.DOUBLE + if isinstance(value, str): + return Parameter.Type.STRING + if isinstance(value, (list, tuple)): + if all(isinstance(v, bool) for v in value): + return Parameter.Type.BOOL_ARRAY + if all(isinstance(v, int) and not isinstance(v, bool) for v in value): + return Parameter.Type.INTEGER_ARRAY + if all(isinstance(v, float) for v in value): + return Parameter.Type.DOUBLE_ARRAY + if all(isinstance(v, str) for v in value): + return Parameter.Type.STRING_ARRAY + raise TypeError(f"Unsupported parameter value type for {value!r}") diff --git a/src/rclpy_async/_async_node/service_handler_spec.py b/src/rclpy_async/_async_node/service_handler_spec.py new file mode 100644 index 0000000..1d5c214 --- /dev/null +++ b/src/rclpy_async/_async_node/service_handler_spec.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import Any, Awaitable, Callable + + +@dataclass +class ServiceHandlerSpec: + """Specification for a service handler. + + async_fn signature: (request: SrvType.Request, response: SrvType.Response) -> Awaitable[SrvType.Response] + """ + + srv_type: type + srv_name: str + kwargs: dict[str, Any] + async_fn: Callable[[Any, Any], Awaitable[Any]] diff --git a/src/rclpy_async/_async_node/state.py b/src/rclpy_async/_async_node/state.py new file mode 100644 index 0000000..67887a7 --- /dev/null +++ b/src/rclpy_async/_async_node/state.py @@ -0,0 +1,29 @@ +from typing import Any + + +class State: + """ + An object that can be used to store arbitrary state. + + Used for `request.state` and `app.state`. + """ + + _state: dict[str, Any] + + def __init__(self, state: dict[str, Any] | None = None): + if state is None: + state = {} + super().__setattr__("_state", state) + + def __setattr__(self, key: Any, value: Any) -> None: + self._state[key] = value + + def __getattr__(self, key: Any) -> Any: + try: + return self._state[key] + except KeyError: + message = "'{}' object has no attribute '{}'" + raise AttributeError(message.format(self.__class__.__name__, key)) + + def __delattr__(self, key: Any) -> None: + del self._state[key] diff --git a/src/rclpy_async/_async_node/timer_handler_spec.py b/src/rclpy_async/_async_node/timer_handler_spec.py new file mode 100644 index 0000000..5189590 --- /dev/null +++ b/src/rclpy_async/_async_node/timer_handler_spec.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import Any, Awaitable, Callable, Generic, TypeVar, Union + +from .parameter_schema import ParameterSchema + +TParams = TypeVar("TParams", bound=ParameterSchema) + + +@dataclass +class TimerHandlerSpec(Generic[TParams]): + """Specification for a timer handler. + + async_fn signature: () -> Awaitable[None] + timer_period_sec may be float or a callable taking params -> float. + """ + + timer_period_sec: Union[float, Callable[[TParams], float]] + max_queue_size: int + drop_oldest: bool + kwargs: dict[str, Any] + async_fn: Callable[[], Awaitable[None]] diff --git a/src/rclpy_async/_async_node/topic_handler_spec.py b/src/rclpy_async/_async_node/topic_handler_spec.py new file mode 100644 index 0000000..61022c7 --- /dev/null +++ b/src/rclpy_async/_async_node/topic_handler_spec.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import Any, Awaitable, Callable + +from rclpy.qos import QoSProfile + + +@dataclass +class TopicHandlerSpec: + """Specification for a subscription handler. + + async_fn signature: (message: MsgType) -> Awaitable[None] + """ + + msg_type: type + topic_name: str + qos_profile: QoSProfile + max_queue_size: int + drop_oldest: bool + kwargs: dict[str, Any] + async_fn: Callable[[Any], Awaitable[None]] diff --git a/src/rclpy_async/async_node.py b/src/rclpy_async/async_node.py index 601e950..ef6e7c1 100644 --- a/src/rclpy_async/async_node.py +++ b/src/rclpy_async/async_node.py @@ -1,26 +1,23 @@ import inspect -from dataclasses import dataclass -from abc import ABC, abstractmethod -from typing import ( - Any, - Awaitable, - Callable, - Generic, - List, - Optional, - Type, - TypeVar, - Union, -) +from typing import Any, Awaitable, Callable, List, Optional, Type, TypeVar, Union import anyio import rclpy +from rclpy.action.server import ServerGoalHandle from rclpy.node import Node from rclpy.qos import QoSProfile import rclpy_async -from ._node_proto import NodeProto +from ._async_node import ( + ActionHandlerSpec, + NodeProto, + ParameterSchema, + ServiceHandlerSpec, + State, + TimerHandlerSpec, + TopicHandlerSpec, +) # Public interface member names gathered from the Protocol; used for delegation. _DELEGATE_NAMES = { @@ -43,87 +40,9 @@ def _is_overridden(cls: Type[Any], name: str) -> bool: return True -@dataclass -class TopicHandlerSpec: - """Specification for a subscription handler. - - async_fn signature: (message: MsgType) -> Awaitable[None] - """ - - msg_type: type - topic_name: str - qos_profile: QoSProfile - max_queue_size: int - drop_oldest: bool - async_fn: Callable[[Any], Awaitable[None]] - - -class ParameterSchema(ABC): - """Abstract base class for node parameter schema used by `AsyncNode`. - - Subclasses must provide a method `as_parameters` returning an iterable - of parameter declarations accepted by `Node.declare_parameters`, and a - classmethod `from_parameters` that constructs and returns an instance of - the schema populated from a getter callable (e.g. `node.get_parameter`). - """ - - @abstractmethod - def as_parameters(cls) -> list[tuple[str, Any]]: - """Return list of (name, value) tuples for declaration.""" - raise NotImplementedError - - @classmethod - @abstractmethod - def from_parameters(cls, get_parameter: Callable[[str], Any]) -> "ParameterSchema": - """Construct instance from declared parameters.""" - raise NotImplementedError - - TParams = TypeVar("TParams", bound=ParameterSchema) -@dataclass -class TimerHandlerSpec(Generic[TParams]): - """Specification for a timer handler. - - async_fn signature: () -> Awaitable[None] - timer_period_sec may be float or a callable taking params -> float. - """ - - timer_period_sec: Union[float, Callable[[TParams], float]] - max_queue_size: int - drop_oldest: bool - async_fn: Callable[[], Awaitable[None]] - - -class State: - """ - An object that can be used to store arbitrary state. - - Used for `request.state` and `app.state`. - """ - - _state: dict[str, Any] - - def __init__(self, state: dict[str, Any] | None = None): - if state is None: - state = {} - super().__setattr__("_state", state) - - def __setattr__(self, key: Any, value: Any) -> None: - self._state[key] = value - - def __getattr__(self, key: Any) -> Any: - try: - return self._state[key] - except KeyError: - message = "'{}' object has no attribute '{}'" - raise AttributeError(message.format(self.__class__.__name__, key)) - - def __delattr__(self, key: Any) -> None: - del self._state[key] - - class AsyncNode(NodeProto): """Asynchronous wrapper around a ROS 2 `Node` using dynamic delegation. @@ -131,9 +50,11 @@ class AsyncNode(NodeProto): Provides decorators for subscription and timer handlers executed with anyio. """ + __action_handler_specs: List[ActionHandlerSpec] = [] __inner: Optional[Node] = None # Underlying rclpy Node instance __node_name: str __params_type: Optional[Type[TParams]] + __service_handler_specs: List[ServiceHandlerSpec] = [] __timer_handler_specs: List[TimerHandlerSpec[TParams]] = [] __topic_handler_specs: List[TopicHandlerSpec] = [] params: Optional[TParams] = None @@ -161,11 +82,10 @@ def initialize(self, **kwargs) -> None: if kwargs.get("namespace", None) is not None else "" ), - parameters=self.__params_type().as_parameters(), + parameters=self.__params_type.as_parameters(), ) - - self.params = self.__params_type.from_parameters(self.__inner.get_parameter) - self.__inner.get_logger().info(f"PubSub parameters: {self.params}") + self.params = self.__params_type.from_node(self.__inner) + self.__inner.get_logger().debug(f"Parameters: {self.params}") def __getattr__(self, name: str) -> Any: """Delegate protocol members to inner node when not overridden locally.""" @@ -221,6 +141,41 @@ def __dir__(self) -> List[str]: def __repr__(self) -> str: return f"" + def action(self, action_type: type, action_name: str, **kwargs) -> Callable[ + [Callable[[ServerGoalHandle], Awaitable[Any]]], + Callable[[ServerGoalHandle], Awaitable[Any]], + ]: + """Decorator registering an async action handler.""" + + def _decorator(async_fn: Callable[[ServerGoalHandle], Awaitable[Any]]): + spec = ActionHandlerSpec( + action_type=action_type, + action_name=action_name, + kwargs=kwargs, + async_fn=async_fn, + ) + self.__action_handler_specs.append(spec) + return async_fn + + return _decorator + + def service( + self, srv_type: type, srv_name: str, **kwargs + ) -> Callable[[Callable[[Any], Awaitable[None]]], Callable[[Any], Awaitable[None]]]: + """Decorator registering an async service handler.""" + + def _decorator(async_fn: Callable[[Any], Awaitable[None]]): + spec = ServiceHandlerSpec( + srv_type=srv_type, + srv_name=srv_name, + kwargs=kwargs, + async_fn=async_fn, + ) + self.__service_handler_specs.append(spec) + return async_fn + + return _decorator + def subscription( self, msg_type: type, @@ -272,6 +227,36 @@ async def spin(self) -> None: async with anyio.create_task_group() as tg: _attached_consumers = [] + _action_servers = [] + + for spec in self.__action_handler_specs: + async_fn = spec.async_fn + action_type = spec.action_type + action_name = spec.action_name + kwargs = spec.kwargs + + _action_servers.append( + rclpy_async.action_server( + self.__inner, + action_type, + action_name, + async_fn, + **kwargs, + ) + ) + + for spec in self.__service_handler_specs: + async_fn = spec.async_fn + srv_type = spec.srv_type + srv_name = spec.srv_name + kwargs = spec.kwargs + + self.__inner.create_service( + srv_type, + srv_name, + async_fn, + **kwargs, + ) for spec in self.__topic_handler_specs: async_fn = spec.async_fn # expects (ctx, msg) @@ -280,6 +265,7 @@ async def spin(self) -> None: qos_profile = spec.qos_profile max_queue_size = spec.max_queue_size drop_oldest = spec.drop_oldest + kwargs = spec.kwargs send_stream, receive_stream = anyio.create_memory_object_stream( max_queue_size @@ -302,7 +288,11 @@ def _sub_callback(msg, *, _send=send_stream, _recv=receive_stream): pass self.__inner.create_subscription( - msg_type, topic_name, _sub_callback, qos_profile=qos_profile + msg_type, + topic_name, + _sub_callback, + qos_profile=qos_profile, + **kwargs, ) async def _consumer_task(fn=async_fn, _recv=receive_stream): @@ -313,6 +303,7 @@ async def _consumer_task(fn=async_fn, _recv=receive_stream): for spec in self.__timer_handler_specs: async_fn = spec.async_fn # expects (ctx) + kwargs = spec.kwargs timer_period_sec = spec.timer_period_sec if callable(timer_period_sec): try: @@ -344,7 +335,7 @@ def _timer_callback(_send=send_stream, _recv=receive_stream): except anyio.WouldBlock: pass - self.__inner.create_timer(period_value, _timer_callback) + self.__inner.create_timer(period_value, _timer_callback, **kwargs) async def _consumer_task(fn=async_fn, _recv=receive_stream): async for _ in _recv: @@ -355,8 +346,15 @@ async def _consumer_task(fn=async_fn, _recv=receive_stream): for consumer_task in _attached_consumers: tg.start_soon(consumer_task) - # Sleep forever; cancellation of the task group stops processing. - await anyio.sleep(float("inf")) + for action_server in _action_servers: + action_server.__enter__() + + try: + # Sleep forever; cancellation of the task group stops processing. + await anyio.sleep(float("inf")) + finally: + for action_server in _action_servers: + action_server.__exit__(None, None, None) async def spin_one(self) -> None: """Run a single executor managing this node and spin handlers concurrently.""" From 77e226b75d534f5f995a032b1a705bb50ff63e54 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Wed, 19 Nov 2025 13:41:57 +0000 Subject: [PATCH 07/11] Restore uv build --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 3 +- pyproject.toml | 13 +- uv.lock | 226 ++++++++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 uv.lock diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a906773..96cac4e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,4 +5,4 @@ RUN export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-inf apt update && export DEBIAN_FRONTEND=noninteractive && \ rm -f /tmp/ros2-apt-source.deb && \ apt install -y ros-humble-ros-base ros-humble-turtlesim ros-humble-example-interfaces && \ - echo source /opt/ros/humble/setup.bash >> /home/vscode/.profile + echo source /opt/ros/humble/setup.bash >> /home/vscode/.profile \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8204072..23a94af 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,9 +1,10 @@ { - "name": "ROS2", + "name": "ROS2 with uv", "build": { "dockerfile": "Dockerfile" }, "features": { + "ghcr.io/devcontainers-extra/features/uv:latest": {}, "ghcr.io/devcontainers/features/git-lfs:latest": {} } } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1893ae2..fba8636 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,9 @@ dependencies = [ "numpy>=2.2.6", ] -[build-system] -requires = ["setuptools>=70", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools] -package-dir = {"" = "src"} +[project.scripts] +rclpy-async = "rclpy_async:main" -[tool.setuptools.packages.find] -where = ["src"] +[build-system] +requires = ["uv_build>=0.8.18,<0.9.0"] +build-backend = "uv_build" \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..98652c9 --- /dev/null +++ b/uv.lock @@ -0,0 +1,226 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/45/e80d203ef6b267aa29b22714fb558930b27960a0c5ce3c19c999232bb3eb/numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", size = 21259253, upload-time = "2025-09-09T15:56:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/cf2c648fccf339e59302e00e5f2bc87725a3ce1992f30f3f78c9044d7c43/numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", size = 14450980, upload-time = "2025-09-09T15:56:05.926Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/9af1082bec870188c42a1c239839915b74a5099c392389ff04215dcee812/numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", size = 5379709, upload-time = "2025-09-09T15:56:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/bfd7abca52bcbf9a4a65abc83fe18ef01ccdeb37bfb28bbd6ad613447c79/numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", size = 6913923, upload-time = "2025-09-09T15:56:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/79/55/d69adad255e87ab7afda1caf93ca997859092afeb697703e2f010f7c2e55/numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", size = 14589591, upload-time = "2025-09-09T15:56:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/10/a2/010b0e27ddeacab7839957d7a8f00e91206e0c2c47abbb5f35a2630e5387/numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", size = 16938714, upload-time = "2025-09-09T15:56:14.637Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6b/12ce8ede632c7126eb2762b9e15e18e204b81725b81f35176eac14dc5b82/numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", size = 16370592, upload-time = "2025-09-09T15:56:17.285Z" }, + { url = "https://files.pythonhosted.org/packages/b4/35/aba8568b2593067bb6a8fe4c52babb23b4c3b9c80e1b49dff03a09925e4a/numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", size = 18884474, upload-time = "2025-09-09T15:56:20.943Z" }, + { url = "https://files.pythonhosted.org/packages/45/fa/7f43ba10c77575e8be7b0138d107e4f44ca4a1ef322cd16980ea3e8b8222/numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57", size = 6599794, upload-time = "2025-09-09T15:56:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a2/a4f78cb2241fe5664a22a10332f2be886dcdea8784c9f6a01c272da9b426/numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa", size = 13088104, upload-time = "2025-09-09T15:56:25.476Z" }, + { url = "https://files.pythonhosted.org/packages/79/64/e424e975adbd38282ebcd4891661965b78783de893b381cbc4832fb9beb2/numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7", size = 10460772, upload-time = "2025-09-09T15:56:27.679Z" }, + { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, + { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, + { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload-time = "2025-09-09T15:56:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload-time = "2025-09-09T15:56:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload-time = "2025-09-09T15:56:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, + { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, + { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" }, + { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, + { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, + { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" }, + { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, + { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, + { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" }, + { url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, + { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, + { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" }, + { url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" }, + { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/7e0a37cfced2644c9563c529f29fa28acbd0960dde32ece683aafa6f4949/numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", size = 21131019, upload-time = "2025-09-09T15:58:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/3291f505297ed63831135a6cc0f474da0c868a1f31b0dd9a9f03a7a0d2ed/numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", size = 14376288, upload-time = "2025-09-09T15:58:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4b/ae02e985bdeee73d7b5abdefeb98aef1207e96d4c0621ee0cf228ddfac3c/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", size = 5305425, upload-time = "2025-09-09T15:58:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/eb/9df215d6d7250db32007941500dc51c48190be25f2401d5b2b564e467247/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", size = 6819053, upload-time = "2025-09-09T15:58:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/57/62/208293d7d6b2a8998a4a1f23ac758648c3c32182d4ce4346062018362e29/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", size = 14420354, upload-time = "2025-09-09T15:58:52.704Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/11/0cc63f9f321ccf63886ac203336777140011fb669e739da36d8db3c53b98/numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", size = 12971844, upload-time = "2025-09-09T15:58:57.359Z" }, +] + +[[package]] +name = "rclpy-async" +version = "0.10" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.10.0" }, + { name = "numpy", specifier = ">=2.2.6" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +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" }, +] \ No newline at end of file From 81a30b19db83396efb03e0476871c2c3a3b696d2 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Wed, 19 Nov 2025 13:57:14 +0000 Subject: [PATCH 08/11] Formatting changes --- .devcontainer/Dockerfile | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 96cac4e..a906773 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,4 +5,4 @@ RUN export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-inf apt update && export DEBIAN_FRONTEND=noninteractive && \ rm -f /tmp/ros2-apt-source.deb && \ apt install -y ros-humble-ros-base ros-humble-turtlesim ros-humble-example-interfaces && \ - echo source /opt/ros/humble/setup.bash >> /home/vscode/.profile \ No newline at end of file + echo source /opt/ros/humble/setup.bash >> /home/vscode/.profile diff --git a/pyproject.toml b/pyproject.toml index fba8636..048ed1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,4 +17,4 @@ rclpy-async = "rclpy_async:main" [build-system] requires = ["uv_build>=0.8.18,<0.9.0"] -build-backend = "uv_build" \ No newline at end of file +build-backend = "uv_build" diff --git a/uv.lock b/uv.lock index 98652c9..0944d60 100644 --- a/uv.lock +++ b/uv.lock @@ -223,4 +223,4 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 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" }, -] \ No newline at end of file +] From 2f10f63ac16928f93b60c3d9d55527c1b5cc0224 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Wed, 19 Nov 2025 16:12:12 +0000 Subject: [PATCH 09/11] Remove parameters dataclass impl as unnecessary for now --- examples/async_node.py | 69 --------- examples/async_node/README.md | 16 +-- examples/async_node/param.py | 30 ++-- src/rclpy_async/_async_node/__init__.py | 2 - .../_async_node/parameter_schema.py | 132 ------------------ .../_async_node/timer_handler_spec.py | 12 +- src/rclpy_async/async_node.py | 43 ++---- 7 files changed, 42 insertions(+), 262 deletions(-) delete mode 100644 examples/async_node.py delete mode 100644 src/rclpy_async/_async_node/parameter_schema.py diff --git a/examples/async_node.py b/examples/async_node.py deleted file mode 100644 index 5479fb3..0000000 --- a/examples/async_node.py +++ /dev/null @@ -1,69 +0,0 @@ -"""AsyncNode example demonstrating parameter schema, subscription, and timer. - -This example shows how to: -1. Define a `ParameterSchema` (here `NodeParameters`) to declare and retrieve ROS 2 - parameters via the `AsyncNode` abstraction. -2. Register an asynchronous subscription handler using `@node.subscription` that - logs incoming `std_msgs/msg/String` messages on the `chatter` topic. -3. Register an asynchronous timer via `@node.timer`, whose period is computed - dynamically from declared parameters, publishing incrementing counter messages - on the `timer_event` topic. -4. Use `node.state` for mutable runtime state shared across handlers (counter and - publisher reference). - -Run this file and in another shell publish test data: - ros2 topic pub /chatter std_msgs/msg/String '{data: "Hello World"}' --once -Observe timer events: - ros2 topic echo /timer_event std_msgs/msg/String -""" - -from dataclasses import dataclass - -import anyio -import rclpy -from std_msgs.msg import String - -from rclpy_async.async_node import AsyncNode, ParameterSchema - - -@dataclass -class NodeParameters(ParameterSchema): - timed_event_counter: int = 0 - timed_event_periodicity: float = 2.0 - - -node = AsyncNode("mynode", NodeParameters) - - -@node.subscription(String, "chatter") -async def chatter_callback(msg): - node.get_logger().info(f"I heard: {msg.data}") - - -@node.timer(lambda params: params.timed_event_periodicity) -async def timer_callback(): - msg = String(data=f"Timer event {node.state.timed_event_counter}") - node.state.timed_event_counter += 1 - node.state.publisher_.publish(msg) - - -async def main(): - rclpy.init() - node.initialize() - - node.state.timed_event_counter = node.params.timed_event_counter - node.state.publisher_ = node.create_publisher(String, "timer_event", 10) - - print( - "Node started. Listening on 'chatter' and publishing to 'timer_event'.\n" - + "To test, you can publish messages using:\n" - + "\tros2 topic pub /chatter std_msgs/msg/String '{data: \"Hello World\"}' --once\n" - + "You should see the messages printed.\n" - + "To see the published messages, you can subscribe using:\n" - + "\tros2 topic echo /timer_event std_msgs/msg/String\n" - + "Press Ctrl+C to stop the node.\n" - ) - await node.spin_one() - - -anyio.run(main) diff --git a/examples/async_node/README.md b/examples/async_node/README.md index 2941878..f259214 100644 --- a/examples/async_node/README.md +++ b/examples/async_node/README.md @@ -9,27 +9,27 @@ The following samples are based off the ROS2 documented [tutorials](https://docs A simple publisher and subscriber: ```sh # Terminal 1: -python3 ./examples/async_node/subscriber.py +python3 -m examples.async_node.subscriber # Terminal 2: -python3 ./examples/async_node/publisher.py +python3 -m examples.async_node.publisher ``` A simple service and client: ```sh # Terminal 1: -python3 ./examples/async_node/service.py +python3 -m examples.async_node.service # Terminal 2: -python3 ./examples/async_node/client.py 1 2 +python3 -m examples.async_node.client 1 2 ``` Using parameters in a class: ```sh # Start the node -python3 ./examples/async_node/param.py +python3 -m examples.async_node.param # [Optionally] start the node with the parameter as something other that "world" by adding: -python3 ./examples/async_node/param.py --ros-args -p my_parameter:=world2 +python3 -m examples.async_node.param --ros-args -p my_parameter:=world2 # In another terminal set the parameter again ros2 param set /minimal_param_node my_parameter earth @@ -38,8 +38,8 @@ ros2 param set /minimal_param_node my_parameter earth An action server and client: ```sh # Terminal 1: -python3 ./examples/async_node/action_server.py +python3 -m examples.async_node.action_server # Terminal 2: -python3 ./examples/async_node/action_client.py +python3 -m examples.async_node.action_client ``` diff --git a/examples/async_node/param.py b/examples/async_node/param.py index a04256c..b851765 100644 --- a/examples/async_node/param.py +++ b/examples/async_node/param.py @@ -1,28 +1,34 @@ import anyio import rclpy -from dataclasses import dataclass -from rclpy_async.async_node import AsyncNode, ParameterSchema +from rclpy_async.async_node import AsyncNode +from rclpy.parameter import Parameter -@dataclass -class NodeParameters(ParameterSchema): - my_parameter: str = "world" +node = AsyncNode("minimal_param_node") -node = AsyncNode("minimal_param_node", NodeParameters) - -@node.timer(0.5) +@node.timer(lambda: node.get_parameter("timer_period").get_parameter_value().double_value) async def timer_callback(): - # Read current value from ROS parameter server (dynamic) and then update it. - node.get_logger().info(f"Hello {node.params.my_parameter}!") - node.params.my_parameter = "world" - + node.get_logger().info(f"Hello {node.get_parameter('my_parameter').get_parameter_value().string_value}!") + node.set_parameters([ + Parameter( + "my_parameter", + Parameter.Type.STRING, + "world" + ), + Parameter( + "timer_period", + Parameter.Type.DOUBLE, + node.get_parameter("timer_period").get_parameter_value().double_value, + ) + ]) async def main(): rclpy.init() node.initialize() + node.declare_parameters(namespace="", parameters=[('my_parameter', 'world'), ('timer_period', 2.0)]) await node.spin_one() diff --git a/src/rclpy_async/_async_node/__init__.py b/src/rclpy_async/_async_node/__init__.py index b5d3a2a..096b966 100644 --- a/src/rclpy_async/_async_node/__init__.py +++ b/src/rclpy_async/_async_node/__init__.py @@ -1,5 +1,4 @@ from .node_proto import NodeProto -from .parameter_schema import ParameterSchema from .state import State from .timer_handler_spec import TimerHandlerSpec from .topic_handler_spec import TopicHandlerSpec @@ -9,7 +8,6 @@ __all__ = [ "ActionHandlerSpec", "NodeProto", - "ParameterSchema", "ServiceHandlerSpec", "State", "TimerHandlerSpec", diff --git a/src/rclpy_async/_async_node/parameter_schema.py b/src/rclpy_async/_async_node/parameter_schema.py deleted file mode 100644 index 04f02bf..0000000 --- a/src/rclpy_async/_async_node/parameter_schema.py +++ /dev/null @@ -1,132 +0,0 @@ -from abc import ABC -from dataclasses import fields, is_dataclass -from typing import Any, List, Optional, Tuple - -from rcl_interfaces.msg import ParameterValue -from rclpy.node import Node -from rclpy.parameter import Parameter - - -class ParameterSchema(ABC): - """Base class enabling dynamic ROS 2 parameter get/set with strong typing. - - Subclasses should be declared as dataclasses. Field names and default values are - used to declare parameters. When attached to a node, attribute reads reflect the - current parameter value in the node and writes propagate through `set_parameters`. - """ - - _node: Optional[Node] = None - _dynamic: bool = False - - # --- Declaration helpers ------------------------------------------------- - @classmethod - def as_parameters(cls) -> List[Tuple[str, Any]]: - if not is_dataclass(cls): - raise TypeError("ParameterSchema subclasses must be dataclasses") - params: List[Tuple[str, Any]] = [] - for f in fields(cls): - params.append((f.name, getattr(cls, f.name))) - return params - - @classmethod - def from_node(cls, node: Node) -> "ParameterSchema": - if not is_dataclass(cls): - raise TypeError("ParameterSchema subclasses must be dataclasses") - values: dict[str, Any] = {} - for f in fields(cls): - try: - values[f.name] = _parameter_value_to_python( - node.get_parameter(f.name).get_parameter_value() - ) - except Exception: - values[f.name] = getattr(cls, f.name) - obj = cls(**values) - obj._dynamic = True - obj._node = node - return obj - - # --- Internal helpers ---------------------------------------------------- - def _get_parameter(self, name: str) -> ParameterValue: - if not self._dynamic or self._node is None: - raise RuntimeError("Schema not attached to node") - return self._node.get_parameter(name).get_parameter_value() - - def _set_parameters(self, parameters: List[Parameter]) -> None: - if not self._dynamic or self._node is None: - raise RuntimeError("Schema not attached to node") - self._node.set_parameters(parameters) - - def __getattribute__(self, name: str) -> Any: - # Intercept dataclass field reads when dynamic. - dynamic: bool = object.__getattribute__(self, "_dynamic") - node: Node = object.__getattribute__(self, "_node") - - if ( - dynamic - and node is not None - and name in getattr(type(self), "__dataclass_fields__", {}) - ): - try: - pv = node.get_parameter(name).get_parameter_value() - return _parameter_value_to_python(pv) - except Exception: - pass - return object.__getattribute__(self, name) - - def __setattr__(self, name: str, value: Any) -> None: - if name.startswith("_") or name in {"_node", "_dynamic"}: - object.__setattr__(self, name, value) - return - if ( - self._dynamic - and self._node is not None - and name in getattr(type(self), "__dataclass_fields__", {}) - ): - self._node.set_parameters( - [Parameter(name, _python_value_to_param_type(value), value)] - ) - object.__setattr__(self, name, value) - - -def _parameter_value_to_python(pv: ParameterValue) -> Any: - t = Parameter.Type(pv.type) - if t == Parameter.Type.NOT_SET: - return None - if t == Parameter.Type.BOOL: - return pv.bool_value - if t == Parameter.Type.INTEGER: - return pv.integer_value - if t == Parameter.Type.DOUBLE: - return pv.double_value - if t == Parameter.Type.STRING: - return pv.string_value - if t == Parameter.Type.BOOL_ARRAY: - return list(pv.bool_array_value) - if t == Parameter.Type.INTEGER_ARRAY: - return list(pv.integer_array_value) - if t == Parameter.Type.DOUBLE_ARRAY: - return list(pv.double_array_value) - if t == Parameter.Type.STRING_ARRAY: - return list(pv.string_array_value) - return None - - -def _python_value_to_param_type(value: Any) -> int: - if isinstance(value, bool): - return Parameter.Type.BOOL - if isinstance(value, int) and not isinstance(value, bool): - return Parameter.Type.INTEGER - if isinstance(value, float): - return Parameter.Type.DOUBLE - if isinstance(value, str): - return Parameter.Type.STRING - if isinstance(value, (list, tuple)): - if all(isinstance(v, bool) for v in value): - return Parameter.Type.BOOL_ARRAY - if all(isinstance(v, int) and not isinstance(v, bool) for v in value): - return Parameter.Type.INTEGER_ARRAY - if all(isinstance(v, float) for v in value): - return Parameter.Type.DOUBLE_ARRAY - if all(isinstance(v, str) for v in value): - return Parameter.Type.STRING_ARRAY - raise TypeError(f"Unsupported parameter value type for {value!r}") diff --git a/src/rclpy_async/_async_node/timer_handler_spec.py b/src/rclpy_async/_async_node/timer_handler_spec.py index 5189590..ecfea08 100644 --- a/src/rclpy_async/_async_node/timer_handler_spec.py +++ b/src/rclpy_async/_async_node/timer_handler_spec.py @@ -1,20 +1,16 @@ from dataclasses import dataclass -from typing import Any, Awaitable, Callable, Generic, TypeVar, Union - -from .parameter_schema import ParameterSchema - -TParams = TypeVar("TParams", bound=ParameterSchema) +from typing import Any, Awaitable, Callable, Union @dataclass -class TimerHandlerSpec(Generic[TParams]): +class TimerHandlerSpec(): """Specification for a timer handler. async_fn signature: () -> Awaitable[None] - timer_period_sec may be float or a callable taking params -> float. + timer_period_sec may be float or a callable None -> float. """ - timer_period_sec: Union[float, Callable[[TParams], float]] + timer_period_sec: Union[float, Callable[[], float]] max_queue_size: int drop_oldest: bool kwargs: dict[str, Any] diff --git a/src/rclpy_async/async_node.py b/src/rclpy_async/async_node.py index ef6e7c1..dc31a28 100644 --- a/src/rclpy_async/async_node.py +++ b/src/rclpy_async/async_node.py @@ -12,7 +12,6 @@ from ._async_node import ( ActionHandlerSpec, NodeProto, - ParameterSchema, ServiceHandlerSpec, State, TimerHandlerSpec, @@ -40,9 +39,6 @@ def _is_overridden(cls: Type[Any], name: str) -> bool: return True -TParams = TypeVar("TParams", bound=ParameterSchema) - - class AsyncNode(NodeProto): """Asynchronous wrapper around a ROS 2 `Node` using dynamic delegation. @@ -50,43 +46,24 @@ class AsyncNode(NodeProto): Provides decorators for subscription and timer handlers executed with anyio. """ - __action_handler_specs: List[ActionHandlerSpec] = [] __inner: Optional[Node] = None # Underlying rclpy Node instance __node_name: str - __params_type: Optional[Type[TParams]] + state = State() # Arbitrary user state container + + __action_handler_specs: List[ActionHandlerSpec] = [] __service_handler_specs: List[ServiceHandlerSpec] = [] - __timer_handler_specs: List[TimerHandlerSpec[TParams]] = [] + __timer_handler_specs: List[TimerHandlerSpec] = [] __topic_handler_specs: List[TopicHandlerSpec] = [] - params: Optional[TParams] = None - state = State() # Arbitrary user state container - def __init__(self, node_name: str, params_type: Type[TParams] = None): + def __init__(self, node_name: str): self.__node_name = node_name - self.__params_type = params_type # Parameter schema type or None - - @property - def inner(self) -> Optional[Node]: - """Return underlying `Node` (read-only reference).""" - return self.__inner - def initialize(self, **kwargs) -> None: + def initialize(self) -> None: """Create underlying rclpy Node and declare parameters if a schema is provided.""" if self.__inner is not None: raise RuntimeError("AsyncNode already initialized") self.__inner = rclpy.create_node(self.__node_name) - if self.__params_type is not None: - self.__inner.declare_parameters( - namespace=( - kwargs.get("namespace") - if kwargs.get("namespace", None) is not None - else "" - ), - parameters=self.__params_type.as_parameters(), - ) - self.params = self.__params_type.from_node(self.__inner) - self.__inner.get_logger().debug(f"Parameters: {self.params}") - def __getattr__(self, name: str) -> Any: """Delegate protocol members to inner node when not overridden locally.""" inner = self.__inner @@ -183,6 +160,7 @@ def subscription( qos_profile: QoSProfile = 10, max_queue_size: int = 0, drop_oldest: bool = False, + **kwargs, ) -> Callable[[Callable[[Any], Awaitable[None]]], Callable[[Any], Awaitable[None]]]: """Decorator registering an async subscription handler.""" @@ -193,6 +171,7 @@ def _decorator(async_fn: Callable[[Any], Awaitable[None]]): qos_profile=qos_profile, max_queue_size=max_queue_size, drop_oldest=drop_oldest, + kwargs=kwargs, async_fn=async_fn, ) self.__topic_handler_specs.append(spec) @@ -202,9 +181,10 @@ def _decorator(async_fn: Callable[[Any], Awaitable[None]]): def timer( self, - timer_period_sec: Union[float, Callable[[TParams], float]], + timer_period_sec: Union[float, Callable[[], float]], max_queue_size: int = 0, drop_oldest: bool = False, + **kwargs, ) -> Callable[[Callable[[], Awaitable[None]]], Callable[[], Awaitable[None]]]: """Decorator registering an async timer handler.""" @@ -213,6 +193,7 @@ def _decorator(async_fn: Callable[[], Awaitable[None]]): timer_period_sec=timer_period_sec, max_queue_size=max_queue_size, drop_oldest=drop_oldest, + kwargs=kwargs, async_fn=async_fn, ) self.__timer_handler_specs.append(spec) @@ -307,7 +288,7 @@ async def _consumer_task(fn=async_fn, _recv=receive_stream): timer_period_sec = spec.timer_period_sec if callable(timer_period_sec): try: - period_value = float(timer_period_sec(self.params)) + period_value = float(timer_period_sec()) except Exception: period_value = 1.0 else: From 43868fee7b93f03634f1380caff9efe052c9582b Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Thu, 20 Nov 2025 11:46:40 +0000 Subject: [PATCH 10/11] Clean impl --- examples/async_node/action_client.py | 12 +- examples/async_node/action_server.py | 7 +- examples/async_node/client.py | 2 +- examples/async_node/param.py | 44 +- examples/async_node/publisher.py | 7 +- examples/async_node/service.py | 7 +- examples/async_node/subscriber.py | 13 +- src/rclpy_async/__init__.py | 6 + src/rclpy_async/_async_node/__init__.py | 6 +- .../_async_node/_base_handler_spec.py | 23 + .../_async_node/action_handler_spec.py | 16 +- .../_async_node/backpressure_handler_spec.py | 9 + .../_async_node/service_handler_spec.py | 13 +- .../_async_node/timer_handler_spec.py | 16 +- .../_async_node/topic_handler_spec.py | 13 +- src/rclpy_async/async_node.py | 395 ++++++++++-------- 16 files changed, 345 insertions(+), 244 deletions(-) create mode 100644 src/rclpy_async/_async_node/_base_handler_spec.py create mode 100644 src/rclpy_async/_async_node/backpressure_handler_spec.py diff --git a/examples/async_node/action_client.py b/examples/async_node/action_client.py index 4aa4d88..a1f4011 100644 --- a/examples/async_node/action_client.py +++ b/examples/async_node/action_client.py @@ -1,9 +1,11 @@ +from typing import Callable + import anyio import rclpy from example_interfaces.action import Fibonacci import rclpy_async -from rclpy_async.async_node import AsyncNode +from rclpy_async import AsyncNode node = AsyncNode("fibonacci_action_client") @@ -16,10 +18,12 @@ async def main(): xtor.add_node(node) with rclpy_async.action_client(node, Fibonacci, "fibonacci") as action_client: - result = await action_client( - Fibonacci.Goal(order=10), - lambda msg: node.get_logger().info(f"Fibonacci feedback: {msg.feedback}"), # type: ignore + print_feedback: Callable[[Fibonacci.Impl.FeedbackMessage], None] = ( + lambda msg: node.get_logger().info( + f"Fibonacci feedback: {msg.feedback}" + ) ) + result = await action_client(Fibonacci.Goal(order=10), print_feedback) node.get_logger().info(f"Fibonacci result: {result}") diff --git a/examples/async_node/action_server.py b/examples/async_node/action_server.py index 3710ec6..cbf4964 100644 --- a/examples/async_node/action_server.py +++ b/examples/async_node/action_server.py @@ -3,7 +3,8 @@ from example_interfaces.action import Fibonacci from rclpy.action.server import ServerGoalHandle -from rclpy_async.async_node import AsyncNode +import rclpy_async +from rclpy_async import AsyncNode, async_run node = AsyncNode("fibonacci_action_server_node") @@ -34,7 +35,9 @@ async def main(): rclpy.init() node.initialize() - await node.spin_one() + async with rclpy_async.start_executor() as xtor: + xtor.add_node(node) + await async_run(node) if __name__ == "__main__": diff --git a/examples/async_node/client.py b/examples/async_node/client.py index e09963c..59a344a 100644 --- a/examples/async_node/client.py +++ b/examples/async_node/client.py @@ -5,7 +5,7 @@ from example_interfaces.srv import AddTwoInts import rclpy_async -from rclpy_async.async_node import AsyncNode +from rclpy_async import AsyncNode node = AsyncNode("minimal_client_async") diff --git a/examples/async_node/param.py b/examples/async_node/param.py index b851765..d21adfc 100644 --- a/examples/async_node/param.py +++ b/examples/async_node/param.py @@ -1,36 +1,42 @@ import anyio import rclpy - -from rclpy_async.async_node import AsyncNode from rclpy.parameter import Parameter - +import rclpy_async +from rclpy_async import AsyncNode, async_run node = AsyncNode("minimal_param_node") -@node.timer(lambda: node.get_parameter("timer_period").get_parameter_value().double_value) +@node.timer( + lambda: node.get_parameter("timer_period").get_parameter_value().double_value +) async def timer_callback(): - node.get_logger().info(f"Hello {node.get_parameter('my_parameter').get_parameter_value().string_value}!") - node.set_parameters([ - Parameter( - "my_parameter", - Parameter.Type.STRING, - "world" - ), - Parameter( - "timer_period", - Parameter.Type.DOUBLE, - node.get_parameter("timer_period").get_parameter_value().double_value, - ) - ]) + node.get_logger().info( + f"Hello {node.get_parameter('my_parameter').get_parameter_value().string_value}!" + ) + node.set_parameters( + [ + Parameter("my_parameter", Parameter.Type.STRING, "world"), + Parameter( + "timer_period", + Parameter.Type.DOUBLE, + node.get_parameter("timer_period").get_parameter_value().double_value, + ), + ] + ) + async def main(): rclpy.init() node.initialize() - node.declare_parameters(namespace="", parameters=[('my_parameter', 'world'), ('timer_period', 2.0)]) + node.declare_parameters( + namespace="", parameters=[("my_parameter", "world"), ("timer_period", 2.0)] + ) - await node.spin_one() + async with rclpy_async.start_executor() as xtor: + xtor.add_node(node) + await async_run(node) if __name__ == "__main__": diff --git a/examples/async_node/publisher.py b/examples/async_node/publisher.py index fc1f2f3..de8f807 100644 --- a/examples/async_node/publisher.py +++ b/examples/async_node/publisher.py @@ -2,7 +2,8 @@ import rclpy from std_msgs.msg import String -from rclpy_async.async_node import AsyncNode +import rclpy_async +from rclpy_async import AsyncNode, async_run node = AsyncNode("minimal_publisher") @@ -23,7 +24,9 @@ async def main(): node.state.i = 0 node.state.publisher_ = node.create_publisher(String, "topic", 10) - await node.spin_one() + async with rclpy_async.start_executor() as xtor: + xtor.add_node(node) + await async_run(node) if __name__ == "__main__": diff --git a/examples/async_node/service.py b/examples/async_node/service.py index eff55f0..567d54c 100644 --- a/examples/async_node/service.py +++ b/examples/async_node/service.py @@ -2,7 +2,8 @@ import rclpy from example_interfaces.srv import AddTwoInts -from rclpy_async.async_node import AsyncNode +import rclpy_async +from rclpy_async import AsyncNode, async_run node = AsyncNode("minimal_service") @@ -19,7 +20,9 @@ async def main(): rclpy.init() node.initialize() - await node.spin_one() + async with rclpy_async.start_executor() as xtor: + xtor.add_node(node) + await async_run(node) if __name__ == "__main__": diff --git a/examples/async_node/subscriber.py b/examples/async_node/subscriber.py index ce2c9fb..55cb776 100644 --- a/examples/async_node/subscriber.py +++ b/examples/async_node/subscriber.py @@ -2,12 +2,17 @@ import rclpy from std_msgs.msg import String -from rclpy_async.async_node import AsyncNode +import rclpy_async +from rclpy_async import AsyncNode, BackpressureHandlerSpec node = AsyncNode("minimal_subscriber") -@node.subscription(String, "topic") +@node.subscription( + String, + "topic", + backpressure_handler=BackpressureHandlerSpec(max_queue_size=5, drop_oldest=True), +) async def listener_callback(msg: String): node.get_logger().info('I heard: "%s"' % msg.data) @@ -16,7 +21,9 @@ async def main(): rclpy.init() node.initialize() - await node.spin_one() + async with rclpy_async.start_executor() as xtor: + xtor.add_node(node) + await rclpy_async.async_run(node) if __name__ == "__main__": diff --git a/src/rclpy_async/__init__.py b/src/rclpy_async/__init__.py index 34eae4b..b53a300 100644 --- a/src/rclpy_async/__init__.py +++ b/src/rclpy_async/__init__.py @@ -1,8 +1,11 @@ from importlib.metadata import version +from rclpy_async._async_node import BackpressureHandlerSpec from rclpy_async.action_client import action_client from rclpy_async.action_server import action_server from rclpy_async.async_executor import start_executor +from rclpy_async.async_node import AsyncNode +from rclpy_async.async_node import run as async_run from rclpy_async.service_client import service_client from rclpy_async.utilities import ( future_result, @@ -21,4 +24,7 @@ "service_client", "action_client", "action_server", + "async_run", + "AsyncNode", + "BackpressureHandlerSpec", ] diff --git a/src/rclpy_async/_async_node/__init__.py b/src/rclpy_async/_async_node/__init__.py index 096b966..4ad8621 100644 --- a/src/rclpy_async/_async_node/__init__.py +++ b/src/rclpy_async/_async_node/__init__.py @@ -1,12 +1,14 @@ +from .action_handler_spec import ActionHandlerSpec +from .backpressure_handler_spec import BackpressureHandlerSpec from .node_proto import NodeProto +from .service_handler_spec import ServiceHandlerSpec from .state import State from .timer_handler_spec import TimerHandlerSpec from .topic_handler_spec import TopicHandlerSpec -from .action_handler_spec import ActionHandlerSpec -from .service_handler_spec import ServiceHandlerSpec __all__ = [ "ActionHandlerSpec", + "BackpressureHandlerSpec", "NodeProto", "ServiceHandlerSpec", "State", diff --git a/src/rclpy_async/_async_node/_base_handler_spec.py b/src/rclpy_async/_async_node/_base_handler_spec.py new file mode 100644 index 0000000..03439bc --- /dev/null +++ b/src/rclpy_async/_async_node/_base_handler_spec.py @@ -0,0 +1,23 @@ +from abc import ABC +from dataclasses import dataclass +from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar, ParamSpec + +from .backpressure_handler_spec import BackpressureHandlerSpec + +PInput = ParamSpec("PInput") +TOutput = TypeVar("TOutput") + + +@dataclass +class BaseHandlerSpec(ABC, Generic[PInput, TOutput]): + """Base Handler spec for registering ROS callbacks + + Args: + ABC (_type_): Abstract Base Class + Generic (_type_): Generic class + """ + + backpressure_handler: Optional[BackpressureHandlerSpec] + kwargs: dict[str, Any] + + async_fn: Callable[PInput, Awaitable[TOutput]] diff --git a/src/rclpy_async/_async_node/action_handler_spec.py b/src/rclpy_async/_async_node/action_handler_spec.py index aa7809c..60530d4 100644 --- a/src/rclpy_async/_async_node/action_handler_spec.py +++ b/src/rclpy_async/_async_node/action_handler_spec.py @@ -1,16 +1,20 @@ from dataclasses import dataclass -from typing import Any, Awaitable, Callable +from typing import Any + from rclpy.action.server import ServerGoalHandle +from ._base_handler_spec import BaseHandlerSpec + @dataclass -class ActionHandlerSpec: - """Specification for an action handler. +class ActionHandlerSpec(BaseHandlerSpec[[ServerGoalHandle], Any]): + """Specification for a ROS action handler + + async_fn signature: (goal: ServerGoalHandle) -> Awaitable[Any] - async_fn signature: (goal_handle: ServerGoalHandle) -> Awaitable[Any] + Args: + BaseHandlerSpec (_type_): Inherits from BaseHandlerSpec """ action_type: type action_name: str - kwargs: dict[str, Any] - async_fn: Callable[[ServerGoalHandle], Awaitable[Any]] diff --git a/src/rclpy_async/_async_node/backpressure_handler_spec.py b/src/rclpy_async/_async_node/backpressure_handler_spec.py new file mode 100644 index 0000000..ab7ca74 --- /dev/null +++ b/src/rclpy_async/_async_node/backpressure_handler_spec.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class BackpressureHandlerSpec: + """Specification for a backpressure handler""" + + max_queue_size: int = 0 + drop_oldest: bool = True diff --git a/src/rclpy_async/_async_node/service_handler_spec.py b/src/rclpy_async/_async_node/service_handler_spec.py index 1d5c214..984f2f2 100644 --- a/src/rclpy_async/_async_node/service_handler_spec.py +++ b/src/rclpy_async/_async_node/service_handler_spec.py @@ -1,15 +1,18 @@ from dataclasses import dataclass -from typing import Any, Awaitable, Callable +from typing import Any + +from ._base_handler_spec import BaseHandlerSpec @dataclass -class ServiceHandlerSpec: - """Specification for a service handler. +class ServiceHandlerSpec(BaseHandlerSpec[[Any, Any], Any]): + """Specification for a ROS service handler async_fn signature: (request: SrvType.Request, response: SrvType.Response) -> Awaitable[SrvType.Response] + + Args: + BaseHandlerSpec (_type_): Inherits from BaseHandlerSpec """ srv_type: type srv_name: str - kwargs: dict[str, Any] - async_fn: Callable[[Any, Any], Awaitable[Any]] diff --git a/src/rclpy_async/_async_node/timer_handler_spec.py b/src/rclpy_async/_async_node/timer_handler_spec.py index ecfea08..3f80db3 100644 --- a/src/rclpy_async/_async_node/timer_handler_spec.py +++ b/src/rclpy_async/_async_node/timer_handler_spec.py @@ -1,17 +1,17 @@ from dataclasses import dataclass -from typing import Any, Awaitable, Callable, Union +from typing import Callable, Union + +from ._base_handler_spec import BaseHandlerSpec @dataclass -class TimerHandlerSpec(): - """Specification for a timer handler. +class TimerHandlerSpec(BaseHandlerSpec[[], None]): + """Specification for a ROS service handler async_fn signature: () -> Awaitable[None] - timer_period_sec may be float or a callable None -> float. + + Args: + BaseHandlerSpec (_type_): Inherits from BaseHandlerSpec """ timer_period_sec: Union[float, Callable[[], float]] - max_queue_size: int - drop_oldest: bool - kwargs: dict[str, Any] - async_fn: Callable[[], Awaitable[None]] diff --git a/src/rclpy_async/_async_node/topic_handler_spec.py b/src/rclpy_async/_async_node/topic_handler_spec.py index 61022c7..934eaba 100644 --- a/src/rclpy_async/_async_node/topic_handler_spec.py +++ b/src/rclpy_async/_async_node/topic_handler_spec.py @@ -1,20 +1,21 @@ from dataclasses import dataclass -from typing import Any, Awaitable, Callable +from typing import Any, Awaitable, Callable, Optional from rclpy.qos import QoSProfile +from ._base_handler_spec import BaseHandlerSpec + @dataclass -class TopicHandlerSpec: +class TopicHandlerSpec(BaseHandlerSpec[[Any], None]): """Specification for a subscription handler. async_fn signature: (message: MsgType) -> Awaitable[None] + + Args: + BaseHandlerSpec (_type_): Inherits from BaseHandlerSpec """ msg_type: type topic_name: str qos_profile: QoSProfile - max_queue_size: int - drop_oldest: bool - kwargs: dict[str, Any] - async_fn: Callable[[Any], Awaitable[None]] diff --git a/src/rclpy_async/async_node.py b/src/rclpy_async/async_node.py index dc31a28..a9eca5a 100644 --- a/src/rclpy_async/async_node.py +++ b/src/rclpy_async/async_node.py @@ -1,5 +1,6 @@ import inspect -from typing import Any, Awaitable, Callable, List, Optional, Type, TypeVar, Union +from contextlib import ExitStack +from typing import Any, Awaitable, Callable, List, Optional, Tuple, Type, Union import anyio import rclpy @@ -11,6 +12,7 @@ from ._async_node import ( ActionHandlerSpec, + BackpressureHandlerSpec, NodeProto, ServiceHandlerSpec, State, @@ -47,8 +49,12 @@ class AsyncNode(NodeProto): """ __inner: Optional[Node] = None # Underlying rclpy Node instance - __node_name: str state = State() # Arbitrary user state container + __node_name: str + __exit_stack = ExitStack() + __fallback_values: dict[str, Any] = ( + {} + ) # Local storage for protocol-only properties when inner lacks them. __action_handler_specs: List[ActionHandlerSpec] = [] __service_handler_specs: List[ServiceHandlerSpec] = [] @@ -84,29 +90,37 @@ def __getattribute__(self, name: str) -> Any: return super().__getattribute__(name) inner = super().__getattribute__("_AsyncNode__inner") - # Delegate protocol-defined members unless overridden in AsyncNode. - if ( - inner is not None - and name in _DELEGATE_NAMES - and not _is_overridden(type(self), name) - ): - return getattr(inner, name) + # For protocol-defined members not overridden locally, attempt delegation. + if name in _DELEGATE_NAMES and not _is_overridden(type(self), name): + if inner is not None and hasattr(inner, name): + return getattr(inner, name) + # Fallback to locally stored value if inner doesn't provide it. + fb = super().__getattribute__("_AsyncNode__fallback_values") + if name in fb: + return fb[name] return super().__getattribute__(name) def __setattr__(self, name: str, value: Any) -> None: - # Allow normal setting for our private / known attributes. + # Allow normal setting for private / explicitly implemented attributes on AsyncNode itself. if ( name.startswith("_AsyncNode__") or name in {"params", "state"} - or hasattr(type(self), name) + or name in type(self).__dict__ ): super().__setattr__(name, value) return - inner = getattr(self, "_AsyncNode__inner", None) - if inner is not None and hasattr(inner, name): - setattr(inner, name, value) - else: - super().__setattr__(name, value) + # Delegation path for protocol-defined members not overridden here. + if name in _DELEGATE_NAMES and not _is_overridden(type(self), name): + inner = getattr(self, "_AsyncNode__inner", None) + if inner is not None and hasattr(inner, name): + setattr(inner, name, value) + return + # Store locally when inner lacks the attribute (e.g. protocol stub property). + fb = getattr(self, "_AsyncNode__fallback_values") + fb[name] = value + return + # Ordinary attribute (not protocol-defined): store on wrapper instance. + super().__setattr__(name, value) def __dir__(self) -> List[str]: inner = self.__inner @@ -116,39 +130,53 @@ def __dir__(self) -> List[str]: return sorted(names) def __repr__(self) -> str: - return f"" + return f"" - def action(self, action_type: type, action_name: str, **kwargs) -> Callable[ + def action( + self, + action_type: type, + action_name: str, + backpressure_handler: Optional[BackpressureHandlerSpec] = None, + **kwargs, + ) -> Callable[ [Callable[[ServerGoalHandle], Awaitable[Any]]], Callable[[ServerGoalHandle], Awaitable[Any]], ]: """Decorator registering an async action handler.""" def _decorator(async_fn: Callable[[ServerGoalHandle], Awaitable[Any]]): - spec = ActionHandlerSpec( - action_type=action_type, - action_name=action_name, - kwargs=kwargs, - async_fn=async_fn, + self.__action_handler_specs.append( + ActionHandlerSpec( + action_name=action_name, + action_type=action_type, + async_fn=async_fn, + backpressure_handler=backpressure_handler, + kwargs=kwargs, + ) ) - self.__action_handler_specs.append(spec) return async_fn return _decorator def service( - self, srv_type: type, srv_name: str, **kwargs + self, + srv_type: type, + srv_name: str, + backpressure_handler: Optional[BackpressureHandlerSpec] = None, + **kwargs, ) -> Callable[[Callable[[Any], Awaitable[None]]], Callable[[Any], Awaitable[None]]]: """Decorator registering an async service handler.""" def _decorator(async_fn: Callable[[Any], Awaitable[None]]): - spec = ServiceHandlerSpec( - srv_type=srv_type, - srv_name=srv_name, - kwargs=kwargs, - async_fn=async_fn, + self.__service_handler_specs.append( + ServiceHandlerSpec( + async_fn=async_fn, + backpressure_handler=backpressure_handler, + kwargs=kwargs, + srv_name=srv_name, + srv_type=srv_type, + ) ) - self.__service_handler_specs.append(spec) return async_fn return _decorator @@ -158,23 +186,22 @@ def subscription( msg_type: type, topic_name: str, qos_profile: QoSProfile = 10, - max_queue_size: int = 0, - drop_oldest: bool = False, + backpressure_handler: Optional[BackpressureHandlerSpec] = None, **kwargs, ) -> Callable[[Callable[[Any], Awaitable[None]]], Callable[[Any], Awaitable[None]]]: """Decorator registering an async subscription handler.""" def _decorator(async_fn: Callable[[Any], Awaitable[None]]): - spec = TopicHandlerSpec( - msg_type=msg_type, - topic_name=topic_name, - qos_profile=qos_profile, - max_queue_size=max_queue_size, - drop_oldest=drop_oldest, - kwargs=kwargs, - async_fn=async_fn, + self.__topic_handler_specs.append( + TopicHandlerSpec( + async_fn=async_fn, + backpressure_handler=backpressure_handler, + kwargs=kwargs, + msg_type=msg_type, + qos_profile=qos_profile, + topic_name=topic_name, + ) ) - self.__topic_handler_specs.append(spec) return async_fn return _decorator @@ -182,165 +209,165 @@ def _decorator(async_fn: Callable[[Any], Awaitable[None]]): def timer( self, timer_period_sec: Union[float, Callable[[], float]], - max_queue_size: int = 0, - drop_oldest: bool = False, + backpressure_handler: Optional[BackpressureHandlerSpec] = None, **kwargs, ) -> Callable[[Callable[[], Awaitable[None]]], Callable[[], Awaitable[None]]]: """Decorator registering an async timer handler.""" def _decorator(async_fn: Callable[[], Awaitable[None]]): - spec = TimerHandlerSpec( - timer_period_sec=timer_period_sec, - max_queue_size=max_queue_size, - drop_oldest=drop_oldest, - kwargs=kwargs, - async_fn=async_fn, + self.__timer_handler_specs.append( + TimerHandlerSpec( + async_fn=async_fn, + backpressure_handler=backpressure_handler, + kwargs=kwargs, + timer_period_sec=timer_period_sec, + ) ) - self.__timer_handler_specs.append(spec) return async_fn return _decorator - async def spin(self) -> None: + def destroy_node(self) -> None: + """Ensure underlying node is properly destroyed.""" + self.__exit_stack.close() + inner = self.__inner + if inner is not None: + inner.destroy_node() + self.__inner = None + + def gather_coroutines( + self, + ) -> List[Callable[[], Awaitable[None]]]: """Start processing subscription and timer handlers until cancelled.""" if self.__inner is None: raise RuntimeError("AsyncNode not initialized; call initialize() first") - async with anyio.create_task_group() as tg: - _attached_consumers = [] - _action_servers = [] - - for spec in self.__action_handler_specs: - async_fn = spec.async_fn - action_type = spec.action_type - action_name = spec.action_name - kwargs = spec.kwargs - - _action_servers.append( - rclpy_async.action_server( - self.__inner, - action_type, - action_name, - async_fn, - **kwargs, - ) - ) + _attached_consumers = [] - for spec in self.__service_handler_specs: - async_fn = spec.async_fn - srv_type = spec.srv_type - srv_name = spec.srv_name - kwargs = spec.kwargs - - self.__inner.create_service( - srv_type, - srv_name, - async_fn, - **kwargs, - ) + for spec in self.__action_handler_specs: + handler, coro = _wrap_handler_with_backpressure( + spec.async_fn, spec.backpressure_handler + ) - for spec in self.__topic_handler_specs: - async_fn = spec.async_fn # expects (ctx, msg) - msg_type = spec.msg_type - topic_name = spec.topic_name - qos_profile = spec.qos_profile - max_queue_size = spec.max_queue_size - drop_oldest = spec.drop_oldest - kwargs = spec.kwargs - - send_stream, receive_stream = anyio.create_memory_object_stream( - max_queue_size + self.__exit_stack.enter_context( + rclpy_async.action_server( + self.__inner, + spec.action_type, + spec.action_name, + handler, + **spec.kwargs, ) + ) - def _sub_callback(msg, *, _send=send_stream, _recv=receive_stream): - try: - _send.send_nowait(msg) - except anyio.WouldBlock: - if max_queue_size == 0: - return - if drop_oldest: - try: - _ = _recv.receive_nowait() - except anyio.WouldBlock: - return - try: - _send.send_nowait(msg) - except anyio.WouldBlock: - pass - - self.__inner.create_subscription( - msg_type, - topic_name, - _sub_callback, - qos_profile=qos_profile, - **kwargs, - ) + if coro is not None: + _attached_consumers.append(coro) - async def _consumer_task(fn=async_fn, _recv=receive_stream): - async for _msg in _recv: - await fn(_msg) - - _attached_consumers.append(_consumer_task) - - for spec in self.__timer_handler_specs: - async_fn = spec.async_fn # expects (ctx) - kwargs = spec.kwargs - timer_period_sec = spec.timer_period_sec - if callable(timer_period_sec): - try: - period_value = float(timer_period_sec()) - except Exception: - period_value = 1.0 - else: - period_value = float(timer_period_sec) - max_queue_size = spec.max_queue_size - drop_oldest = spec.drop_oldest - - send_stream, receive_stream = anyio.create_memory_object_stream( - max_queue_size - ) + for spec in self.__service_handler_specs: + async_fn = spec.async_fn + srv_type = spec.srv_type + srv_name = spec.srv_name - def _timer_callback(_send=send_stream, _recv=receive_stream): - try: - _send.send_nowait(None) - except anyio.WouldBlock: - if max_queue_size == 0: - return - if drop_oldest: - try: - _ = _recv.receive_nowait() - except anyio.WouldBlock: - return - try: - _send.send_nowait(None) - except anyio.WouldBlock: - pass - - self.__inner.create_timer(period_value, _timer_callback, **kwargs) - - async def _consumer_task(fn=async_fn, _recv=receive_stream): - async for _ in _recv: - await fn() - - _attached_consumers.append(_consumer_task) - - for consumer_task in _attached_consumers: - tg.start_soon(consumer_task) - - for action_server in _action_servers: - action_server.__enter__() - - try: - # Sleep forever; cancellation of the task group stops processing. - await anyio.sleep(float("inf")) - finally: - for action_server in _action_servers: - action_server.__exit__(None, None, None) - - async def spin_one(self) -> None: - """Run a single executor managing this node and spin handlers concurrently.""" - if self.__inner is None: - raise RuntimeError("AsyncNode not initialized; call initialize() first") - async with rclpy_async.start_executor() as xtor: - xtor.add_node(self.__inner) - await self.spin() + handler, coro = _wrap_handler_with_backpressure( + async_fn, spec.backpressure_handler + ) + self.__inner.create_service( + srv_type, + srv_name, + handler, + **spec.kwargs, + ) + + if coro is not None: + _attached_consumers.append(coro) + + for spec in self.__topic_handler_specs: + async_fn = spec.async_fn + msg_type = spec.msg_type + topic_name = spec.topic_name + qos_profile = spec.qos_profile + + handler, coro = _wrap_handler_with_backpressure( + async_fn, spec.backpressure_handler + ) + self.__inner.create_subscription( + msg_type, + topic_name, + handler, + qos_profile=qos_profile, + **spec.kwargs, + ) + + if coro is not None: + _attached_consumers.append(coro) + + for spec in self.__timer_handler_specs: + timer_period_sec = spec.timer_period_sec + async_fn = spec.async_fn + if callable(timer_period_sec): + try: + period_value = float(timer_period_sec()) + except Exception: + period_value = 1.0 + else: + period_value = float(timer_period_sec) + + handler, coro = _wrap_handler_with_backpressure( + async_fn, spec.backpressure_handler + ) + self.__inner.create_timer(period_value, handler, **spec.kwargs) + + if coro is not None: + _attached_consumers.append(coro) + + return _attached_consumers + + +def gather_nodes(*nodes: AsyncNode) -> List[Callable[[], Awaitable[None]]]: + """Gather coroutines from multiple AsyncNode instances.""" + coroutines = [] + for node in nodes: + coroutines.extend(node.gather_coroutines()) + return coroutines + + +async def run(*nodes: AsyncNode) -> None: + async with anyio.create_task_group() as tg: + coroutines = gather_nodes(*nodes) + for consumer_task in coroutines: + tg.start_soon(consumer_task) + + await anyio.sleep(float("inf")) + + +def _wrap_handler_with_backpressure( + async_fn: Callable[..., Awaitable[None]], + spec: Optional[BackpressureHandlerSpec] = None, +) -> Tuple[Callable[..., None], Callable[[], Awaitable[None]]]: + """Wrap an async function with a memory object stream for backpressure handling.""" + + if spec is None: + return async_fn, None + + send_stream, receive_stream = anyio.create_memory_object_stream(spec.max_queue_size) + + def _sub_callback(*args, **kwargs): + try: + send_stream.send_nowait((args, kwargs)) + except anyio.WouldBlock: + if spec.max_queue_size == 0: + return + if spec.drop_oldest: + try: + _ = receive_stream.receive_nowait() + except anyio.WouldBlock: + return + try: + send_stream.send_nowait((args, kwargs)) + except anyio.WouldBlock: + pass + + async def _consumer_task(): + async for args, kwargs in receive_stream: + await async_fn(*args, **kwargs) + + return _sub_callback, _consumer_task From a32a0ce7052176c7168b2a7be7322ea0329753ba Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Thu, 20 Nov 2025 16:16:50 +0000 Subject: [PATCH 11/11] Simplify String init --- examples/async_node/publisher.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/async_node/publisher.py b/examples/async_node/publisher.py index de8f807..ebdf5c3 100644 --- a/examples/async_node/publisher.py +++ b/examples/async_node/publisher.py @@ -1,17 +1,15 @@ import anyio import rclpy -from std_msgs.msg import String - import rclpy_async from rclpy_async import AsyncNode, async_run +from std_msgs.msg import String node = AsyncNode("minimal_publisher") @node.timer(0.5) async def timer_callback(): - msg = String() - msg.data = "Hello World: %d" % node.state.i + msg = String(data="Hello World: %d" % node.state.i) node.state.publisher_.publish(msg) node.get_logger().info('Publishing: "%s"' % msg.data) node.state.i += 1