mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-29 11:19:54 +02:00
Merge branch 'main' into jdr/uv-check
This commit is contained in:
Generated
+61
@@ -1831,6 +1831,7 @@ dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langchain-protocol" },
|
||||
{ name = "orjson" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1839,6 +1840,7 @@ requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
|
||||
{ name = "langchain-protocol", specifier = ">=0.0.15" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "websockets", specifier = ">=14,<16" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -4011,6 +4013,65 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widgetsnbextension"
|
||||
version = "4.0.15"
|
||||
|
||||
Generated
+61
@@ -600,6 +600,7 @@ dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langchain-protocol" },
|
||||
{ name = "orjson" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -608,6 +609,7 @@ requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
|
||||
{ name = "langchain-protocol", specifier = ">=0.0.15" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "websockets", specifier = ">=14,<16" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -1562,6 +1564,65 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xxhash"
|
||||
version = "3.6.0"
|
||||
|
||||
@@ -23,7 +23,7 @@ from typing import Any
|
||||
|
||||
from langchain_protocol import Event, SubscribeParams
|
||||
|
||||
from langgraph_sdk.stream.transport import EventStreamHandle
|
||||
from langgraph_sdk.stream.transport import AsyncProtocolTransport, EventStreamHandle
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bounded LRU set for event-id dedup
|
||||
@@ -103,7 +103,10 @@ class StreamController:
|
||||
- fan-out from the shared stream to per-subscription queues
|
||||
|
||||
Args:
|
||||
transport: the transport used to open event streams.
|
||||
transport: the `AsyncProtocolTransport` bound to this thread session.
|
||||
run_start_gate: zero-argument async callable that resolves once the
|
||||
current `run.start` has committed server-side (no-op when no
|
||||
run is in flight).
|
||||
max_queue_size: per-subscription queue bound (default 1024).
|
||||
seen_event_ids_max: LRU cap for the dedup set (default 10_000).
|
||||
"""
|
||||
@@ -111,7 +114,7 @@ class StreamController:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
transport: Any,
|
||||
transport: AsyncProtocolTransport,
|
||||
run_start_gate: Callable[[], Awaitable[None]] | None = None, # noqa: ARG002
|
||||
max_queue_size: int = 1024,
|
||||
seen_event_ids_max: int = 10_000,
|
||||
|
||||
@@ -14,9 +14,9 @@ from typing import Any
|
||||
from langchain_protocol import Event, SubscribeParams
|
||||
|
||||
from langgraph_sdk.stream.subscription import compute_union_filter, filter_covers
|
||||
from langgraph_sdk.stream.transport.sync_http import (
|
||||
from langgraph_sdk.stream.transport import (
|
||||
SyncEventStreamHandle,
|
||||
SyncProtocolSseTransport,
|
||||
SyncProtocolTransport,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -39,7 +39,7 @@ class SyncStreamController:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
transport: SyncProtocolSseTransport,
|
||||
transport: SyncProtocolTransport,
|
||||
*,
|
||||
run_start_gate: threading.Event | None = None,
|
||||
run_start_timeout: float = _DEFAULT_RUN_START_TIMEOUT,
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
"""Public exports for the v3 streaming transport layer."""
|
||||
|
||||
from langgraph_sdk.stream.transport.http import (
|
||||
from langgraph_sdk.stream.transport.base import (
|
||||
AsyncProtocolTransport,
|
||||
EventStreamHandle,
|
||||
ProtocolSseTransport,
|
||||
SyncEventStreamHandle,
|
||||
SyncProtocolTransport,
|
||||
build_event_stream_body,
|
||||
build_websocket_url,
|
||||
)
|
||||
from langgraph_sdk.stream.transport.http import ProtocolSseTransport
|
||||
from langgraph_sdk.stream.transport.sync_http import SyncProtocolSseTransport
|
||||
from langgraph_sdk.stream.transport.sync_ws import SyncProtocolWebSocketTransport
|
||||
from langgraph_sdk.stream.transport.ws import ProtocolWebSocketTransport
|
||||
|
||||
__all__ = ["EventStreamHandle", "ProtocolSseTransport"]
|
||||
__all__ = [
|
||||
"AsyncProtocolTransport",
|
||||
"EventStreamHandle",
|
||||
"ProtocolSseTransport",
|
||||
"ProtocolWebSocketTransport",
|
||||
"SyncEventStreamHandle",
|
||||
"SyncProtocolSseTransport",
|
||||
"SyncProtocolTransport",
|
||||
"SyncProtocolWebSocketTransport",
|
||||
"build_event_stream_body",
|
||||
"build_websocket_url",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Shared transport contracts for v3 thread-centric streaming."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
import httpx
|
||||
from langchain_protocol import Event
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventStreamHandle:
|
||||
"""Handle for one async filtered event stream."""
|
||||
|
||||
events: AsyncIterator[Event]
|
||||
ready: asyncio.Future[None]
|
||||
done: asyncio.Future[BaseException | None]
|
||||
close: Callable[[], Awaitable[None]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncEventStreamHandle:
|
||||
"""Handle for one sync filtered event stream."""
|
||||
|
||||
events: Iterator[Event]
|
||||
error: Callable[[], BaseException | None]
|
||||
close: Callable[[], None]
|
||||
|
||||
|
||||
class AsyncProtocolTransport(Protocol):
|
||||
"""Protocol implemented by async SSE and WebSocket transports."""
|
||||
|
||||
thread_id: str
|
||||
|
||||
async def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None: ...
|
||||
|
||||
def open_event_stream(self, params: dict[str, Any]) -> EventStreamHandle: ...
|
||||
|
||||
async def close(self) -> None: ...
|
||||
|
||||
|
||||
class SyncProtocolTransport(Protocol):
|
||||
"""Protocol implemented by sync SSE and WebSocket transports."""
|
||||
|
||||
thread_id: str
|
||||
|
||||
def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None: ...
|
||||
|
||||
def open_event_stream(self, params: dict[str, Any]) -> SyncEventStreamHandle: ...
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
def build_event_stream_body(params: dict[str, Any]) -> dict[str, Any]:
|
||||
body: dict[str, Any] = {"channels": params["channels"]}
|
||||
if params.get("namespaces") is not None:
|
||||
body["namespaces"] = params["namespaces"]
|
||||
if params.get("depth") is not None:
|
||||
body["depth"] = params["depth"]
|
||||
since = params.get("since")
|
||||
if isinstance(since, int):
|
||||
body["since"] = since
|
||||
return body
|
||||
|
||||
|
||||
def build_websocket_url(base_url: httpx.URL, path: str) -> str:
|
||||
"""Convert an HTTP base URL plus API path into a WebSocket URL."""
|
||||
scheme = "wss" if base_url.scheme == "https" else "ws"
|
||||
base_path = base_url.path.rstrip("/")
|
||||
stream_path = path if path.startswith("/") else f"/{path}"
|
||||
full_path = f"{base_path}{stream_path}" if base_path else stream_path
|
||||
return str(base_url.copy_with(scheme=scheme, path=full_path, query=None))
|
||||
|
||||
|
||||
def websocket_headers(headers: Mapping[str, str] | None) -> list[tuple[str, str]]:
|
||||
return list(dict(headers or {}).items())
|
||||
@@ -13,8 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import AsyncIterator, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
@@ -22,39 +21,12 @@ import orjson
|
||||
from langchain_protocol import Event
|
||||
|
||||
from langgraph_sdk.sse import BytesLineDecoder, SSEDecoder
|
||||
from langgraph_sdk.stream.transport.base import (
|
||||
EventStreamHandle,
|
||||
build_event_stream_body,
|
||||
)
|
||||
|
||||
|
||||
def _build_event_stream_body(params: dict[str, Any]) -> dict[str, Any]:
|
||||
body: dict[str, Any] = {"channels": params["channels"]}
|
||||
if params.get("namespaces") is not None:
|
||||
body["namespaces"] = params["namespaces"]
|
||||
if params.get("depth") is not None:
|
||||
body["depth"] = params["depth"]
|
||||
since = params.get("since")
|
||||
if isinstance(since, int):
|
||||
body["since"] = since
|
||||
return body
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventStreamHandle:
|
||||
"""Handle for one filtered SSE stream.
|
||||
|
||||
Attributes:
|
||||
events: async iterator of typed `Event`s. Exhausts when the
|
||||
stream closes (server hangup or `close()`).
|
||||
ready: resolves once HTTP response headers arrive; rejects on
|
||||
connection failure before headers.
|
||||
done: resolves to `None` on clean end, or the post-ready stream
|
||||
exception that ended the pump.
|
||||
close: invoke to cancel the underlying task and free the
|
||||
connection.
|
||||
"""
|
||||
|
||||
events: AsyncIterator[Event]
|
||||
ready: asyncio.Future[None]
|
||||
done: asyncio.Future[BaseException | None]
|
||||
close: Callable[[], Awaitable[None]]
|
||||
_build_event_stream_body = build_event_stream_body
|
||||
|
||||
|
||||
class ProtocolSseTransport:
|
||||
@@ -148,7 +120,7 @@ class ProtocolSseTransport:
|
||||
async with self._client.stream(
|
||||
"POST",
|
||||
self._stream_url,
|
||||
content=orjson.dumps(_build_event_stream_body(params)),
|
||||
content=orjson.dumps(build_event_stream_body(params)),
|
||||
headers=sse_headers,
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Callable, Iterator, Mapping
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Iterator, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
@@ -12,16 +11,10 @@ import orjson
|
||||
from langchain_protocol import Event
|
||||
|
||||
from langgraph_sdk.sse import BytesLineDecoder, SSEDecoder
|
||||
from langgraph_sdk.stream.transport.http import _build_event_stream_body
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncEventStreamHandle:
|
||||
"""Handle for one filtered synchronous SSE stream."""
|
||||
|
||||
events: Iterator[Event]
|
||||
error: Callable[[], BaseException | None]
|
||||
close: Callable[[], None]
|
||||
from langgraph_sdk.stream.transport.base import (
|
||||
SyncEventStreamHandle,
|
||||
build_event_stream_body,
|
||||
)
|
||||
|
||||
|
||||
class SyncProtocolSseTransport:
|
||||
@@ -73,7 +66,7 @@ class SyncProtocolSseTransport:
|
||||
request = self._client.build_request(
|
||||
"POST",
|
||||
self._stream_url,
|
||||
content=orjson.dumps(_build_event_stream_body(params)),
|
||||
content=orjson.dumps(build_event_stream_body(params)),
|
||||
headers=sse_headers,
|
||||
)
|
||||
stream_cm = self._client.send(request, stream=True)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Sync WebSocket transport for the v3 thread-centric protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Callable, Iterator, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
from langchain_protocol import Event
|
||||
from websockets.sync.client import connect as websocket_connect
|
||||
|
||||
from langgraph_sdk.stream.transport.base import (
|
||||
SyncEventStreamHandle,
|
||||
build_event_stream_body,
|
||||
build_websocket_url,
|
||||
websocket_headers,
|
||||
)
|
||||
|
||||
|
||||
class SyncProtocolWebSocketTransport:
|
||||
"""Sync v3 protocol transport using HTTP commands and WebSocket events."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
client: httpx.Client,
|
||||
thread_id: str,
|
||||
commands_path: str | None = None,
|
||||
stream_path: str | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
connect: Callable[..., Any] = websocket_connect,
|
||||
ping_interval: float | None = 20.0,
|
||||
ping_timeout: float | None = 20.0,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self.thread_id = thread_id
|
||||
self._commands_url = commands_path or f"/threads/{thread_id}/commands"
|
||||
self._stream_path = stream_path or f"/threads/{thread_id}/stream/events"
|
||||
self._default_headers: dict[str, str] = dict(headers or {})
|
||||
self._connect = connect
|
||||
self._ping_interval = ping_interval
|
||||
self._ping_timeout = ping_timeout
|
||||
self._closed = False
|
||||
|
||||
def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if self._closed:
|
||||
raise RuntimeError("Protocol transport is closed.")
|
||||
merged_headers = {**self._default_headers, "content-type": "application/json"}
|
||||
response = self._client.post(
|
||||
self._commands_url,
|
||||
content=orjson.dumps(command),
|
||||
headers=merged_headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
if response.status_code in (202, 204):
|
||||
return None
|
||||
payload = orjson.loads(response.content)
|
||||
if not isinstance(payload, dict) or "id" not in payload:
|
||||
raise RuntimeError("Protocol command did not return a valid response.")
|
||||
return payload
|
||||
|
||||
def open_event_stream(self, params: dict[str, Any]) -> SyncEventStreamHandle:
|
||||
if self._closed:
|
||||
raise RuntimeError("Protocol transport is closed.")
|
||||
closed = False
|
||||
stream_error: BaseException | None = None
|
||||
|
||||
url = build_websocket_url(self._client.base_url, self._stream_path)
|
||||
handshake_headers = list(websocket_headers(self._default_headers))
|
||||
cookie_header = _cookie_header(self._client)
|
||||
if cookie_header:
|
||||
handshake_headers.append(("Cookie", cookie_header))
|
||||
# Pre-enter the WebSocket context manager so close() can reach the socket
|
||||
# immediately, even before the caller has started iterating events().
|
||||
ws_cm = self._connect(
|
||||
url,
|
||||
additional_headers=handshake_headers,
|
||||
ping_interval=self._ping_interval,
|
||||
ping_timeout=self._ping_timeout,
|
||||
)
|
||||
websocket = ws_cm.__enter__()
|
||||
|
||||
def events() -> Iterator[Event]:
|
||||
nonlocal stream_error
|
||||
try:
|
||||
websocket.send(orjson.dumps(build_event_stream_body(params)).decode())
|
||||
for raw in websocket:
|
||||
if closed:
|
||||
return
|
||||
payload = _decode_frame(raw)
|
||||
if isinstance(payload, dict):
|
||||
yield cast("Event", payload)
|
||||
except BaseException as exc:
|
||||
if not closed:
|
||||
stream_error = exc
|
||||
raise
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
ws_cm.__exit__(None, None, None)
|
||||
|
||||
def error() -> BaseException | None:
|
||||
return stream_error
|
||||
|
||||
def close() -> None:
|
||||
nonlocal closed
|
||||
closed = True
|
||||
with contextlib.suppress(Exception):
|
||||
websocket.close()
|
||||
|
||||
return SyncEventStreamHandle(events=events(), error=error, close=close)
|
||||
|
||||
def close(self) -> None:
|
||||
self._closed = True
|
||||
|
||||
|
||||
def _decode_frame(raw: str | bytes | bytearray | memoryview) -> Any:
|
||||
if isinstance(raw, str):
|
||||
return orjson.loads(raw.encode())
|
||||
return orjson.loads(bytes(raw))
|
||||
|
||||
|
||||
def _cookie_header(client: httpx.Client) -> str | None:
|
||||
"""Build a `Cookie` header value from the httpx client's cookie jar."""
|
||||
cookies = dict(client.cookies)
|
||||
if not cookies:
|
||||
return None
|
||||
return "; ".join(f"{k}={v}" for k, v in cookies.items())
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Async WebSocket transport for the v3 thread-centric protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import AsyncIterator, Callable, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
from langchain_protocol import Event
|
||||
from websockets.asyncio.client import connect as websocket_connect
|
||||
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
|
||||
|
||||
from langgraph_sdk.stream.transport.base import (
|
||||
EventStreamHandle,
|
||||
build_event_stream_body,
|
||||
build_websocket_url,
|
||||
websocket_headers,
|
||||
)
|
||||
|
||||
|
||||
class ProtocolWebSocketTransport:
|
||||
"""v3 protocol transport using HTTP commands and WebSocket events."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
client: httpx.AsyncClient,
|
||||
thread_id: str,
|
||||
commands_path: str | None = None,
|
||||
stream_path: str | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
connect: Callable[..., Any] = websocket_connect,
|
||||
max_queue_size: int = 1024,
|
||||
ping_interval: float | None = 20.0,
|
||||
ping_timeout: float | None = 20.0,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self.thread_id = thread_id
|
||||
self._commands_url = commands_path or f"/threads/{thread_id}/commands"
|
||||
self._stream_path = stream_path or f"/threads/{thread_id}/stream/events"
|
||||
self._default_headers: dict[str, str] = dict(headers or {})
|
||||
self._connect = connect
|
||||
self._max_queue_size = max_queue_size
|
||||
self._ping_interval = ping_interval
|
||||
self._ping_timeout = ping_timeout
|
||||
self._closed = False
|
||||
self._event_streams: set[asyncio.Task[None]] = set()
|
||||
|
||||
async def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if self._closed:
|
||||
raise RuntimeError("Protocol transport is closed.")
|
||||
merged_headers = {**self._default_headers, "content-type": "application/json"}
|
||||
response = await self._client.post(
|
||||
self._commands_url,
|
||||
content=orjson.dumps(command),
|
||||
headers=merged_headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
if response.status_code in (202, 204):
|
||||
return None
|
||||
payload = orjson.loads(response.content)
|
||||
if not isinstance(payload, dict) or "id" not in payload:
|
||||
raise RuntimeError("Protocol command did not return a valid response.")
|
||||
return payload
|
||||
|
||||
def open_event_stream(self, params: dict[str, Any]) -> EventStreamHandle:
|
||||
if self._closed:
|
||||
raise RuntimeError("Protocol transport is closed.")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
ready: asyncio.Future[None] = loop.create_future()
|
||||
done: asyncio.Future[BaseException | None] = loop.create_future()
|
||||
queue: asyncio.Queue[Event | None] = asyncio.Queue(maxsize=self._max_queue_size)
|
||||
cancel_event = asyncio.Event()
|
||||
ws_holder: dict[str, Any] = {"ws": None}
|
||||
|
||||
async def pump() -> None:
|
||||
try:
|
||||
url = build_websocket_url(self._client.base_url, self._stream_path)
|
||||
handshake_headers = list(websocket_headers(self._default_headers))
|
||||
cookie_header = _cookie_header(self._client)
|
||||
if cookie_header:
|
||||
handshake_headers.append(("Cookie", cookie_header))
|
||||
async with self._connect(
|
||||
url,
|
||||
additional_headers=handshake_headers,
|
||||
ping_interval=self._ping_interval,
|
||||
ping_timeout=self._ping_timeout,
|
||||
) as websocket:
|
||||
ws_holder["ws"] = websocket
|
||||
try:
|
||||
await websocket.send(
|
||||
orjson.dumps(build_event_stream_body(params)).decode()
|
||||
)
|
||||
if not ready.done():
|
||||
ready.set_result(None)
|
||||
async for raw in websocket:
|
||||
if cancel_event.is_set():
|
||||
break
|
||||
payload = _decode_frame(raw, done)
|
||||
if payload is not None:
|
||||
await queue.put(cast("Event", payload))
|
||||
finally:
|
||||
ws_holder["ws"] = None
|
||||
except asyncio.CancelledError as err:
|
||||
if not done.done():
|
||||
done.set_result(err)
|
||||
raise
|
||||
except ConnectionClosedOK:
|
||||
# Server sent close code 1000 — clean end, not an error.
|
||||
if not done.done():
|
||||
done.set_result(None)
|
||||
except ConnectionClosedError as err:
|
||||
# Abnormal close (1006) or application error (4xxx).
|
||||
if not ready.done():
|
||||
ready.set_exception(err)
|
||||
if not done.done():
|
||||
done.set_result(err)
|
||||
except Exception as err:
|
||||
if not ready.done():
|
||||
ready.set_exception(err)
|
||||
if not done.done():
|
||||
done.set_result(err)
|
||||
finally:
|
||||
if not done.done():
|
||||
done.set_result(None)
|
||||
await queue.put(None)
|
||||
|
||||
task = asyncio.create_task(pump())
|
||||
self._event_streams.add(task)
|
||||
task.add_done_callback(self._event_streams.discard)
|
||||
|
||||
async def aiter() -> AsyncIterator[Event]:
|
||||
while True:
|
||||
item = await queue.get()
|
||||
if item is None or cancel_event.is_set():
|
||||
return
|
||||
yield item
|
||||
|
||||
async def close() -> None:
|
||||
cancel_event.set()
|
||||
ws = ws_holder.get("ws")
|
||||
if ws is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await ws.close(code=1000, reason="client close")
|
||||
queue.put_nowait(None)
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError, Exception):
|
||||
await task
|
||||
|
||||
return EventStreamHandle(events=aiter(), ready=ready, done=done, close=close)
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
tasks = list(self._event_streams)
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
if tasks:
|
||||
with contextlib.suppress(Exception, asyncio.CancelledError):
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
def _decode_frame(
|
||||
raw: str | bytes | bytearray | memoryview,
|
||||
done: asyncio.Future[BaseException | None],
|
||||
) -> dict[str, Any] | None:
|
||||
"""Decode a raw WS frame into an Event dict.
|
||||
|
||||
Returns None and sets `done` if the frame is invalid JSON or not a JSON object.
|
||||
"""
|
||||
try:
|
||||
payload = orjson.loads(raw.encode() if isinstance(raw, str) else bytes(raw))
|
||||
except orjson.JSONDecodeError as err:
|
||||
if not done.done():
|
||||
done.set_result(RuntimeError(f"WS frame is not valid JSON: {err!r}"))
|
||||
return None
|
||||
if not isinstance(payload, dict):
|
||||
if not done.done():
|
||||
done.set_result(
|
||||
RuntimeError(f"WS frame is not a JSON object: {type(payload).__name__}")
|
||||
)
|
||||
return None
|
||||
return payload
|
||||
|
||||
|
||||
def _cookie_header(client: httpx.AsyncClient) -> str | None:
|
||||
"""Build a `Cookie` header value from the httpx client's cookie jar."""
|
||||
cookies = dict(client.cookies)
|
||||
if not cookies:
|
||||
return None
|
||||
return "; ".join(f"{k}={v}" for k, v in cookies.items())
|
||||
@@ -16,6 +16,7 @@ dependencies = [
|
||||
"orjson>=3.11.5",
|
||||
"langchain-protocol>=0.0.15",
|
||||
"langchain-core>=1.4.0,<2",
|
||||
"websockets>=14,<16",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
import pytest
|
||||
|
||||
from langgraph_sdk.stream.transport.sync_ws import SyncProtocolWebSocketTransport
|
||||
from streaming._events import values_event
|
||||
|
||||
|
||||
class _FakeSyncWebSocket:
|
||||
def __init__(
|
||||
self,
|
||||
frames: list[dict[str, Any]],
|
||||
*,
|
||||
fail_after: int | None = None,
|
||||
) -> None:
|
||||
self.frames = list(frames)
|
||||
self.fail_after = fail_after
|
||||
self.sent: list[str] = []
|
||||
self.closed = False
|
||||
|
||||
def __enter__(self) -> _FakeSyncWebSocket:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||||
self.close()
|
||||
|
||||
def send(self, data: str | bytes) -> None:
|
||||
self.sent.append(data.decode() if isinstance(data, bytes) else data)
|
||||
|
||||
def __iter__(self):
|
||||
for index, frame in enumerate(self.frames, start=1):
|
||||
yield orjson.dumps(frame).decode()
|
||||
if self.fail_after is not None and index >= self.fail_after:
|
||||
raise RuntimeError("scripted sync websocket failure")
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
def test_sync_websocket_sends_subscribe_body_and_yields_events():
|
||||
event = values_event(seq=1, values={"counter": 1})
|
||||
socket = _FakeSyncWebSocket([event])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
transport = SyncProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
handle = transport.open_event_stream(
|
||||
{"channels": ["values"], "namespaces": [[]], "since": 7}
|
||||
)
|
||||
received = list(handle.events)
|
||||
err = handle.error()
|
||||
handle.close()
|
||||
|
||||
assert orjson.loads(socket.sent[0]) == {
|
||||
"channels": ["values"],
|
||||
"namespaces": [[]],
|
||||
"since": 7,
|
||||
}
|
||||
assert received == [event]
|
||||
assert err is None
|
||||
|
||||
|
||||
def test_sync_websocket_records_post_ready_error():
|
||||
socket = _FakeSyncWebSocket([values_event(seq=1)], fail_after=1)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
transport = SyncProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
with pytest.raises(RuntimeError, match="scripted sync websocket failure"):
|
||||
list(handle.events)
|
||||
err = handle.error()
|
||||
handle.close()
|
||||
|
||||
assert isinstance(err, RuntimeError)
|
||||
|
||||
|
||||
def test_sync_websocket_send_command_uses_http_commands_endpoint():
|
||||
from streaming._sync_fake_server import SyncFakeServer
|
||||
|
||||
fake = SyncFakeServer()
|
||||
with httpx.Client(transport=fake.transport, base_url="http://test") as client:
|
||||
ws = SyncProtocolWebSocketTransport(client=client, thread_id="t-1")
|
||||
result = ws.send_command(
|
||||
{"id": 3, "method": "run.start", "params": {"input": {"x": 1}}}
|
||||
)
|
||||
|
||||
assert result == {"type": "success", "id": 3, "result": {"run_id": "run-1"}}
|
||||
assert fake.received_commands[0]["method"] == "run.start"
|
||||
|
||||
|
||||
def test_sync_websocket_open_event_stream_raises_when_closed():
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
ws = SyncProtocolWebSocketTransport(client=client, thread_id="t-1")
|
||||
ws.close()
|
||||
with pytest.raises(RuntimeError, match="closed"):
|
||||
ws.open_event_stream({"channels": ["values"]})
|
||||
|
||||
|
||||
def test_sync_websocket_transport_feeds_sync_stream_controller():
|
||||
from langgraph_sdk.stream.sync_controller import SyncStreamController
|
||||
|
||||
socket = _FakeSyncWebSocket(
|
||||
[
|
||||
values_event(seq=1, values={"counter": 1}),
|
||||
values_event(seq=2, values={"counter": 2}),
|
||||
]
|
||||
)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
transport = SyncProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
controller = SyncStreamController(transport)
|
||||
sub = controller.register_subscription({"channels": ["values"]})
|
||||
controller.reconcile_stream({"channels": ["values"]})
|
||||
controller.ensure_fanout_running()
|
||||
|
||||
first = sub.queue.get(timeout=1.0)
|
||||
second = sub.queue.get(timeout=1.0)
|
||||
end = sub.queue.get(timeout=1.0)
|
||||
controller.close()
|
||||
transport.close()
|
||||
|
||||
assert first is not None
|
||||
assert second is not None
|
||||
assert first["seq"] == 1
|
||||
assert second["seq"] == 2
|
||||
assert end is None
|
||||
|
||||
|
||||
def test_sync_websocket_controller_reconnects_with_since_after_drop():
|
||||
from langgraph_sdk.stream.sync_controller import SyncStreamController
|
||||
|
||||
first_socket = _FakeSyncWebSocket(
|
||||
[values_event(seq=1, values={"counter": 1})],
|
||||
fail_after=1,
|
||||
)
|
||||
second_socket = _FakeSyncWebSocket([values_event(seq=2, values={"counter": 2})])
|
||||
sockets = [first_socket, second_socket]
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return sockets.pop(0)
|
||||
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
transport = SyncProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
controller = SyncStreamController(transport)
|
||||
sub = controller.register_subscription({"channels": ["values"]})
|
||||
controller.reconcile_stream({"channels": ["values"]})
|
||||
controller.ensure_fanout_running()
|
||||
|
||||
first = sub.queue.get(timeout=1.0)
|
||||
second = sub.queue.get(timeout=1.0)
|
||||
end = sub.queue.get(timeout=1.0)
|
||||
controller.close()
|
||||
transport.close()
|
||||
|
||||
assert first is not None
|
||||
assert second is not None
|
||||
assert first["seq"] == 1
|
||||
assert second["seq"] == 2
|
||||
assert end is None
|
||||
assert orjson.loads(second_socket.sent[0])["since"] == 1
|
||||
|
||||
|
||||
def test_sync_ws_transport_forwards_ping_kwargs():
|
||||
"""ping_interval and ping_timeout are stored and forwarded to the connect callable."""
|
||||
captured_kwargs: list[dict] = []
|
||||
socket = _FakeSyncWebSocket([values_event(seq=1)])
|
||||
|
||||
def connect(
|
||||
url: str,
|
||||
additional_headers: list[tuple[str, str]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
_ = (url, additional_headers)
|
||||
captured_kwargs.append(kwargs)
|
||||
return socket
|
||||
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
transport = SyncProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
ping_interval=15.0,
|
||||
ping_timeout=20.0,
|
||||
)
|
||||
assert transport._ping_interval == 15.0
|
||||
assert transport._ping_timeout == 20.0
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
list(handle.events)
|
||||
handle.close()
|
||||
|
||||
assert len(captured_kwargs) == 1
|
||||
assert captured_kwargs[0].get("ping_interval") == 15.0
|
||||
assert captured_kwargs[0].get("ping_timeout") == 20.0
|
||||
|
||||
|
||||
def test_sync_ws_handshake_forwards_httpx_client_cookies():
|
||||
"""Cookies on the httpx.Client are forwarded to the WS handshake."""
|
||||
captured_headers: list[list[tuple[str, str]]] = []
|
||||
socket = _FakeSyncWebSocket([values_event(seq=1)])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = url
|
||||
captured_headers.append(list(additional_headers or []))
|
||||
return socket
|
||||
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
client.cookies.set("session", "abc123")
|
||||
transport = SyncProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
list(handle.events)
|
||||
handle.close()
|
||||
|
||||
assert len(captured_headers) == 1
|
||||
headers_dict = dict(captured_headers[0])
|
||||
assert "Cookie" in headers_dict
|
||||
assert "session=abc123" in headers_dict["Cookie"]
|
||||
|
||||
|
||||
def test_sync_close_before_iteration_closes_socket():
|
||||
"""Calling `handle.close()` before iterating events must close the socket."""
|
||||
connect_calls: list[str] = []
|
||||
socket = _FakeSyncWebSocket([values_event(seq=1)])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
connect_calls.append(url)
|
||||
return socket
|
||||
|
||||
with httpx.Client(base_url="http://test") as client:
|
||||
transport = SyncProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
# Close without consuming any events.
|
||||
handle.close()
|
||||
|
||||
assert socket.closed
|
||||
@@ -0,0 +1,612 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
import pytest
|
||||
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
|
||||
from websockets.frames import Close
|
||||
|
||||
from langgraph_sdk.stream.transport.ws import ProtocolWebSocketTransport
|
||||
from streaming._events import values_event
|
||||
|
||||
|
||||
class _FakeAsyncWebSocket:
|
||||
def __init__(
|
||||
self,
|
||||
frames: list[dict[str, Any]],
|
||||
*,
|
||||
fail_after: int | None = None,
|
||||
) -> None:
|
||||
self.frames = list(frames)
|
||||
self.fail_after = fail_after
|
||||
self.sent: list[str] = []
|
||||
self.closed = False
|
||||
|
||||
async def __aenter__(self) -> _FakeAsyncWebSocket:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||||
await self.close()
|
||||
|
||||
async def send(self, data: str | bytes) -> None:
|
||||
self.sent.append(data.decode() if isinstance(data, bytes) else data)
|
||||
|
||||
def __aiter__(self) -> AsyncIterator[str]:
|
||||
return self._iter()
|
||||
|
||||
async def _iter(self) -> AsyncIterator[str]:
|
||||
for index, frame in enumerate(self.frames, start=1):
|
||||
yield orjson.dumps(frame).decode()
|
||||
if self.fail_after is not None and index >= self.fail_after:
|
||||
raise RuntimeError("scripted websocket failure")
|
||||
|
||||
async def close(self, code: int = 1000, reason: str = "") -> None: # noqa: ARG002
|
||||
self.closed = True
|
||||
self.close_code = code
|
||||
|
||||
|
||||
class _ClosingFakeAsyncWebSocket:
|
||||
"""Fake websocket that yields frames then raises a ConnectionClosed variant."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frames: list[dict[str, Any]],
|
||||
*,
|
||||
close_exc: BaseException,
|
||||
) -> None:
|
||||
self.frames = list(frames)
|
||||
self.close_exc = close_exc
|
||||
self.sent: list[str] = []
|
||||
self.closed = False
|
||||
|
||||
async def __aenter__(self) -> _ClosingFakeAsyncWebSocket:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||||
self.closed = True
|
||||
|
||||
async def send(self, data: str | bytes) -> None:
|
||||
self.sent.append(data.decode() if isinstance(data, bytes) else data)
|
||||
|
||||
def __aiter__(self) -> AsyncIterator[str]:
|
||||
return self._iter()
|
||||
|
||||
async def _iter(self) -> AsyncIterator[str]:
|
||||
for frame in self.frames:
|
||||
yield orjson.dumps(frame).decode()
|
||||
raise self.close_exc
|
||||
|
||||
async def close(self, code: int = 1000, reason: str = "") -> None: # noqa: ARG002
|
||||
self.closed = True
|
||||
|
||||
|
||||
async def test_ws_close_code_1000_resolves_done_with_none():
|
||||
"""Close code 1000 (normal closure) resolves `done` with None."""
|
||||
socket = _ClosingFakeAsyncWebSocket(
|
||||
[values_event(seq=1)],
|
||||
close_exc=ConnectionClosedOK(Close(1000, "OK"), None),
|
||||
)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
_ = [e async for e in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert err is None
|
||||
|
||||
|
||||
async def test_ws_close_code_1006_resolves_done_with_error():
|
||||
"""Close code 1006 (abnormal) resolves `done` with ConnectionClosedError."""
|
||||
socket = _ClosingFakeAsyncWebSocket(
|
||||
[],
|
||||
close_exc=ConnectionClosedError(None, Close(1006, "abnormal")),
|
||||
)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
_ = [e async for e in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert isinstance(err, ConnectionClosedError)
|
||||
|
||||
|
||||
async def test_ws_close_code_4000_resolves_done_with_error():
|
||||
"""4xxx close codes resolve `done` with ConnectionClosedError."""
|
||||
socket = _ClosingFakeAsyncWebSocket(
|
||||
[],
|
||||
close_exc=ConnectionClosedError(None, Close(4000, "app error")),
|
||||
)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
_ = [e async for e in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert isinstance(err, ConnectionClosedError)
|
||||
|
||||
|
||||
class _RawFrameAsyncWebSocket:
|
||||
"""Fake websocket that yields arbitrary raw strings (not dicts)."""
|
||||
|
||||
def __init__(self, raw_frames: list[str]) -> None:
|
||||
self.raw_frames = list(raw_frames)
|
||||
self.sent: list[str] = []
|
||||
self.closed = False
|
||||
|
||||
async def __aenter__(self) -> _RawFrameAsyncWebSocket:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||||
self.closed = True
|
||||
|
||||
async def send(self, data: str | bytes) -> None:
|
||||
self.sent.append(data.decode() if isinstance(data, bytes) else data)
|
||||
|
||||
def __aiter__(self) -> AsyncIterator[str]:
|
||||
return self._iter()
|
||||
|
||||
async def _iter(self) -> AsyncIterator[str]:
|
||||
for frame in self.raw_frames:
|
||||
yield frame
|
||||
|
||||
async def close(self, code: int = 1000, reason: str = "") -> None: # noqa: ARG002
|
||||
self.closed = True
|
||||
|
||||
|
||||
async def test_decode_frame_invalid_json_surfaces_error_on_done():
|
||||
"""A WS frame with invalid JSON resolves `done` with a RuntimeError."""
|
||||
socket = _RawFrameAsyncWebSocket(["not-valid-json{{{"])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
received = [e async for e in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert received == []
|
||||
assert isinstance(err, RuntimeError)
|
||||
assert "not valid JSON" in str(err)
|
||||
|
||||
|
||||
async def test_decode_frame_non_dict_surfaces_error_on_done():
|
||||
"""A WS frame that is valid JSON but not an object resolves `done` with RuntimeError."""
|
||||
socket = _RawFrameAsyncWebSocket(["[1, 2, 3]"])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
received = [e async for e in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert received == []
|
||||
assert isinstance(err, RuntimeError)
|
||||
assert "not a JSON object" in str(err)
|
||||
|
||||
|
||||
async def test_websocket_url_uses_ws_scheme_and_base_path():
|
||||
seen: list[tuple[str, list[tuple[str, str]] | None]] = []
|
||||
socket = _FakeAsyncWebSocket([])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
seen.append((url, additional_headers))
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="https://example.com/api") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
headers={"x-test": "1"},
|
||||
connect=connect,
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
_ = [event async for event in handle.events]
|
||||
await handle.close()
|
||||
|
||||
assert seen == [
|
||||
(
|
||||
"wss://example.com/api/threads/t-1/stream/events",
|
||||
[("x-test", "1")],
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def test_websocket_sends_subscribe_body_and_yields_events():
|
||||
event = values_event(seq=1, values={"counter": 1})
|
||||
socket = _FakeAsyncWebSocket([event])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
handle = transport.open_event_stream(
|
||||
{"channels": ["values"], "namespaces": [[]], "since": 7}
|
||||
)
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
received = [event async for event in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert orjson.loads(socket.sent[0]) == {
|
||||
"channels": ["values"],
|
||||
"namespaces": [[]],
|
||||
"since": 7,
|
||||
}
|
||||
assert received == [event]
|
||||
assert err is None
|
||||
|
||||
|
||||
async def test_websocket_done_records_post_ready_error():
|
||||
socket = _FakeAsyncWebSocket([values_event(seq=1)], fail_after=1)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
received = [event async for event in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert received == [values_event(seq=1)]
|
||||
assert isinstance(err, RuntimeError)
|
||||
assert "scripted websocket failure" in str(err)
|
||||
|
||||
|
||||
async def test_websocket_send_command_uses_http_commands_endpoint():
|
||||
from streaming._fake_server import FakeServer
|
||||
|
||||
fake = FakeServer()
|
||||
transport = httpx.ASGITransport(app=fake.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
ws = ProtocolWebSocketTransport(client=client, thread_id="t-1")
|
||||
result = await ws.send_command(
|
||||
{"id": 3, "method": "run.start", "params": {"input": {"x": 1}}}
|
||||
)
|
||||
|
||||
assert result == {"type": "success", "id": 3, "result": {"run_id": "run-1"}}
|
||||
assert fake.received_commands[0]["method"] == "run.start"
|
||||
|
||||
|
||||
async def test_websocket_open_event_stream_raises_when_closed():
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
ws = ProtocolWebSocketTransport(client=client, thread_id="t-1")
|
||||
await ws.close()
|
||||
with pytest.raises(RuntimeError, match="closed"):
|
||||
ws.open_event_stream({"channels": ["values"]})
|
||||
|
||||
|
||||
async def test_websocket_transport_feeds_async_stream_controller():
|
||||
from langgraph_sdk.stream.controller import StreamController
|
||||
|
||||
socket = _FakeAsyncWebSocket(
|
||||
[
|
||||
values_event(seq=1, values={"counter": 1}),
|
||||
values_event(seq=2, values={"counter": 2}),
|
||||
]
|
||||
)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
|
||||
async def gate() -> None:
|
||||
return None
|
||||
|
||||
controller = StreamController(transport=transport, run_start_gate=gate)
|
||||
sub = controller.register_subscription({"channels": ["values"]})
|
||||
await controller.reconcile_stream({"channels": ["values"]})
|
||||
controller.ensure_fanout_running()
|
||||
|
||||
first = await asyncio.wait_for(sub.queue.get(), timeout=1.0)
|
||||
second = await asyncio.wait_for(sub.queue.get(), timeout=1.0)
|
||||
end = await asyncio.wait_for(sub.queue.get(), timeout=1.0)
|
||||
await controller.close()
|
||||
await transport.close()
|
||||
|
||||
assert first["seq"] == 1
|
||||
assert second["seq"] == 2
|
||||
assert end is None
|
||||
|
||||
|
||||
async def test_ws_transport_accepts_max_queue_size_kwarg():
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
max_queue_size=42,
|
||||
)
|
||||
assert transport._max_queue_size == 42
|
||||
|
||||
|
||||
async def test_ws_transport_default_max_queue_size_is_1024():
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
)
|
||||
assert transport._max_queue_size == 1024
|
||||
|
||||
|
||||
async def test_websocket_controller_reconnects_with_since_after_drop():
|
||||
from langgraph_sdk.stream.controller import StreamController
|
||||
|
||||
first_socket = _FakeAsyncWebSocket(
|
||||
[values_event(seq=1, values={"counter": 1})],
|
||||
fail_after=1,
|
||||
)
|
||||
second_socket = _FakeAsyncWebSocket([values_event(seq=2, values={"counter": 2})])
|
||||
sockets = [first_socket, second_socket]
|
||||
sent_urls: list[str] = []
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = additional_headers
|
||||
sent_urls.append(url)
|
||||
return sockets.pop(0)
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
)
|
||||
|
||||
async def gate() -> None:
|
||||
return None
|
||||
|
||||
controller = StreamController(transport=transport, run_start_gate=gate)
|
||||
sub = controller.register_subscription({"channels": ["values"]})
|
||||
await controller.reconcile_stream({"channels": ["values"]})
|
||||
controller.ensure_fanout_running()
|
||||
|
||||
first = await asyncio.wait_for(sub.queue.get(), timeout=1.0)
|
||||
second = await asyncio.wait_for(sub.queue.get(), timeout=1.0)
|
||||
end = await asyncio.wait_for(sub.queue.get(), timeout=1.0)
|
||||
await controller.close()
|
||||
await transport.close()
|
||||
|
||||
assert first["seq"] == 1
|
||||
assert second["seq"] == 2
|
||||
assert end is None
|
||||
assert len(sent_urls) == 2
|
||||
assert orjson.loads(second_socket.sent[0])["since"] == 1
|
||||
|
||||
|
||||
async def test_async_close_sends_normal_close_frame():
|
||||
"""`handle.close()` sends a WebSocket close frame with code 1000 explicitly."""
|
||||
import asyncio
|
||||
|
||||
# Use an event to distinguish an explicit close(code=1000) call from
|
||||
# the implicit one in __aexit__ when the task is cancelled.
|
||||
explicit_1000: list[bool] = []
|
||||
ready_event = asyncio.Event()
|
||||
|
||||
class _TrackingWebSocket(_FakeAsyncWebSocket):
|
||||
def __aiter__(self) -> AsyncIterator[str]:
|
||||
return self._wait_forever()
|
||||
|
||||
async def _wait_forever(self) -> AsyncIterator[str]: # type: ignore[override]
|
||||
ready_event.set()
|
||||
# Block until cancelled — never yield frames.
|
||||
await asyncio.sleep(9999)
|
||||
return
|
||||
yield # make it an async generator
|
||||
|
||||
async def close( # type: ignore[override]
|
||||
self,
|
||||
code: int = 1000,
|
||||
reason: str = "", # noqa: ARG002
|
||||
) -> None:
|
||||
explicit_1000.append(code == 1000)
|
||||
self.closed = True
|
||||
|
||||
socket = _TrackingWebSocket([])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
await asyncio.wait_for(ready_event.wait(), timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
# close() must have been called at least once with code=1000.
|
||||
assert explicit_1000, "ws.close() was never called"
|
||||
assert explicit_1000[0], "first ws.close() call did not use code=1000"
|
||||
|
||||
|
||||
async def test_ws_transport_forwards_ping_kwargs():
|
||||
"""ping_interval and ping_timeout are stored and forwarded to websockets.connect."""
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
ping_interval=15.0,
|
||||
ping_timeout=20.0,
|
||||
)
|
||||
assert transport._ping_interval == 15.0
|
||||
assert transport._ping_timeout == 20.0
|
||||
|
||||
|
||||
async def test_ws_transport_ping_kwargs_forwarded_to_connect():
|
||||
"""ping_interval and ping_timeout are passed through to the connect callable."""
|
||||
captured_kwargs: list[dict[str, Any]] = []
|
||||
socket = _FakeAsyncWebSocket([])
|
||||
|
||||
def connect(
|
||||
url: str,
|
||||
additional_headers: list[tuple[str, str]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
_ = (url, additional_headers)
|
||||
captured_kwargs.append(kwargs)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client,
|
||||
thread_id="t-1",
|
||||
connect=connect,
|
||||
ping_interval=15.0,
|
||||
ping_timeout=20.0,
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
_ = [e async for e in handle.events]
|
||||
await handle.close()
|
||||
|
||||
assert len(captured_kwargs) == 1
|
||||
assert captured_kwargs[0].get("ping_interval") == 15.0
|
||||
assert captured_kwargs[0].get("ping_timeout") == 20.0
|
||||
|
||||
|
||||
async def test_ws_handshake_forwards_httpx_client_cookies():
|
||||
"""Cookies set on the httpx.AsyncClient are forwarded to the WS handshake."""
|
||||
captured_headers: list[list[tuple[str, str]]] = []
|
||||
socket = _FakeAsyncWebSocket([])
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = url
|
||||
captured_headers.append(list(additional_headers or []))
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
client.cookies.set("session", "abc123")
|
||||
client.cookies.set("other", "xyz")
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
_ = [e async for e in handle.events]
|
||||
await handle.close()
|
||||
|
||||
assert len(captured_headers) == 1
|
||||
headers_dict = dict(captured_headers[0])
|
||||
assert "Cookie" in headers_dict
|
||||
cookie_val = headers_dict["Cookie"]
|
||||
assert "session=abc123" in cookie_val
|
||||
assert "other=xyz" in cookie_val
|
||||
|
||||
|
||||
async def test_ws_server_initiated_mid_stream_close_surfaces_error():
|
||||
"""A server that closes mid-stream with code 1011 surfaces ConnectionClosedError on done."""
|
||||
socket = _ClosingFakeAsyncWebSocket(
|
||||
[values_event(seq=1)],
|
||||
close_exc=ConnectionClosedError(None, Close(1011, "server error")),
|
||||
)
|
||||
|
||||
def connect(
|
||||
url: str, additional_headers: list[tuple[str, str]] | None = None, **_kw: Any
|
||||
):
|
||||
_ = (url, additional_headers)
|
||||
return socket
|
||||
|
||||
async with httpx.AsyncClient(base_url="http://test") as client:
|
||||
transport = ProtocolWebSocketTransport(
|
||||
client=client, thread_id="t-1", connect=connect
|
||||
)
|
||||
handle = transport.open_event_stream({"channels": ["values"]})
|
||||
await asyncio.wait_for(handle.ready, timeout=1.0)
|
||||
received = [e async for e in handle.events]
|
||||
err = await asyncio.wait_for(handle.done, timeout=1.0)
|
||||
await handle.close()
|
||||
|
||||
assert received == [values_event(seq=1)]
|
||||
assert isinstance(err, ConnectionClosedError)
|
||||
Generated
+61
@@ -487,6 +487,7 @@ dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langchain-protocol" },
|
||||
{ name = "orjson" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -523,6 +524,7 @@ requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
|
||||
{ name = "langchain-protocol", specifier = ">=0.0.15" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "websockets", specifier = ">=14,<16" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -1392,6 +1394,65 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xxhash"
|
||||
version = "3.6.0"
|
||||
|
||||
Reference in New Issue
Block a user